@hexclave/next 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/clickmap/clickmap-core.d.ts +15 -0
- package/dist/clickmap/clickmap-core.d.ts.map +1 -0
- package/dist/clickmap/clickmap-core.js +1527 -0
- package/dist/clickmap/clickmap-core.js.map +1 -0
- package/dist/clickmap/clickmap-styles.d.ts +5 -0
- package/dist/clickmap/clickmap-styles.d.ts.map +1 -0
- package/dist/clickmap/clickmap-styles.js +1095 -0
- package/dist/clickmap/clickmap-styles.js.map +1 -0
- package/dist/clickmap/index.d.ts +16 -0
- package/dist/clickmap/index.d.ts.map +1 -0
- package/dist/clickmap/index.js +74 -0
- package/dist/clickmap/index.js.map +1 -0
- package/dist/components/api-key-dialogs.d.ts +1 -1
- package/dist/components/api-key-dialogs.js +5 -5
- package/dist/components/credential-sign-in.js +3 -3
- package/dist/components/credential-sign-up.js +5 -5
- package/dist/components/elements/sidebar-layout.js +1 -1
- package/dist/components/elements/user-avatar.js +1 -1
- package/dist/components/magic-link-sign-in.js +5 -5
- package/dist/components/message-cards/known-error-message-card.d.ts +1 -1
- package/dist/components/message-cards/predefined-message-card.js +1 -1
- package/dist/components/passkey-button.js +1 -1
- package/dist/components/profile-image-editor.js +1 -1
- package/dist/components/team-icon.js +1 -1
- package/dist/components/team-switcher.js +2 -2
- package/dist/components/user-button.js +1 -1
- package/dist/components-page/account-settings/active-sessions/active-sessions-page.js +1 -1
- package/dist/components-page/account-settings/editable-text.js +1 -1
- package/dist/components-page/account-settings/email-and-auth/emails-section.js +3 -3
- package/dist/components-page/account-settings/email-and-auth/mfa-section.js +1 -1
- package/dist/components-page/account-settings/email-and-auth/password-section.js +3 -3
- package/dist/components-page/account-settings/teams/team-api-keys-section.js +1 -1
- package/dist/components-page/account-settings/teams/team-creation-page.js +3 -3
- package/dist/components-page/account-settings/teams/team-member-invitation-section.js +4 -4
- package/dist/components-page/account-settings.js +3 -3
- package/dist/components-page/auth-page.js +2 -2
- package/dist/components-page/cli-auth-confirm.js +2 -2
- package/dist/components-page/cli-auth-confirm.test.js +1 -1
- package/dist/components-page/email-verification.js +1 -1
- package/dist/components-page/forgot-password.d.ts.map +1 -1
- package/dist/components-page/forgot-password.js +6 -7
- package/dist/components-page/forgot-password.js.map +1 -1
- package/dist/components-page/hexclave-handler-client.d.ts +1 -1
- package/dist/components-page/hexclave-handler-client.js +2 -2
- package/dist/components-page/magic-link-callback.js +1 -1
- package/dist/components-page/mfa.js +7 -22
- package/dist/components-page/mfa.js.map +1 -1
- package/dist/components-page/oauth-callback.js +2 -2
- package/dist/components-page/onboarding.js +4 -4
- package/dist/components-page/password-reset.d.ts.map +1 -1
- package/dist/components-page/password-reset.js +12 -14
- package/dist/components-page/password-reset.js.map +1 -1
- package/dist/components-page/team-creation.js +5 -5
- package/dist/dev-tool/dev-tool-core.d.ts.map +1 -1
- package/dist/dev-tool/dev-tool-core.js +258 -262
- package/dist/dev-tool/dev-tool-core.js.map +1 -1
- package/dist/dev-tool/dev-tool-styles.d.ts +1 -1
- package/dist/dev-tool/dev-tool-styles.d.ts.map +1 -1
- package/dist/dev-tool/dev-tool-styles.js +13 -143
- package/dist/dev-tool/dev-tool-styles.js.map +1 -1
- package/dist/dev-tool/index.d.ts.map +1 -1
- package/dist/dev-tool/index.js +4 -11
- package/dist/dev-tool/index.js.map +1 -1
- package/dist/esm/clickmap/clickmap-core.d.ts +15 -0
- package/dist/esm/clickmap/clickmap-core.d.ts.map +1 -0
- package/dist/esm/clickmap/clickmap-core.js +1525 -0
- package/dist/esm/clickmap/clickmap-core.js.map +1 -0
- package/dist/esm/clickmap/clickmap-styles.d.ts +5 -0
- package/dist/esm/clickmap/clickmap-styles.d.ts.map +1 -0
- package/dist/esm/clickmap/clickmap-styles.js +1093 -0
- package/dist/esm/clickmap/clickmap-styles.js.map +1 -0
- package/dist/esm/clickmap/index.d.ts +16 -0
- package/dist/esm/clickmap/index.d.ts.map +1 -0
- package/dist/esm/clickmap/index.js +72 -0
- package/dist/esm/clickmap/index.js.map +1 -0
- package/dist/esm/components/api-key-dialogs.d.ts +1 -1
- package/dist/esm/components/api-key-dialogs.js +5 -5
- package/dist/esm/components/credential-sign-in.js +3 -3
- package/dist/esm/components/credential-sign-up.js +5 -5
- package/dist/esm/components/elements/sidebar-layout.js +1 -1
- package/dist/esm/components/elements/user-avatar.js +1 -1
- package/dist/esm/components/magic-link-sign-in.js +5 -5
- package/dist/esm/components/message-cards/predefined-message-card.js +1 -1
- package/dist/esm/components/passkey-button.js +1 -1
- package/dist/esm/components/profile-image-editor.js +1 -1
- package/dist/esm/components/team-icon.js +1 -1
- package/dist/esm/components/team-switcher.js +2 -2
- package/dist/esm/components/user-button.js +1 -1
- package/dist/esm/components-page/account-settings/active-sessions/active-sessions-page.js +1 -1
- package/dist/esm/components-page/account-settings/editable-text.js +1 -1
- package/dist/esm/components-page/account-settings/email-and-auth/emails-section.js +3 -3
- package/dist/esm/components-page/account-settings/email-and-auth/mfa-section.js +1 -1
- package/dist/esm/components-page/account-settings/email-and-auth/password-section.js +3 -3
- package/dist/esm/components-page/account-settings/teams/team-api-keys-section.js +1 -1
- package/dist/esm/components-page/account-settings/teams/team-creation-page.js +3 -3
- package/dist/esm/components-page/account-settings/teams/team-member-invitation-section.js +4 -4
- package/dist/esm/components-page/account-settings.d.ts +1 -1
- package/dist/esm/components-page/account-settings.js +3 -3
- package/dist/esm/components-page/auth-page.js +2 -2
- package/dist/esm/components-page/cli-auth-confirm.js +2 -2
- package/dist/esm/components-page/cli-auth-confirm.test.js +1 -1
- package/dist/esm/components-page/email-verification.js +1 -1
- package/dist/esm/components-page/forgot-password.d.ts.map +1 -1
- package/dist/esm/components-page/forgot-password.js +6 -7
- package/dist/esm/components-page/forgot-password.js.map +1 -1
- package/dist/esm/components-page/hexclave-handler-client.d.ts +1 -1
- package/dist/esm/components-page/hexclave-handler-client.js +2 -2
- package/dist/esm/components-page/magic-link-callback.js +1 -1
- package/dist/esm/components-page/mfa.js +7 -22
- package/dist/esm/components-page/mfa.js.map +1 -1
- package/dist/esm/components-page/oauth-callback.js +2 -2
- package/dist/esm/components-page/onboarding.js +4 -4
- package/dist/esm/components-page/password-reset.d.ts.map +1 -1
- package/dist/esm/components-page/password-reset.js +11 -13
- package/dist/esm/components-page/password-reset.js.map +1 -1
- package/dist/esm/components-page/team-creation.js +5 -5
- package/dist/esm/dev-tool/dev-tool-core.d.ts.map +1 -1
- package/dist/esm/dev-tool/dev-tool-core.js +35 -39
- package/dist/esm/dev-tool/dev-tool-core.js.map +1 -1
- package/dist/esm/dev-tool/dev-tool-styles.d.ts +1 -1
- package/dist/esm/dev-tool/dev-tool-styles.d.ts.map +1 -1
- package/dist/esm/dev-tool/dev-tool-styles.js +13 -143
- package/dist/esm/dev-tool/dev-tool-styles.js.map +1 -1
- package/dist/esm/dev-tool/index.d.ts.map +1 -1
- package/dist/esm/dev-tool/index.js +1 -8
- package/dist/esm/dev-tool/index.js.map +1 -1
- package/dist/esm/generated/global-css.d.ts +1 -1
- package/dist/esm/generated/global-css.js +1 -1
- package/dist/esm/generated/global-css.js.map +1 -1
- package/dist/esm/generated/quetzal-translations.d.ts +2 -2
- package/dist/esm/in-page-ui/base-styles.d.ts +5 -0
- package/dist/esm/in-page-ui/base-styles.d.ts.map +1 -0
- package/dist/esm/in-page-ui/base-styles.js +166 -0
- package/dist/esm/in-page-ui/base-styles.js.map +1 -0
- package/dist/esm/in-page-ui/dom.d.ts +15 -0
- package/dist/esm/in-page-ui/dom.d.ts.map +1 -0
- package/dist/esm/in-page-ui/dom.js +44 -0
- package/dist/esm/in-page-ui/dom.js.map +1 -0
- package/dist/esm/lib/auth.js +2 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +6 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js +21 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js +4 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/common.js +2 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts +13 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js +146 -14
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js +221 -0
- package/dist/esm/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +2 -2
- package/dist/esm/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts +5 -0
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
- package/dist/esm/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
- package/dist/esm/lib/hexclave-app/users/index.d.ts +1 -1
- package/dist/esm/providers/theme-provider.js +1 -1
- package/dist/esm/providers/translation-provider.js +1 -1
- package/dist/generated/global-css.d.ts +1 -1
- package/dist/generated/global-css.js +1 -1
- package/dist/generated/global-css.js.map +1 -1
- package/dist/generated/quetzal-translations.d.ts +2 -2
- package/dist/in-page-ui/base-styles.d.ts +5 -0
- package/dist/in-page-ui/base-styles.d.ts.map +1 -0
- package/dist/in-page-ui/base-styles.js +168 -0
- package/dist/in-page-ui/base-styles.js.map +1 -0
- package/dist/in-page-ui/dom.d.ts +15 -0
- package/dist/in-page-ui/dom.d.ts.map +1 -0
- package/dist/in-page-ui/dom.js +51 -0
- package/dist/in-page-ui/dom.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/integrations/convex/component/convex.config.d.ts +1 -1
- package/dist/lib/auth.js +2 -2
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts +5 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js +21 -1
- package/dist/lib/hexclave-app/apps/implementations/admin-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js +4 -2
- package/dist/lib/hexclave-app/apps/implementations/client-app-impl.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/common.js +2 -2
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts +13 -0
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.js +146 -14
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js +221 -0
- package/dist/lib/hexclave-app/apps/implementations/event-tracker.test.js.map +1 -1
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.d.ts +1 -1
- package/dist/lib/hexclave-app/apps/implementations/server-app-impl.js +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts +5 -0
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.d.ts.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/admin-app.js.map +1 -1
- package/dist/lib/hexclave-app/apps/interfaces/server-app.d.ts +1 -1
- package/dist/lib/hexclave-app/common.d.ts +1 -1
- package/dist/providers/hexclave-provider-client.d.ts +1 -1
- package/dist/providers/theme-provider.js +1 -1
- package/dist/providers/translation-provider.js +1 -1
- package/dist/{storage-CKzvsBxG.d.ts → storage-ksajV_p6.d.ts} +1 -1
- package/dist/{storage-CKzvsBxG.d.ts.map → storage-ksajV_p6.d.ts.map} +1 -1
- package/package.json +4 -4
- package/src/clickmap/clickmap-core.ts +1997 -0
- package/src/clickmap/clickmap-styles.ts +1102 -0
- package/src/clickmap/index.ts +95 -0
- package/src/components-page/forgot-password.tsx +1 -2
- package/src/components-page/mfa.tsx +12 -21
- package/src/components-page/password-reset.tsx +4 -6
- package/src/dev-tool/dev-tool-core.ts +38 -65
- package/src/dev-tool/dev-tool-styles.ts +13 -142
- package/src/dev-tool/index.ts +1 -14
- package/src/in-page-ui/base-styles.ts +171 -0
- package/src/in-page-ui/dom.ts +80 -0
- package/src/lib/hexclave-app/apps/implementations/admin-app-impl.ts +23 -1
- package/src/lib/hexclave-app/apps/implementations/client-app-impl.ts +7 -0
- package/src/lib/hexclave-app/apps/implementations/event-tracker.test.ts +287 -0
- package/src/lib/hexclave-app/apps/implementations/event-tracker.ts +226 -16
- package/src/lib/hexclave-app/apps/interfaces/admin-app.ts +3 -0
|
@@ -0,0 +1,1525 @@
|
|
|
1
|
+
import { AnalyticsClickmapResponseBodySchema } from "@hexclave/shared/dist/interface/admin-metrics";
|
|
2
|
+
import { CLICKMAP_OVERLAY_RESUME_STORAGE_KEY, CLICKMAP_OVERLAY_TOKEN_STORAGE_KEY, CLICKMAP_OVERLAY_TOKEN_UPDATED_EVENT } from "@hexclave/shared/dist/utils/analytics-clickmap-overlay";
|
|
3
|
+
import { CLICKMAP_ROOT_ID, DEV_TOOL_ROOT_ID } from "@hexclave/shared/dist/utils/dev-tool";
|
|
4
|
+
import { cssEscapeIdent } from "@hexclave/shared/dist/utils/dom";
|
|
5
|
+
import { buildElementsChain, parseElementsChain } from "@hexclave/shared/dist/utils/elements-chain";
|
|
6
|
+
import { runAsynchronously } from "@hexclave/shared/dist/utils/promises";
|
|
7
|
+
import { stringCompare } from "@hexclave/shared/dist/utils/strings";
|
|
8
|
+
import { getGlobalUiInstance, h, hasAppendChild, setGlobalUiInstance, setHtml } from "../in-page-ui/dom.js";
|
|
9
|
+
import { hexclaveAppInternalsSymbol } from "../lib/hexclave-app/common.js";
|
|
10
|
+
import { clickmapCSS } from "./clickmap-styles.js";
|
|
11
|
+
|
|
12
|
+
//#region src/clickmap/clickmap-core.ts
|
|
13
|
+
const CLICKMAP_FILTERS_STORAGE_KEY = "hexclave-clickmap-overlay-filters";
|
|
14
|
+
const CLICKMAP_DEFAULT_FILTERS = {
|
|
15
|
+
range: "7d",
|
|
16
|
+
device: "all",
|
|
17
|
+
urlPattern: "",
|
|
18
|
+
elementSearch: "",
|
|
19
|
+
showDead: false
|
|
20
|
+
};
|
|
21
|
+
const CLICKMAP_RANGE_MS = {
|
|
22
|
+
"24h": 1440 * 60 * 1e3,
|
|
23
|
+
"7d": 10080 * 60 * 1e3,
|
|
24
|
+
"30d": 720 * 60 * 60 * 1e3
|
|
25
|
+
};
|
|
26
|
+
const CLICKMAP_VIEWPORT_BUCKETS = {
|
|
27
|
+
mobile: {
|
|
28
|
+
min: 0,
|
|
29
|
+
max: 767
|
|
30
|
+
},
|
|
31
|
+
tablet: {
|
|
32
|
+
min: 768,
|
|
33
|
+
max: 1023
|
|
34
|
+
},
|
|
35
|
+
laptop: {
|
|
36
|
+
min: 1024,
|
|
37
|
+
max: 1199
|
|
38
|
+
},
|
|
39
|
+
desktop: {
|
|
40
|
+
min: 1200,
|
|
41
|
+
max: 1439
|
|
42
|
+
},
|
|
43
|
+
widescreen: {
|
|
44
|
+
min: 1440,
|
|
45
|
+
max: 1919
|
|
46
|
+
},
|
|
47
|
+
tv: {
|
|
48
|
+
min: 1920,
|
|
49
|
+
max: null
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
function getClickmapViewportBucket(device) {
|
|
53
|
+
if (device === "all") return null;
|
|
54
|
+
return CLICKMAP_VIEWPORT_BUCKETS[device];
|
|
55
|
+
}
|
|
56
|
+
function isClickmapViewportWidthInBucket(width, bucket) {
|
|
57
|
+
return width >= bucket.min && (bucket.max == null || width <= bucket.max);
|
|
58
|
+
}
|
|
59
|
+
function getClickmapRecommendedViewportWidth(bucket) {
|
|
60
|
+
if (bucket.max == null) return bucket.min;
|
|
61
|
+
return Math.round((bucket.min + bucket.max) / 2);
|
|
62
|
+
}
|
|
63
|
+
function formatClickmapViewportBucket(bucket) {
|
|
64
|
+
if (bucket.max == null) return `${bucket.min}px+`;
|
|
65
|
+
return `${bucket.min}-${bucket.max}px`;
|
|
66
|
+
}
|
|
67
|
+
function isClickmapRangeKey(value) {
|
|
68
|
+
return value === "24h" || value === "7d" || value === "30d";
|
|
69
|
+
}
|
|
70
|
+
function isClickmapDeviceKey(value) {
|
|
71
|
+
return value === "all" || value === "mobile" || value === "tablet" || value === "laptop" || value === "desktop" || value === "widescreen" || value === "tv";
|
|
72
|
+
}
|
|
73
|
+
const CLICKMAP_DOM_INDEX_DEBOUNCE_MS = 250;
|
|
74
|
+
function cssEscapeAttrValue(value) {
|
|
75
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
76
|
+
}
|
|
77
|
+
function readChainAttr(segment, attr) {
|
|
78
|
+
if (!Object.prototype.hasOwnProperty.call(segment.attrs, attr)) return "";
|
|
79
|
+
const value = segment.attrs[attr];
|
|
80
|
+
return typeof value === "string" ? value : "";
|
|
81
|
+
}
|
|
82
|
+
function formatClickmapCount(value) {
|
|
83
|
+
let scaled = value;
|
|
84
|
+
let suffix = "";
|
|
85
|
+
for (const nextSuffix of [
|
|
86
|
+
"k",
|
|
87
|
+
"m",
|
|
88
|
+
"b"
|
|
89
|
+
]) {
|
|
90
|
+
if (scaled < 999.95) break;
|
|
91
|
+
scaled /= 1e3;
|
|
92
|
+
suffix = nextSuffix;
|
|
93
|
+
}
|
|
94
|
+
if (suffix === "") return String(Math.round(scaled));
|
|
95
|
+
return `${Math.round(scaled * 10) / 10}${suffix}`;
|
|
96
|
+
}
|
|
97
|
+
function getClickmapHue(count, maxCount) {
|
|
98
|
+
if (maxCount <= 1) return 185;
|
|
99
|
+
const intensity = Math.min(1, count / maxCount);
|
|
100
|
+
return 185 - Math.round(intensity * 155);
|
|
101
|
+
}
|
|
102
|
+
function getReadableElementLabel(element) {
|
|
103
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
104
|
+
if (ariaLabel != null && ariaLabel.trim() !== "") return ariaLabel.trim().slice(0, 80);
|
|
105
|
+
const title = element.getAttribute("title");
|
|
106
|
+
if (title != null && title.trim() !== "") return title.trim().slice(0, 80);
|
|
107
|
+
const text = element.textContent.trim().replace(/\s+/g, " ");
|
|
108
|
+
if (text !== "") return text.slice(0, 80);
|
|
109
|
+
return element.tagName.toLowerCase();
|
|
110
|
+
}
|
|
111
|
+
function isElementVisibleForClickmap(element) {
|
|
112
|
+
if (element.closest(`#${cssEscapeIdent(CLICKMAP_ROOT_ID)}, #${cssEscapeIdent(DEV_TOOL_ROOT_ID)}`) != null) return false;
|
|
113
|
+
if (element.closest("[hidden], [aria-hidden=\"true\"], [inert]") != null) return false;
|
|
114
|
+
const rect = element.getBoundingClientRect();
|
|
115
|
+
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
116
|
+
const style = window.getComputedStyle(element);
|
|
117
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
function getElementFromSelector(selector) {
|
|
121
|
+
try {
|
|
122
|
+
return Array.from(document.querySelectorAll(selector)).find(isElementVisibleForClickmap) ?? null;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function getSessionStorageString(key) {
|
|
128
|
+
try {
|
|
129
|
+
const value = sessionStorage.getItem(key);
|
|
130
|
+
return value == null || value.trim() === "" ? null : value;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function removeSessionStorageItem(key) {
|
|
136
|
+
try {
|
|
137
|
+
sessionStorage.removeItem(key);
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
function getJwtPayloadClaim(token, claim) {
|
|
141
|
+
const tokenParts = token.split(".");
|
|
142
|
+
if (tokenParts.length < 2 || tokenParts[1] === "") return null;
|
|
143
|
+
try {
|
|
144
|
+
const normalized = tokenParts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
145
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
146
|
+
const payload = JSON.parse(atob(padded));
|
|
147
|
+
if (typeof payload !== "object" || payload === null) return null;
|
|
148
|
+
const value = Reflect.get(payload, claim);
|
|
149
|
+
return typeof value === "string" ? value : null;
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function getClickmapTokenFromStorage() {
|
|
155
|
+
return getSessionStorageString(CLICKMAP_OVERLAY_TOKEN_STORAGE_KEY);
|
|
156
|
+
}
|
|
157
|
+
function getClickmapOriginFromStorage() {
|
|
158
|
+
const token = getClickmapTokenFromStorage();
|
|
159
|
+
return token == null ? null : getJwtPayloadClaim(token, "origin");
|
|
160
|
+
}
|
|
161
|
+
function clearClickmapTokenStorage() {
|
|
162
|
+
removeSessionStorageItem(CLICKMAP_OVERLAY_TOKEN_STORAGE_KEY);
|
|
163
|
+
}
|
|
164
|
+
function parseServerClickmapResponse(value, path) {
|
|
165
|
+
let parsed;
|
|
166
|
+
try {
|
|
167
|
+
parsed = AnalyticsClickmapResponseBodySchema.validateSync(value);
|
|
168
|
+
} catch {
|
|
169
|
+
return {
|
|
170
|
+
path,
|
|
171
|
+
totalClicks: 0,
|
|
172
|
+
selectors: [],
|
|
173
|
+
elements: []
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
path,
|
|
178
|
+
totalClicks: parsed.routes.reduce((sum, route) => sum + route.clicks, 0),
|
|
179
|
+
selectors: parsed.selectors.map((selector) => ({
|
|
180
|
+
selector: selector.selector,
|
|
181
|
+
clicks: selector.clicks
|
|
182
|
+
})),
|
|
183
|
+
elements: parsed.elements.map((element) => ({
|
|
184
|
+
elementsChain: element.elements_chain,
|
|
185
|
+
elementsText: element.elements_text,
|
|
186
|
+
tagName: element.tag_name,
|
|
187
|
+
href: element.href,
|
|
188
|
+
clicks: element.clicks,
|
|
189
|
+
deadClicks: element.dead_clicks
|
|
190
|
+
}))
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function isDynamicPathSegment(segment) {
|
|
194
|
+
if (segment === "") return false;
|
|
195
|
+
let decoded = segment;
|
|
196
|
+
try {
|
|
197
|
+
decoded = decodeURIComponent(segment);
|
|
198
|
+
} catch {}
|
|
199
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(decoded)) return true;
|
|
200
|
+
if (/^[0-9a-f]{32}$/i.test(decoded)) return true;
|
|
201
|
+
if (/^[0-9a-f]{24}$/i.test(decoded)) return true;
|
|
202
|
+
if (/^[0-9A-HJKMNP-TV-Z]{26}$/i.test(decoded)) return true;
|
|
203
|
+
if (/^\d+$/.test(decoded)) return true;
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
function wildcardizePathname(pathname) {
|
|
207
|
+
const trailingSlash = pathname.length > 1 && pathname.endsWith("/");
|
|
208
|
+
const joined = pathname.split("/").map((segment) => isDynamicPathSegment(segment) ? "*" : segment).join("/");
|
|
209
|
+
return trailingSlash ? `${joined}/` : joined;
|
|
210
|
+
}
|
|
211
|
+
function globToRegexSource(glob) {
|
|
212
|
+
return glob.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
213
|
+
}
|
|
214
|
+
function patternMatchesPath(pattern, path) {
|
|
215
|
+
if (pattern === "") return true;
|
|
216
|
+
try {
|
|
217
|
+
return new RegExp(`^${globToRegexSource(pattern)}$`).test(path);
|
|
218
|
+
} catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function createClickmapPanel(app, onClose) {
|
|
223
|
+
const container = h("div", { className: "sdt-hm" });
|
|
224
|
+
const overlayHighlight = h("div", { className: "sdt-hm-highlight" });
|
|
225
|
+
const overlayRoot = h("div", {
|
|
226
|
+
className: "sdt-hm-overlay-root",
|
|
227
|
+
"aria-hidden": "true"
|
|
228
|
+
}, overlayHighlight);
|
|
229
|
+
const statsCount = h("div", { className: "sdt-hm-stat-value" }, "0");
|
|
230
|
+
const selectorCount = h("div", { className: "sdt-hm-stat-value" }, "0");
|
|
231
|
+
const viewportValue = h("div", { className: "sdt-hm-stat-value" }, `${window.innerWidth}x${window.innerHeight}`);
|
|
232
|
+
const list = h("div", { className: "sdt-hm-list" });
|
|
233
|
+
const empty = h("div", { className: "sdt-hm-empty" }, "Paste a clickmap token from the dashboard to load aggregated element clicks for this page.");
|
|
234
|
+
const status = h("div", { className: "sdt-hm-token-status" });
|
|
235
|
+
const viewportWarningTitle = h("div", { className: "sdt-hm-viewport-warning-title" });
|
|
236
|
+
const viewportWarningBody = h("div", { className: "sdt-hm-viewport-warning-body" });
|
|
237
|
+
const viewportWarningWidthValue = h("code", { className: "sdt-hm-viewport-warning-code" });
|
|
238
|
+
const viewportWarningHeightValue = h("code", { className: "sdt-hm-viewport-warning-code" });
|
|
239
|
+
const viewportWarningWidthCopy = h("button", {
|
|
240
|
+
className: "sdt-hm-copy-btn",
|
|
241
|
+
type: "button"
|
|
242
|
+
});
|
|
243
|
+
const viewportWarningHeightCopy = h("button", {
|
|
244
|
+
className: "sdt-hm-copy-btn",
|
|
245
|
+
type: "button"
|
|
246
|
+
});
|
|
247
|
+
const viewportWarning = h("div", {
|
|
248
|
+
className: "sdt-hm-viewport-warning",
|
|
249
|
+
role: "status"
|
|
250
|
+
}, viewportWarningTitle, viewportWarningBody, h("div", { className: "sdt-hm-viewport-warning-actions" }, h("span", { className: "sdt-hm-viewport-warning-action" }, h("span", { className: "sdt-hm-viewport-warning-label" }, "Width"), viewportWarningWidthValue, viewportWarningWidthCopy), h("span", { className: "sdt-hm-viewport-warning-action" }, h("span", { className: "sdt-hm-viewport-warning-label" }, "Height"), viewportWarningHeightValue, viewportWarningHeightCopy)));
|
|
251
|
+
const overlayToggle = h("button", { className: "sdt-hm-btn sdt-hm-btn-primary" }, "Hide");
|
|
252
|
+
const expandButton = h("button", {
|
|
253
|
+
className: "sdt-hm-icon-btn",
|
|
254
|
+
"aria-label": "Expand clickmap options",
|
|
255
|
+
"data-sdt-tip": "Expand clickmap options"
|
|
256
|
+
});
|
|
257
|
+
const closeButton = h("button", {
|
|
258
|
+
className: "sdt-hm-icon-btn",
|
|
259
|
+
"aria-label": "Close clickmap",
|
|
260
|
+
"data-sdt-tip": "Close clickmap"
|
|
261
|
+
});
|
|
262
|
+
const miniClicks = h("span", { className: "sdt-hm-toolbar-metric-value" }, "0");
|
|
263
|
+
const miniElements = h("span", { className: "sdt-hm-toolbar-metric-value" }, "0");
|
|
264
|
+
function readStoredFilters() {
|
|
265
|
+
try {
|
|
266
|
+
const raw = sessionStorage.getItem(CLICKMAP_FILTERS_STORAGE_KEY);
|
|
267
|
+
if (raw == null) return { ...CLICKMAP_DEFAULT_FILTERS };
|
|
268
|
+
const parsed = JSON.parse(raw);
|
|
269
|
+
if (parsed == null || typeof parsed !== "object") return { ...CLICKMAP_DEFAULT_FILTERS };
|
|
270
|
+
const obj = parsed;
|
|
271
|
+
return {
|
|
272
|
+
range: isClickmapRangeKey(obj.range) ? obj.range : CLICKMAP_DEFAULT_FILTERS.range,
|
|
273
|
+
device: isClickmapDeviceKey(obj.device) ? obj.device : CLICKMAP_DEFAULT_FILTERS.device,
|
|
274
|
+
urlPattern: typeof obj.urlPattern === "string" ? obj.urlPattern : CLICKMAP_DEFAULT_FILTERS.urlPattern,
|
|
275
|
+
elementSearch: typeof obj.elementSearch === "string" ? obj.elementSearch : CLICKMAP_DEFAULT_FILTERS.elementSearch,
|
|
276
|
+
showDead: typeof obj.showDead === "boolean" ? obj.showDead : CLICKMAP_DEFAULT_FILTERS.showDead
|
|
277
|
+
};
|
|
278
|
+
} catch {
|
|
279
|
+
return { ...CLICKMAP_DEFAULT_FILTERS };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function persistFilters(next) {
|
|
283
|
+
try {
|
|
284
|
+
sessionStorage.setItem(CLICKMAP_FILTERS_STORAGE_KEY, JSON.stringify(next));
|
|
285
|
+
} catch {}
|
|
286
|
+
}
|
|
287
|
+
let currentPath = window.location.pathname;
|
|
288
|
+
let serverClickmap = {
|
|
289
|
+
path: currentPath,
|
|
290
|
+
totalClicks: 0,
|
|
291
|
+
selectors: [],
|
|
292
|
+
elements: []
|
|
293
|
+
};
|
|
294
|
+
let loadingServerClickmap = false;
|
|
295
|
+
let serverClickmapError = null;
|
|
296
|
+
let serverClickmapRequestId = 0;
|
|
297
|
+
let overlayVisible = true;
|
|
298
|
+
let expanded = false;
|
|
299
|
+
let renderFrame = 0;
|
|
300
|
+
let overlayMode = "hidden";
|
|
301
|
+
let highlightedGroupSelector = null;
|
|
302
|
+
let highlightRenderedSelector = null;
|
|
303
|
+
let highlightSettleTimer = null;
|
|
304
|
+
let hoveredGroupSelector = null;
|
|
305
|
+
const mutedGroupSelectors = /* @__PURE__ */ new Set();
|
|
306
|
+
const selectedGroupSelectors = /* @__PURE__ */ new Set();
|
|
307
|
+
let selectionAnchorSelector = null;
|
|
308
|
+
let latestGroups = [];
|
|
309
|
+
const groupOverlayElements = /* @__PURE__ */ new Map();
|
|
310
|
+
const listRowElements = /* @__PURE__ */ new Map();
|
|
311
|
+
function resetCopyButton(button, label) {
|
|
312
|
+
button.textContent = label;
|
|
313
|
+
}
|
|
314
|
+
function copyClickmapViewportValue(button, value, label) {
|
|
315
|
+
runAsynchronously(async () => {
|
|
316
|
+
try {
|
|
317
|
+
await navigator.clipboard.writeText(value);
|
|
318
|
+
button.textContent = "Copied";
|
|
319
|
+
window.setTimeout(() => resetCopyButton(button, label), 1200);
|
|
320
|
+
} catch {
|
|
321
|
+
button.textContent = "Copy failed";
|
|
322
|
+
window.setTimeout(() => resetCopyButton(button, label), 1600);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const domIndex = /* @__PURE__ */ new Map();
|
|
327
|
+
let domIndexDirty = true;
|
|
328
|
+
let domIndexDebounce = 0;
|
|
329
|
+
function rebuildDomIndex() {
|
|
330
|
+
domIndex.clear();
|
|
331
|
+
trimTargetCache = /* @__PURE__ */ new WeakMap();
|
|
332
|
+
const all = document.querySelectorAll("*");
|
|
333
|
+
for (const el of all) {
|
|
334
|
+
if (!isElementVisibleForClickmap(el)) continue;
|
|
335
|
+
const tag = el.tagName.toLowerCase();
|
|
336
|
+
const bucket = domIndex.get(tag) ?? [];
|
|
337
|
+
bucket.push(el);
|
|
338
|
+
domIndex.set(tag, bucket);
|
|
339
|
+
}
|
|
340
|
+
domIndexDirty = false;
|
|
341
|
+
}
|
|
342
|
+
const CLICKMAP_TRIM_TARGET_SELECTOR = "a[href], button, input, select, textarea, summary, label, [role=\"button\"], [role=\"link\"], [role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"], [role=\"tab\"], [role=\"checkbox\"], [role=\"radio\"], [role=\"switch\"], [role=\"option\"], [contenteditable=\"true\"]";
|
|
343
|
+
const CLICKMAP_TRIM_MAX_HOPS = 10;
|
|
344
|
+
let trimTargetCache = /* @__PURE__ */ new WeakMap();
|
|
345
|
+
function resolveClickTarget(start) {
|
|
346
|
+
const cached = trimTargetCache.get(start);
|
|
347
|
+
if (cached != null) return cached;
|
|
348
|
+
let target = start;
|
|
349
|
+
let current = start;
|
|
350
|
+
for (let hops = 0; current != null && current !== document.body && current !== document.documentElement && hops < CLICKMAP_TRIM_MAX_HOPS; hops++) {
|
|
351
|
+
if (current.matches(CLICKMAP_TRIM_TARGET_SELECTOR)) {
|
|
352
|
+
target = current;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
const parent = current.parentElement;
|
|
356
|
+
if (window.getComputedStyle(current).cursor === "pointer" && (parent == null || window.getComputedStyle(parent).cursor !== "pointer")) {
|
|
357
|
+
target = current;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
current = parent;
|
|
361
|
+
}
|
|
362
|
+
const resolved = {
|
|
363
|
+
target,
|
|
364
|
+
key: buildElementsChain(target)
|
|
365
|
+
};
|
|
366
|
+
trimTargetCache.set(start, resolved);
|
|
367
|
+
return resolved;
|
|
368
|
+
}
|
|
369
|
+
function ensureDomIndex() {
|
|
370
|
+
if (domIndexDirty) rebuildDomIndex();
|
|
371
|
+
}
|
|
372
|
+
function invalidateDomIndex() {
|
|
373
|
+
domIndexDirty = true;
|
|
374
|
+
}
|
|
375
|
+
function scheduleDomIndexInvalidation() {
|
|
376
|
+
if (domIndexDebounce !== 0) window.clearTimeout(domIndexDebounce);
|
|
377
|
+
domIndexDebounce = window.setTimeout(() => {
|
|
378
|
+
domIndexDebounce = 0;
|
|
379
|
+
invalidateDomIndex();
|
|
380
|
+
scheduleRender();
|
|
381
|
+
}, CLICKMAP_DOM_INDEX_DEBOUNCE_MS);
|
|
382
|
+
}
|
|
383
|
+
function isElementChainCandidateUnique(matches) {
|
|
384
|
+
const visible = matches.filter(isElementVisibleForClickmap);
|
|
385
|
+
return visible.length === 1 ? visible[0] : null;
|
|
386
|
+
}
|
|
387
|
+
function queryUniqueBySelector(selector) {
|
|
388
|
+
try {
|
|
389
|
+
return isElementChainCandidateUnique(Array.from(document.querySelectorAll(selector)));
|
|
390
|
+
} catch {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function elementMatchesSegment(element, segment, useClasses) {
|
|
395
|
+
if (element.tagName.toLowerCase() !== segment.tag) return false;
|
|
396
|
+
if (useClasses) {
|
|
397
|
+
for (const cls of segment.classes) if (!element.classList.contains(cls)) return false;
|
|
398
|
+
}
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
function ancestorMatchesChain(leaf, chain, useClasses, useNthOfType, useNthChild) {
|
|
402
|
+
let cursor = leaf;
|
|
403
|
+
for (let i = 0; i < chain.length; i++) {
|
|
404
|
+
if (cursor == null) return false;
|
|
405
|
+
const segment = chain[i];
|
|
406
|
+
if (!elementMatchesSegment(cursor, segment, useClasses)) return false;
|
|
407
|
+
if (useNthOfType && segment.nthOfType != null) {
|
|
408
|
+
if (computeNthOfType(cursor) !== segment.nthOfType) return false;
|
|
409
|
+
}
|
|
410
|
+
if (useNthChild && segment.nthChild != null) {
|
|
411
|
+
if (computeNthChild(cursor) !== segment.nthChild) return false;
|
|
412
|
+
}
|
|
413
|
+
cursor = cursor.parentElement;
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
function computeNthOfType(el) {
|
|
418
|
+
let n = 1;
|
|
419
|
+
let sib = el.previousElementSibling;
|
|
420
|
+
const tag = el.tagName;
|
|
421
|
+
while (sib != null) {
|
|
422
|
+
if (sib.tagName === tag) n += 1;
|
|
423
|
+
sib = sib.previousElementSibling;
|
|
424
|
+
}
|
|
425
|
+
return n;
|
|
426
|
+
}
|
|
427
|
+
function computeNthChild(el) {
|
|
428
|
+
let n = 1;
|
|
429
|
+
let sib = el.previousElementSibling;
|
|
430
|
+
while (sib != null) {
|
|
431
|
+
n += 1;
|
|
432
|
+
sib = sib.previousElementSibling;
|
|
433
|
+
}
|
|
434
|
+
return n;
|
|
435
|
+
}
|
|
436
|
+
function inferElementFromChain(chain) {
|
|
437
|
+
if (chain.length === 0) return null;
|
|
438
|
+
const leaf = chain[0];
|
|
439
|
+
for (const { attr } of [
|
|
440
|
+
{ attr: "data-hexclave-id" },
|
|
441
|
+
{ attr: "data-testid" },
|
|
442
|
+
{ attr: "data-test-id" },
|
|
443
|
+
{ attr: "name" }
|
|
444
|
+
]) {
|
|
445
|
+
const value = readChainAttr(leaf, attr);
|
|
446
|
+
if (value === "") continue;
|
|
447
|
+
const match = queryUniqueBySelector(`[${attr}="${cssEscapeAttrValue(value)}"]`);
|
|
448
|
+
if (match) return match;
|
|
449
|
+
}
|
|
450
|
+
const id = readChainAttr(leaf, "id");
|
|
451
|
+
if (id !== "") {
|
|
452
|
+
const match = queryUniqueBySelector(`#${cssEscapeIdent(id)}`);
|
|
453
|
+
if (match) return match;
|
|
454
|
+
}
|
|
455
|
+
if (leaf.href != null && leaf.href !== "" && leaf.tag === "a") {
|
|
456
|
+
const match = queryUniqueBySelector(`a[href="${cssEscapeAttrValue(leaf.href)}"]`);
|
|
457
|
+
if (match) return match;
|
|
458
|
+
}
|
|
459
|
+
for (const attr of [
|
|
460
|
+
"aria-label",
|
|
461
|
+
"role",
|
|
462
|
+
"placeholder",
|
|
463
|
+
"title",
|
|
464
|
+
"type"
|
|
465
|
+
]) {
|
|
466
|
+
const value = readChainAttr(leaf, attr);
|
|
467
|
+
if (value === "") continue;
|
|
468
|
+
const match = queryUniqueBySelector(`${leaf.tag}[${attr}="${cssEscapeAttrValue(value)}"]`);
|
|
469
|
+
if (match) return match;
|
|
470
|
+
}
|
|
471
|
+
ensureDomIndex();
|
|
472
|
+
const candidates = domIndex.get(leaf.tag) ?? [];
|
|
473
|
+
if (candidates.length === 0) return null;
|
|
474
|
+
const v3 = [];
|
|
475
|
+
for (const candidate of candidates) if (ancestorMatchesChain(candidate, chain, true, false, false)) v3.push(candidate);
|
|
476
|
+
const u3 = isElementChainCandidateUnique(v3);
|
|
477
|
+
if (u3 != null) return u3;
|
|
478
|
+
const v4 = [];
|
|
479
|
+
for (const candidate of candidates) if (ancestorMatchesChain(candidate, chain, true, true, false)) v4.push(candidate);
|
|
480
|
+
const u4 = isElementChainCandidateUnique(v4);
|
|
481
|
+
if (u4 != null) return u4;
|
|
482
|
+
const v5 = [];
|
|
483
|
+
for (const candidate of candidates) if (ancestorMatchesChain(candidate, chain, true, true, true)) v5.push(candidate);
|
|
484
|
+
const u5 = isElementChainCandidateUnique(v5);
|
|
485
|
+
if (u5 != null) return u5;
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
setHtml(closeButton, "<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 6 6 18\"/><path d=\"m6 6 12 12\"/></svg>");
|
|
489
|
+
const chevronUpSvg = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m18 15-6-6-6 6\"/></svg>";
|
|
490
|
+
const chevronDownSvg = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 9 6 6 6-6\"/></svg>";
|
|
491
|
+
const clicksIconSvg = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 4.1 12 6\"/><path d=\"m5.1 8-2.9-.8\"/><path d=\"m6 12-1.9 2\"/><path d=\"M7.2 2.2 8 5.1\"/><path d=\"M9.037 9.69a.498.498 0 0 1 .653-.653l11 4.5a.5.5 0 0 1-.074.949l-4.349 1.041a1 1 0 0 0-.74.739l-1.04 4.35a.5.5 0 0 1-.95.074z\"/></svg>";
|
|
492
|
+
const elementsIconSvg = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/><path d=\"M3 9h18\"/><path d=\"M9 21V9\"/></svg>";
|
|
493
|
+
const eyeIconSvg = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>";
|
|
494
|
+
const eyeOffIconSvg = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49\"/><path d=\"M14.084 14.158a3 3 0 0 1-4.242-4.242\"/><path d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143\"/><path d=\"m2 2 20 20\"/></svg>";
|
|
495
|
+
let renderedExpandIcon = "";
|
|
496
|
+
function syncExpandIcon() {
|
|
497
|
+
const icon = expanded ? chevronDownSvg : chevronUpSvg;
|
|
498
|
+
if (renderedExpandIcon === icon) return;
|
|
499
|
+
renderedExpandIcon = icon;
|
|
500
|
+
setHtml(expandButton, icon);
|
|
501
|
+
}
|
|
502
|
+
syncExpandIcon();
|
|
503
|
+
resetCopyButton(viewportWarningWidthCopy, "Copy width");
|
|
504
|
+
resetCopyButton(viewportWarningHeightCopy, "Copy height");
|
|
505
|
+
viewportWarningWidthCopy.addEventListener("click", () => {
|
|
506
|
+
copyClickmapViewportValue(viewportWarningWidthCopy, viewportWarningWidthValue.textContent, "Copy width");
|
|
507
|
+
});
|
|
508
|
+
viewportWarningHeightCopy.addEventListener("click", () => {
|
|
509
|
+
copyClickmapViewportValue(viewportWarningHeightCopy, viewportWarningHeightValue.textContent, "Copy height");
|
|
510
|
+
});
|
|
511
|
+
const stats = h("div", { className: "sdt-hm-stats" }, h("div", { className: "sdt-hm-stat" }, h("div", { className: "sdt-hm-stat-label" }, "Clicks"), statsCount), h("div", { className: "sdt-hm-stat" }, h("div", { className: "sdt-hm-stat-label" }, "Elements"), selectorCount), h("div", { className: "sdt-hm-stat" }, h("div", { className: "sdt-hm-stat-label" }, "Viewport"), viewportValue));
|
|
512
|
+
let filters = readStoredFilters();
|
|
513
|
+
let filterReloadDebounce = 0;
|
|
514
|
+
let urlPatternUserEdited = filters.urlPattern.trim() !== "";
|
|
515
|
+
function getEffectiveUrlPattern() {
|
|
516
|
+
if (urlPatternUserEdited) return filters.urlPattern.trim();
|
|
517
|
+
return wildcardizePathname(window.location.pathname);
|
|
518
|
+
}
|
|
519
|
+
function syncAutoUrlPattern() {
|
|
520
|
+
if (urlPatternUserEdited) return;
|
|
521
|
+
const auto = wildcardizePathname(window.location.pathname);
|
|
522
|
+
if (urlPatternInput.value !== auto) urlPatternInput.value = auto;
|
|
523
|
+
}
|
|
524
|
+
function makeFilterSelect(options, value) {
|
|
525
|
+
const el = h("select", { className: "sdt-hm-filter-input" });
|
|
526
|
+
for (const [optValue, label] of options) {
|
|
527
|
+
const opt = h("option", { value: optValue }, label);
|
|
528
|
+
el.appendChild(opt);
|
|
529
|
+
}
|
|
530
|
+
el.value = value;
|
|
531
|
+
return el;
|
|
532
|
+
}
|
|
533
|
+
const rangeSelect = makeFilterSelect([
|
|
534
|
+
["24h", "Last 24h"],
|
|
535
|
+
["7d", "Last 7 days"],
|
|
536
|
+
["30d", "Last 30 days"]
|
|
537
|
+
], filters.range);
|
|
538
|
+
const deviceOptions = [
|
|
539
|
+
["all", "All"],
|
|
540
|
+
["mobile", "Mobile"],
|
|
541
|
+
["tablet", "Tablet"],
|
|
542
|
+
["laptop", "Laptop"],
|
|
543
|
+
["desktop", "Desktop"],
|
|
544
|
+
["widescreen", "Wide"],
|
|
545
|
+
["tv", "TV"]
|
|
546
|
+
];
|
|
547
|
+
const deviceThumb = h("span", {
|
|
548
|
+
className: "sdt-hm-seg-thumb",
|
|
549
|
+
"aria-hidden": "true"
|
|
550
|
+
});
|
|
551
|
+
const deviceSwitcher = h("div", {
|
|
552
|
+
className: "sdt-hm-seg",
|
|
553
|
+
role: "radiogroup",
|
|
554
|
+
"aria-label": "Viewport"
|
|
555
|
+
}, deviceThumb);
|
|
556
|
+
const deviceButtons = /* @__PURE__ */ new Map();
|
|
557
|
+
let deviceThumbPlaced = false;
|
|
558
|
+
function positionDeviceThumb() {
|
|
559
|
+
const active = deviceButtons.get(filters.device);
|
|
560
|
+
if (active == null || active.offsetWidth === 0) return;
|
|
561
|
+
if (!deviceThumbPlaced) deviceThumb.style.transition = "none";
|
|
562
|
+
deviceThumb.style.transform = `translateX(${active.offsetLeft}px)`;
|
|
563
|
+
deviceThumb.style.width = `${active.offsetWidth}px`;
|
|
564
|
+
if (!deviceThumbPlaced) {
|
|
565
|
+
deviceThumb.offsetWidth;
|
|
566
|
+
deviceThumb.style.transition = "";
|
|
567
|
+
deviceThumbPlaced = true;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const [key, label] of deviceOptions) {
|
|
571
|
+
const btn = h("button", {
|
|
572
|
+
className: "sdt-hm-seg-btn",
|
|
573
|
+
type: "button",
|
|
574
|
+
role: "radio"
|
|
575
|
+
}, label);
|
|
576
|
+
btn.setAttribute("aria-checked", String(key === filters.device));
|
|
577
|
+
btn.addEventListener("click", () => {
|
|
578
|
+
if (filters.device === key) return;
|
|
579
|
+
updateFilters({ device: key });
|
|
580
|
+
for (const [k, b] of deviceButtons) b.setAttribute("aria-checked", String(k === key));
|
|
581
|
+
positionDeviceThumb();
|
|
582
|
+
});
|
|
583
|
+
deviceButtons.set(key, btn);
|
|
584
|
+
deviceSwitcher.appendChild(btn);
|
|
585
|
+
}
|
|
586
|
+
const urlPatternInput = h("input", {
|
|
587
|
+
className: "sdt-hm-filter-input",
|
|
588
|
+
type: "text",
|
|
589
|
+
placeholder: "/products/*",
|
|
590
|
+
spellcheck: "false",
|
|
591
|
+
autocomplete: "off",
|
|
592
|
+
autocapitalize: "off"
|
|
593
|
+
});
|
|
594
|
+
urlPatternInput.value = getEffectiveUrlPattern();
|
|
595
|
+
const urlPatternReset = h("button", {
|
|
596
|
+
className: "sdt-hm-filter-reset",
|
|
597
|
+
type: "button",
|
|
598
|
+
"aria-label": "Revert the URL pattern to the current page",
|
|
599
|
+
"data-sdt-tip": "Revert to the current page"
|
|
600
|
+
});
|
|
601
|
+
setHtml(urlPatternReset, "<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 14 4 9l5-5\"/><path d=\"M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11\"/></svg>");
|
|
602
|
+
function syncUrlPatternResetVisibility() {
|
|
603
|
+
const auto = wildcardizePathname(window.location.pathname);
|
|
604
|
+
const showReset = urlPatternUserEdited && filters.urlPattern.trim() !== auto;
|
|
605
|
+
urlPatternReset.classList.toggle("sdt-hm-filter-reset-visible", showReset);
|
|
606
|
+
}
|
|
607
|
+
const urlPatternInfo = h("button", {
|
|
608
|
+
className: "sdt-hm-filter-info",
|
|
609
|
+
type: "button",
|
|
610
|
+
"aria-label": "URL pattern help",
|
|
611
|
+
"aria-expanded": "false",
|
|
612
|
+
"data-sdt-tip": "How URL patterns work"
|
|
613
|
+
});
|
|
614
|
+
setHtml(urlPatternInfo, "<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></svg>");
|
|
615
|
+
function makeUrlHelpRow(pattern, description) {
|
|
616
|
+
return h("div", { className: "sdt-hm-url-help-row" }, h("code", { className: "sdt-hm-url-help-code" }, pattern), h("span", { className: "sdt-hm-url-help-desc" }, description));
|
|
617
|
+
}
|
|
618
|
+
function makeCode(text) {
|
|
619
|
+
return h("code", { className: "sdt-hm-url-help-code" }, text);
|
|
620
|
+
}
|
|
621
|
+
const urlHelpTitle = h("div", { className: "sdt-hm-url-help-title" });
|
|
622
|
+
const urlHelpBody = h("div", { className: "sdt-hm-url-help-body" });
|
|
623
|
+
const urlHelpRows = h("div", { className: "sdt-hm-url-help-rows" });
|
|
624
|
+
function renderUrlHelp() {
|
|
625
|
+
urlHelpTitle.textContent = "URL pattern · glob";
|
|
626
|
+
urlHelpBody.replaceChildren("Limits the clickmap to pages whose path matches. Matched against the pathname only — no domain, hash, or query string. ", makeCode("*"), " is the only wildcard and stands in for any characters (including ", makeCode("/"), "). Everything else is matched literally.");
|
|
627
|
+
urlHelpRows.replaceChildren(makeUrlHelpRow("/pricing", "That exact page"), makeUrlHelpRow("/products/*", "Any path under /products/"), makeUrlHelpRow("/teams/*/members", "A wildcard segment in the middle"), makeUrlHelpRow("*/settings", "Any path ending in /settings"), makeUrlHelpRow("*", "Every page"), makeUrlHelpRow("(empty)", "Auto-tracks the page you are viewing"));
|
|
628
|
+
}
|
|
629
|
+
const urlPatternHelp = h("div", {
|
|
630
|
+
className: "sdt-hm-url-help",
|
|
631
|
+
role: "dialog",
|
|
632
|
+
"aria-label": "URL pattern help"
|
|
633
|
+
}, urlHelpTitle, urlHelpBody, urlHelpRows);
|
|
634
|
+
let urlHelpOpen = false;
|
|
635
|
+
function setUrlHelpOpen(open) {
|
|
636
|
+
urlHelpOpen = open;
|
|
637
|
+
urlPatternHelp.classList.toggle("sdt-hm-url-help-open", open);
|
|
638
|
+
urlPatternInfo.setAttribute("aria-expanded", String(open));
|
|
639
|
+
}
|
|
640
|
+
urlPatternInfo.addEventListener("click", (event) => {
|
|
641
|
+
event.stopPropagation();
|
|
642
|
+
setUrlHelpOpen(!urlHelpOpen);
|
|
643
|
+
});
|
|
644
|
+
urlPatternHelp.addEventListener("click", (event) => {
|
|
645
|
+
event.stopPropagation();
|
|
646
|
+
});
|
|
647
|
+
renderUrlHelp();
|
|
648
|
+
const elementSearchInput = h("input", {
|
|
649
|
+
className: "sdt-hm-filter-input",
|
|
650
|
+
type: "text",
|
|
651
|
+
placeholder: "Search element text or tag",
|
|
652
|
+
"aria-label": "Search element text or tag",
|
|
653
|
+
spellcheck: "false",
|
|
654
|
+
autocomplete: "off",
|
|
655
|
+
autocapitalize: "off"
|
|
656
|
+
});
|
|
657
|
+
elementSearchInput.value = filters.elementSearch;
|
|
658
|
+
function wrapFilterField(label, input, action) {
|
|
659
|
+
const labelRow = h("span", { className: "sdt-hm-filter-label-row" }, h("span", { className: "sdt-hm-filter-label" }, label));
|
|
660
|
+
if (action != null) labelRow.appendChild(action);
|
|
661
|
+
return h("label", { className: "sdt-hm-filter-field" }, labelRow, input);
|
|
662
|
+
}
|
|
663
|
+
const clicksIcon = h("span", { className: "sdt-hm-toolbar-metric-icon" });
|
|
664
|
+
const elementsIcon = h("span", { className: "sdt-hm-toolbar-metric-icon" });
|
|
665
|
+
setHtml(clicksIcon, clicksIconSvg);
|
|
666
|
+
setHtml(elementsIcon, elementsIconSvg);
|
|
667
|
+
const showDeadIconSvg = "<svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 10h.01\"/><path d=\"M15 10h.01\"/><path d=\"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z\"/></svg>";
|
|
668
|
+
const showDeadMiniToggle = h("button", {
|
|
669
|
+
className: "sdt-hm-icon-btn sdt-hm-dead-toggle",
|
|
670
|
+
type: "button",
|
|
671
|
+
"aria-pressed": "false",
|
|
672
|
+
"aria-label": "Show dead clicks",
|
|
673
|
+
"data-sdt-tip": "Show dead clicks"
|
|
674
|
+
});
|
|
675
|
+
setHtml(showDeadMiniToggle, showDeadIconSvg);
|
|
676
|
+
const showDeadToggleIcon = h("span", { className: "sdt-hm-dead-toggle-icon" });
|
|
677
|
+
setHtml(showDeadToggleIcon, showDeadIconSvg);
|
|
678
|
+
const showDeadToggle = h("button", {
|
|
679
|
+
className: "sdt-hm-btn sdt-hm-dead-toggle",
|
|
680
|
+
type: "button",
|
|
681
|
+
"aria-pressed": "false",
|
|
682
|
+
"data-sdt-tip": "Include clicks that had no effect"
|
|
683
|
+
}, showDeadToggleIcon, "Dead clicks");
|
|
684
|
+
function syncShowDeadToggles() {
|
|
685
|
+
for (const button of [showDeadMiniToggle, showDeadToggle]) {
|
|
686
|
+
button.setAttribute("aria-pressed", String(filters.showDead));
|
|
687
|
+
button.classList.toggle("sdt-hm-dead-toggle-active", filters.showDead);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function setShowDead(next) {
|
|
691
|
+
if (filters.showDead === next) return;
|
|
692
|
+
filters = {
|
|
693
|
+
...filters,
|
|
694
|
+
showDead: next
|
|
695
|
+
};
|
|
696
|
+
persistFilters(filters);
|
|
697
|
+
syncShowDeadToggles();
|
|
698
|
+
scheduleRender();
|
|
699
|
+
}
|
|
700
|
+
showDeadMiniToggle.addEventListener("click", () => setShowDead(!filters.showDead));
|
|
701
|
+
showDeadToggle.addEventListener("click", () => setShowDead(!filters.showDead));
|
|
702
|
+
syncShowDeadToggles();
|
|
703
|
+
const overlayMiniToggle = h("button", {
|
|
704
|
+
className: "sdt-hm-icon-btn",
|
|
705
|
+
type: "button",
|
|
706
|
+
"aria-pressed": "false",
|
|
707
|
+
"aria-label": "Hide overlay",
|
|
708
|
+
"data-sdt-tip": "Hide overlay"
|
|
709
|
+
});
|
|
710
|
+
let renderedOverlayMiniIcon = "";
|
|
711
|
+
function syncOverlayMiniToggle() {
|
|
712
|
+
overlayMiniToggle.setAttribute("aria-pressed", String(!overlayVisible));
|
|
713
|
+
const label = overlayVisible ? "Hide overlay" : "Show overlay";
|
|
714
|
+
overlayMiniToggle.setAttribute("aria-label", label);
|
|
715
|
+
overlayMiniToggle.setAttribute("data-sdt-tip", label);
|
|
716
|
+
overlayMiniToggle.classList.toggle("sdt-hm-overlay-mini-off", !overlayVisible);
|
|
717
|
+
const icon = overlayVisible ? eyeIconSvg : eyeOffIconSvg;
|
|
718
|
+
if (renderedOverlayMiniIcon !== icon) {
|
|
719
|
+
renderedOverlayMiniIcon = icon;
|
|
720
|
+
setHtml(overlayMiniToggle, icon);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
overlayMiniToggle.addEventListener("click", () => {
|
|
724
|
+
overlayVisible = !overlayVisible;
|
|
725
|
+
render();
|
|
726
|
+
});
|
|
727
|
+
syncOverlayMiniToggle();
|
|
728
|
+
const toolbar = h("div", { className: "sdt-hm-toolbar" }, closeButton, h("div", { className: "sdt-hm-toolbar-title" }, "Clickmap"), h("div", { className: "sdt-hm-toolbar-filters" }, rangeSelect, h("div", { className: "sdt-hm-toolbar-url" }, urlPatternInput, urlPatternReset, urlPatternInfo, urlPatternHelp)), h("div", { className: "sdt-hm-toolbar-metrics" }, h("span", {
|
|
729
|
+
className: "sdt-hm-toolbar-metric",
|
|
730
|
+
"data-sdt-tip": "Aggregate clicks"
|
|
731
|
+
}, miniClicks, clicksIcon), h("span", {
|
|
732
|
+
className: "sdt-hm-toolbar-metric",
|
|
733
|
+
"data-sdt-tip": "Mapped elements"
|
|
734
|
+
}, miniElements, elementsIcon)), showDeadMiniToggle, overlayMiniToggle, expandButton);
|
|
735
|
+
const filterRow = h("div", { className: "sdt-hm-filters" }, wrapFilterField("Viewport", deviceSwitcher), viewportWarning);
|
|
736
|
+
function scheduleFilterReload() {
|
|
737
|
+
if (filterReloadDebounce !== 0) window.clearTimeout(filterReloadDebounce);
|
|
738
|
+
filterReloadDebounce = window.setTimeout(() => {
|
|
739
|
+
filterReloadDebounce = 0;
|
|
740
|
+
runAsynchronously(loadServerClickmap());
|
|
741
|
+
}, 250);
|
|
742
|
+
}
|
|
743
|
+
function updateFilters(next) {
|
|
744
|
+
filters = {
|
|
745
|
+
...filters,
|
|
746
|
+
...next
|
|
747
|
+
};
|
|
748
|
+
persistFilters(filters);
|
|
749
|
+
scheduleFilterReload();
|
|
750
|
+
}
|
|
751
|
+
let elementSearchDebounce = 0;
|
|
752
|
+
function updateElementSearch(value) {
|
|
753
|
+
filters = {
|
|
754
|
+
...filters,
|
|
755
|
+
elementSearch: value
|
|
756
|
+
};
|
|
757
|
+
persistFilters(filters);
|
|
758
|
+
if (elementSearchDebounce !== 0) window.clearTimeout(elementSearchDebounce);
|
|
759
|
+
elementSearchDebounce = window.setTimeout(() => {
|
|
760
|
+
elementSearchDebounce = 0;
|
|
761
|
+
scheduleRender();
|
|
762
|
+
}, 120);
|
|
763
|
+
}
|
|
764
|
+
rangeSelect.addEventListener("change", () => {
|
|
765
|
+
if (isClickmapRangeKey(rangeSelect.value)) updateFilters({ range: rangeSelect.value });
|
|
766
|
+
});
|
|
767
|
+
urlPatternInput.addEventListener("input", () => {
|
|
768
|
+
const value = urlPatternInput.value;
|
|
769
|
+
urlPatternUserEdited = value.trim() !== "";
|
|
770
|
+
updateFilters({ urlPattern: value });
|
|
771
|
+
syncUrlPatternResetVisibility();
|
|
772
|
+
});
|
|
773
|
+
urlPatternReset.addEventListener("click", () => {
|
|
774
|
+
urlPatternUserEdited = false;
|
|
775
|
+
urlPatternInput.value = wildcardizePathname(window.location.pathname);
|
|
776
|
+
updateFilters({ urlPattern: "" });
|
|
777
|
+
syncUrlPatternResetVisibility();
|
|
778
|
+
});
|
|
779
|
+
elementSearchInput.addEventListener("input", () => {
|
|
780
|
+
updateElementSearch(elementSearchInput.value);
|
|
781
|
+
});
|
|
782
|
+
const head = h("div", { className: "sdt-hm-head" }, filterRow, h("div", { className: "sdt-hm-actions" }, stats, showDeadToggle, overlayToggle));
|
|
783
|
+
const listHeaderCheck = h("button", {
|
|
784
|
+
className: "sdt-hm-row-check",
|
|
785
|
+
type: "button",
|
|
786
|
+
role: "checkbox",
|
|
787
|
+
"aria-checked": "false",
|
|
788
|
+
"aria-label": "Select all elements"
|
|
789
|
+
});
|
|
790
|
+
const listHeaderSummary = h("span", { className: "sdt-hm-list-header-summary" });
|
|
791
|
+
const listShowButton = h("button", {
|
|
792
|
+
className: "sdt-hm-btn sdt-hm-btn-sm",
|
|
793
|
+
type: "button"
|
|
794
|
+
}, "Show all");
|
|
795
|
+
const listHideButton = h("button", {
|
|
796
|
+
className: "sdt-hm-btn sdt-hm-btn-sm",
|
|
797
|
+
type: "button"
|
|
798
|
+
}, "Hide all");
|
|
799
|
+
const listHeader = h("div", { className: "sdt-hm-list-header" }, listHeaderCheck, listHeaderSummary, elementSearchInput, listShowButton, listHideButton);
|
|
800
|
+
listHeaderCheck.addEventListener("click", () => {
|
|
801
|
+
if (latestGroups.length > 0 && latestGroups.every((group) => selectedGroupSelectors.has(group.selector))) {
|
|
802
|
+
clearSelection();
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
selectedGroupSelectors.clear();
|
|
806
|
+
for (const group of latestGroups) selectedGroupSelectors.add(group.selector);
|
|
807
|
+
selectionAnchorSelector = null;
|
|
808
|
+
scheduleRender();
|
|
809
|
+
});
|
|
810
|
+
listShowButton.addEventListener("click", () => {
|
|
811
|
+
for (const group of getBulkActionGroups()) mutedGroupSelectors.delete(group.selector);
|
|
812
|
+
scheduleRender();
|
|
813
|
+
});
|
|
814
|
+
listHideButton.addEventListener("click", () => {
|
|
815
|
+
for (const group of getBulkActionGroups()) mutedGroupSelectors.add(group.selector);
|
|
816
|
+
scheduleRender();
|
|
817
|
+
});
|
|
818
|
+
const body = h("div", { className: "sdt-hm-body" }, status, listHeader, list);
|
|
819
|
+
const details = h("div", { className: "sdt-hm-details" }, head, body);
|
|
820
|
+
function getGroups() {
|
|
821
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
822
|
+
if (serverClickmap.path !== currentPath) return [];
|
|
823
|
+
const searchQuery = filters.elementSearch.trim().toLowerCase();
|
|
824
|
+
const matchesSearch = (entry) => {
|
|
825
|
+
if (searchQuery === "") return true;
|
|
826
|
+
return [
|
|
827
|
+
entry.elementsText,
|
|
828
|
+
entry.tagName,
|
|
829
|
+
entry.href ?? "",
|
|
830
|
+
entry.elementsChain
|
|
831
|
+
].some((value) => value.toLowerCase().includes(searchQuery));
|
|
832
|
+
};
|
|
833
|
+
if (serverClickmap.elements.length > 0) {
|
|
834
|
+
ensureDomIndex();
|
|
835
|
+
for (const elementEntry of serverClickmap.elements) {
|
|
836
|
+
if (!matchesSearch(elementEntry)) continue;
|
|
837
|
+
const chain = parseElementsChain(elementEntry.elementsChain);
|
|
838
|
+
let element = chain.length > 0 ? inferElementFromChain(chain) : null;
|
|
839
|
+
if (element == null && elementEntry.href != null && elementEntry.href !== "" && elementEntry.tagName.toLowerCase() === "a") element = queryUniqueBySelector(`a[href="${cssEscapeAttrValue(elementEntry.href)}"]`);
|
|
840
|
+
if (element == null) continue;
|
|
841
|
+
const { target, key } = resolveClickTarget(element);
|
|
842
|
+
const existing = byKey.get(key);
|
|
843
|
+
if (existing != null) {
|
|
844
|
+
existing.count += elementEntry.clicks;
|
|
845
|
+
existing.deadCount += elementEntry.deadClicks;
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
byKey.set(key, {
|
|
849
|
+
selector: key,
|
|
850
|
+
label: getReadableElementLabel(target),
|
|
851
|
+
count: elementEntry.clicks,
|
|
852
|
+
deadCount: elementEntry.deadClicks,
|
|
853
|
+
element: target,
|
|
854
|
+
rect: target.getBoundingClientRect()
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (byKey.size === 0) for (const selectorClickmap of serverClickmap.selectors) {
|
|
859
|
+
if (searchQuery !== "" && !selectorClickmap.selector.toLowerCase().includes(searchQuery)) continue;
|
|
860
|
+
const element = getElementFromSelector(selectorClickmap.selector);
|
|
861
|
+
if (element == null) continue;
|
|
862
|
+
const { target, key } = resolveClickTarget(element);
|
|
863
|
+
const existing = byKey.get(key);
|
|
864
|
+
if (existing != null) {
|
|
865
|
+
existing.count += selectorClickmap.clicks;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
byKey.set(key, {
|
|
869
|
+
selector: key,
|
|
870
|
+
label: getReadableElementLabel(target),
|
|
871
|
+
count: selectorClickmap.clicks,
|
|
872
|
+
deadCount: 0,
|
|
873
|
+
element: target,
|
|
874
|
+
rect: target.getBoundingClientRect()
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
let groups = Array.from(byKey.values());
|
|
878
|
+
if (!filters.showDead) groups = groups.filter((group) => getGroupDisplayCount(group) > 0);
|
|
879
|
+
return groups.sort((a, b) => getGroupDisplayCount(b) - getGroupDisplayCount(a) || stringCompare(a.selector, b.selector));
|
|
880
|
+
}
|
|
881
|
+
function getGroupDisplayCount(group) {
|
|
882
|
+
return filters.showDead ? group.count : Math.max(0, group.count - group.deadCount);
|
|
883
|
+
}
|
|
884
|
+
function getDeadClickPercentage(group) {
|
|
885
|
+
if (group.count <= 0) return 100;
|
|
886
|
+
return Math.min(100, Math.round(group.deadCount / group.count * 100));
|
|
887
|
+
}
|
|
888
|
+
function scheduleRender() {
|
|
889
|
+
cancelAnimationFrame(renderFrame);
|
|
890
|
+
renderFrame = requestAnimationFrame(render);
|
|
891
|
+
}
|
|
892
|
+
function clearClickmapOverlayElements() {
|
|
893
|
+
groupOverlayElements.clear();
|
|
894
|
+
overlayRoot.replaceChildren(overlayHighlight);
|
|
895
|
+
overlayHighlight.classList.remove("sdt-hm-highlight-visible", "sdt-hm-highlight-animating");
|
|
896
|
+
highlightRenderedSelector = null;
|
|
897
|
+
}
|
|
898
|
+
function clearClickmapListElements() {
|
|
899
|
+
listRowElements.clear();
|
|
900
|
+
list.replaceChildren();
|
|
901
|
+
}
|
|
902
|
+
function getClickmapViewportSize() {
|
|
903
|
+
const visualViewport = window.visualViewport;
|
|
904
|
+
if (visualViewport != null) return {
|
|
905
|
+
width: visualViewport.width,
|
|
906
|
+
height: visualViewport.height
|
|
907
|
+
};
|
|
908
|
+
return {
|
|
909
|
+
width: window.innerWidth,
|
|
910
|
+
height: window.innerHeight
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function shouldShowElements() {
|
|
914
|
+
return overlayVisible;
|
|
915
|
+
}
|
|
916
|
+
function toggleMutedGroup(selector) {
|
|
917
|
+
if (mutedGroupSelectors.has(selector)) mutedGroupSelectors.delete(selector);
|
|
918
|
+
else mutedGroupSelectors.add(selector);
|
|
919
|
+
scheduleRender();
|
|
920
|
+
}
|
|
921
|
+
function clearSelection() {
|
|
922
|
+
if (selectedGroupSelectors.size === 0 && highlightedGroupSelector == null) return;
|
|
923
|
+
selectedGroupSelectors.clear();
|
|
924
|
+
selectionAnchorSelector = null;
|
|
925
|
+
highlightedGroupSelector = null;
|
|
926
|
+
scheduleRender();
|
|
927
|
+
}
|
|
928
|
+
function toggleSelectedGroup(selector) {
|
|
929
|
+
if (selectedGroupSelectors.has(selector)) {
|
|
930
|
+
selectedGroupSelectors.delete(selector);
|
|
931
|
+
if (highlightedGroupSelector === selector) highlightedGroupSelector = null;
|
|
932
|
+
} else {
|
|
933
|
+
selectedGroupSelectors.add(selector);
|
|
934
|
+
highlightedGroupSelector = selector;
|
|
935
|
+
}
|
|
936
|
+
selectionAnchorSelector = selector;
|
|
937
|
+
scheduleRender();
|
|
938
|
+
}
|
|
939
|
+
function selectGroupFromEvent(group, event) {
|
|
940
|
+
const toggle = event.ctrlKey || event.metaKey;
|
|
941
|
+
if (event.shiftKey && selectionAnchorSelector != null) {
|
|
942
|
+
const order = latestGroups.map((candidate) => candidate.selector);
|
|
943
|
+
const anchorIndex = order.indexOf(selectionAnchorSelector);
|
|
944
|
+
const targetIndex = order.indexOf(group.selector);
|
|
945
|
+
if (anchorIndex !== -1 && targetIndex !== -1) {
|
|
946
|
+
if (!toggle) selectedGroupSelectors.clear();
|
|
947
|
+
const [start, end] = anchorIndex <= targetIndex ? [anchorIndex, targetIndex] : [targetIndex, anchorIndex];
|
|
948
|
+
for (const selector of order.slice(start, end + 1)) selectedGroupSelectors.add(selector);
|
|
949
|
+
highlightedGroupSelector = group.selector;
|
|
950
|
+
scheduleRender();
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
if (toggle) {
|
|
955
|
+
toggleSelectedGroup(group.selector);
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
selectionAnchorSelector = group.selector;
|
|
959
|
+
if (selectedGroupSelectors.size === 1 && selectedGroupSelectors.has(group.selector)) {
|
|
960
|
+
selectedGroupSelectors.delete(group.selector);
|
|
961
|
+
highlightedGroupSelector = null;
|
|
962
|
+
} else {
|
|
963
|
+
selectedGroupSelectors.clear();
|
|
964
|
+
selectedGroupSelectors.add(group.selector);
|
|
965
|
+
highlightedGroupSelector = group.selector;
|
|
966
|
+
}
|
|
967
|
+
scheduleRender();
|
|
968
|
+
}
|
|
969
|
+
function getBulkActionGroups() {
|
|
970
|
+
if (selectedGroupSelectors.size > 0) return latestGroups.filter((group) => selectedGroupSelectors.has(group.selector));
|
|
971
|
+
return latestGroups;
|
|
972
|
+
}
|
|
973
|
+
function setHoveredGroup(selector) {
|
|
974
|
+
if (hoveredGroupSelector === selector) return;
|
|
975
|
+
hoveredGroupSelector = selector;
|
|
976
|
+
scheduleRender();
|
|
977
|
+
}
|
|
978
|
+
const checkIconSvg = "<svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 6 9 17l-5-5\"/></svg>";
|
|
979
|
+
const dashIconSvg = "<svg width=\"11\" height=\"11\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14\"/></svg>";
|
|
980
|
+
function createListRowElement(selector) {
|
|
981
|
+
const count = h("span", { className: "sdt-hm-row-count" });
|
|
982
|
+
const label = h("span", { className: "sdt-hm-row-label" });
|
|
983
|
+
const dead = h("span", { className: "sdt-hm-row-dead" });
|
|
984
|
+
const selectorText = h("span", { className: "sdt-hm-row-selector" });
|
|
985
|
+
const check = h("button", {
|
|
986
|
+
className: "sdt-hm-row-check",
|
|
987
|
+
type: "button",
|
|
988
|
+
role: "checkbox",
|
|
989
|
+
"aria-checked": "false"
|
|
990
|
+
});
|
|
991
|
+
const eye = h("button", {
|
|
992
|
+
className: "sdt-hm-row-eye",
|
|
993
|
+
type: "button"
|
|
994
|
+
});
|
|
995
|
+
const row = h("div", {
|
|
996
|
+
className: "sdt-hm-row",
|
|
997
|
+
role: "button",
|
|
998
|
+
tabindex: "0"
|
|
999
|
+
}, check, count, h("span", { className: "sdt-hm-row-meta" }, h("span", { className: "sdt-hm-row-label-row" }, label, dead), selectorText), eye);
|
|
1000
|
+
const rowElement = {
|
|
1001
|
+
row,
|
|
1002
|
+
count,
|
|
1003
|
+
check,
|
|
1004
|
+
eye,
|
|
1005
|
+
label,
|
|
1006
|
+
dead,
|
|
1007
|
+
selector: selectorText,
|
|
1008
|
+
group: null,
|
|
1009
|
+
renderedEyeIcon: "",
|
|
1010
|
+
renderedCheckIcon: ""
|
|
1011
|
+
};
|
|
1012
|
+
check.addEventListener("click", (event) => {
|
|
1013
|
+
event.preventDefault();
|
|
1014
|
+
event.stopPropagation();
|
|
1015
|
+
if (rowElement.group == null) return;
|
|
1016
|
+
if (event.shiftKey && selectionAnchorSelector != null) {
|
|
1017
|
+
selectGroupFromEvent(rowElement.group, {
|
|
1018
|
+
shiftKey: true,
|
|
1019
|
+
ctrlKey: true,
|
|
1020
|
+
metaKey: false
|
|
1021
|
+
});
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
toggleSelectedGroup(rowElement.group.selector);
|
|
1025
|
+
});
|
|
1026
|
+
eye.addEventListener("click", (event) => {
|
|
1027
|
+
event.preventDefault();
|
|
1028
|
+
event.stopPropagation();
|
|
1029
|
+
toggleMutedGroup(selector);
|
|
1030
|
+
});
|
|
1031
|
+
row.addEventListener("click", (event) => {
|
|
1032
|
+
if (rowElement.group == null) return;
|
|
1033
|
+
selectGroupFromEvent(rowElement.group, event);
|
|
1034
|
+
});
|
|
1035
|
+
row.addEventListener("keydown", (event) => {
|
|
1036
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1037
|
+
event.preventDefault();
|
|
1038
|
+
if (rowElement.group == null) return;
|
|
1039
|
+
selectGroupFromEvent(rowElement.group, event);
|
|
1040
|
+
});
|
|
1041
|
+
return rowElement;
|
|
1042
|
+
}
|
|
1043
|
+
function updateListRowElement(rowElement, group) {
|
|
1044
|
+
const muted = mutedGroupSelectors.has(group.selector);
|
|
1045
|
+
const highlighted = highlightedGroupSelector === group.selector;
|
|
1046
|
+
const selected = selectedGroupSelectors.has(group.selector);
|
|
1047
|
+
rowElement.group = group;
|
|
1048
|
+
rowElement.row.classList.toggle("sdt-hm-row-muted", muted);
|
|
1049
|
+
rowElement.row.classList.toggle("sdt-hm-row-highlighted", highlighted);
|
|
1050
|
+
rowElement.row.classList.toggle("sdt-hm-row-selected", selected);
|
|
1051
|
+
rowElement.check.setAttribute("aria-checked", String(selected));
|
|
1052
|
+
rowElement.check.setAttribute("aria-label", selected ? `Deselect ${group.label}` : `Select ${group.label}`);
|
|
1053
|
+
const checkIcon = selected ? checkIconSvg : "";
|
|
1054
|
+
if (rowElement.renderedCheckIcon !== checkIcon) {
|
|
1055
|
+
rowElement.renderedCheckIcon = checkIcon;
|
|
1056
|
+
setHtml(rowElement.check, checkIcon);
|
|
1057
|
+
}
|
|
1058
|
+
rowElement.count.textContent = formatClickmapCount(getGroupDisplayCount(group));
|
|
1059
|
+
rowElement.eye.setAttribute("aria-pressed", String(muted));
|
|
1060
|
+
rowElement.eye.setAttribute("aria-label", muted ? `Unmute ${group.label}` : `Mute ${group.label}`);
|
|
1061
|
+
rowElement.eye.title = muted ? "Unmute element" : "Mute element";
|
|
1062
|
+
const eyeIcon = muted ? eyeOffIconSvg : eyeIconSvg;
|
|
1063
|
+
if (rowElement.renderedEyeIcon !== eyeIcon) {
|
|
1064
|
+
rowElement.renderedEyeIcon = eyeIcon;
|
|
1065
|
+
setHtml(rowElement.eye, eyeIcon);
|
|
1066
|
+
}
|
|
1067
|
+
rowElement.label.textContent = group.label;
|
|
1068
|
+
if (filters.showDead && group.deadCount > 0) {
|
|
1069
|
+
const deadPct = getDeadClickPercentage(group);
|
|
1070
|
+
rowElement.dead.textContent = `${deadPct}% dead`;
|
|
1071
|
+
rowElement.dead.title = `${formatClickmapCount(group.deadCount)} of ${formatClickmapCount(group.count)} clicks had no visible effect`;
|
|
1072
|
+
rowElement.dead.classList.add("sdt-hm-row-dead-visible");
|
|
1073
|
+
} else {
|
|
1074
|
+
rowElement.dead.textContent = "";
|
|
1075
|
+
rowElement.dead.title = "";
|
|
1076
|
+
rowElement.dead.classList.remove("sdt-hm-row-dead-visible");
|
|
1077
|
+
}
|
|
1078
|
+
rowElement.selector.textContent = group.selector;
|
|
1079
|
+
}
|
|
1080
|
+
function renderOverlay(groups) {
|
|
1081
|
+
const nextMode = shouldShowElements() ? "elements" : "hidden";
|
|
1082
|
+
if (overlayMode !== nextMode) {
|
|
1083
|
+
overlayMode = nextMode;
|
|
1084
|
+
clearClickmapOverlayElements();
|
|
1085
|
+
}
|
|
1086
|
+
if (!shouldShowElements()) return;
|
|
1087
|
+
const visibleGroupKeys = /* @__PURE__ */ new Set();
|
|
1088
|
+
const maxCount = Math.max(1, ...groups.map(getGroupDisplayCount));
|
|
1089
|
+
for (const group of groups) {
|
|
1090
|
+
if (group.rect == null || group.rect.width <= 0 || group.rect.height <= 0) continue;
|
|
1091
|
+
visibleGroupKeys.add(group.selector);
|
|
1092
|
+
const displayCount = getGroupDisplayCount(group);
|
|
1093
|
+
const hue = getClickmapHue(displayCount, maxCount);
|
|
1094
|
+
const muted = mutedGroupSelectors.has(group.selector);
|
|
1095
|
+
const highlighted = highlightedGroupSelector === group.selector || selectedGroupSelectors.has(group.selector);
|
|
1096
|
+
let overlayElement = groupOverlayElements.get(group.selector);
|
|
1097
|
+
if (overlayElement == null) {
|
|
1098
|
+
const marker = h("button", {
|
|
1099
|
+
className: "sdt-hm-marker",
|
|
1100
|
+
type: "button",
|
|
1101
|
+
tabindex: "-1"
|
|
1102
|
+
});
|
|
1103
|
+
marker.addEventListener("click", (event) => {
|
|
1104
|
+
event.preventDefault();
|
|
1105
|
+
event.stopPropagation();
|
|
1106
|
+
toggleMutedGroup(group.selector);
|
|
1107
|
+
});
|
|
1108
|
+
marker.addEventListener("pointerenter", () => setHoveredGroup(group.selector));
|
|
1109
|
+
marker.addEventListener("pointerleave", () => {
|
|
1110
|
+
if (hoveredGroupSelector === group.selector) setHoveredGroup(null);
|
|
1111
|
+
});
|
|
1112
|
+
overlayElement = {
|
|
1113
|
+
marker,
|
|
1114
|
+
outline: h("div", { className: "sdt-hm-outline" })
|
|
1115
|
+
};
|
|
1116
|
+
groupOverlayElements.set(group.selector, overlayElement);
|
|
1117
|
+
overlayRoot.append(overlayElement.outline, overlayElement.marker);
|
|
1118
|
+
}
|
|
1119
|
+
const { marker, outline } = overlayElement;
|
|
1120
|
+
const deadSuffix = filters.showDead && group.deadCount > 0 && group.count > 0 ? ` (${getDeadClickPercentage(group)}% dead)` : "";
|
|
1121
|
+
marker.title = muted ? `Unmute ${group.selector}` : `Mute ${displayCount} clicks${deadSuffix} on ${group.selector}`;
|
|
1122
|
+
marker.setAttribute("aria-label", marker.title);
|
|
1123
|
+
marker.style.left = `${Math.round(group.rect.left + group.rect.width / 2)}px`;
|
|
1124
|
+
marker.style.top = `${Math.round(group.rect.top + group.rect.height / 2)}px`;
|
|
1125
|
+
marker.style.background = `hsla(${hue}, 96%, 58%, 0.94)`;
|
|
1126
|
+
marker.style.boxShadow = `0 0 0 1px hsla(${hue}, 96%, 22%, 0.35), 0 8px 24px hsla(${hue}, 96%, 45%, 0.32)`;
|
|
1127
|
+
marker.textContent = formatClickmapCount(displayCount);
|
|
1128
|
+
marker.classList.toggle("sdt-hm-marker-muted", muted);
|
|
1129
|
+
marker.classList.toggle("sdt-hm-marker-highlighted", highlighted);
|
|
1130
|
+
outline.style.left = `${group.rect.left}px`;
|
|
1131
|
+
outline.style.top = `${group.rect.top}px`;
|
|
1132
|
+
outline.style.width = `${group.rect.width}px`;
|
|
1133
|
+
outline.style.height = `${group.rect.height}px`;
|
|
1134
|
+
outline.style.borderColor = `hsla(${hue}, 96%, 58%, 0.5)`;
|
|
1135
|
+
outline.style.background = hoveredGroupSelector === group.selector ? `hsla(${hue}, 96%, 58%, 0.16)` : "";
|
|
1136
|
+
outline.classList.toggle("sdt-hm-outline-muted", muted);
|
|
1137
|
+
outline.classList.toggle("sdt-hm-outline-highlighted", highlighted);
|
|
1138
|
+
}
|
|
1139
|
+
for (const [key, overlayElement] of groupOverlayElements) if (!visibleGroupKeys.has(key)) {
|
|
1140
|
+
overlayElement.marker.remove();
|
|
1141
|
+
overlayElement.outline.remove();
|
|
1142
|
+
groupOverlayElements.delete(key);
|
|
1143
|
+
}
|
|
1144
|
+
renderHighlightBox(groups);
|
|
1145
|
+
}
|
|
1146
|
+
function renderHighlightBox(groups) {
|
|
1147
|
+
const group = highlightedGroupSelector == null ? null : groups.find((candidate) => candidate.selector === highlightedGroupSelector) ?? null;
|
|
1148
|
+
const rect = group?.rect ?? null;
|
|
1149
|
+
if (group == null || rect == null || rect.width <= 0 || rect.height <= 0) {
|
|
1150
|
+
if (highlightSettleTimer != null) {
|
|
1151
|
+
window.clearTimeout(highlightSettleTimer);
|
|
1152
|
+
highlightSettleTimer = null;
|
|
1153
|
+
}
|
|
1154
|
+
overlayHighlight.classList.remove("sdt-hm-highlight-visible", "sdt-hm-highlight-animating");
|
|
1155
|
+
highlightRenderedSelector = null;
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (overlayHighlight.classList.contains("sdt-hm-highlight-visible") && highlightRenderedSelector !== group.selector) {
|
|
1159
|
+
overlayHighlight.classList.add("sdt-hm-highlight-animating");
|
|
1160
|
+
if (highlightSettleTimer != null) window.clearTimeout(highlightSettleTimer);
|
|
1161
|
+
highlightSettleTimer = window.setTimeout(() => {
|
|
1162
|
+
overlayHighlight.classList.remove("sdt-hm-highlight-animating");
|
|
1163
|
+
highlightSettleTimer = null;
|
|
1164
|
+
}, 700);
|
|
1165
|
+
}
|
|
1166
|
+
highlightRenderedSelector = group.selector;
|
|
1167
|
+
overlayHighlight.style.left = `${rect.left}px`;
|
|
1168
|
+
overlayHighlight.style.top = `${rect.top}px`;
|
|
1169
|
+
overlayHighlight.style.width = `${rect.width}px`;
|
|
1170
|
+
overlayHighlight.style.height = `${rect.height}px`;
|
|
1171
|
+
overlayHighlight.classList.add("sdt-hm-highlight-visible");
|
|
1172
|
+
}
|
|
1173
|
+
function renderList(groups) {
|
|
1174
|
+
if (groups.length === 0) {
|
|
1175
|
+
clearClickmapListElements();
|
|
1176
|
+
list.appendChild(empty);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
const previousScrollTop = body.scrollTop;
|
|
1180
|
+
empty.remove();
|
|
1181
|
+
const renderedKeys = /* @__PURE__ */ new Set();
|
|
1182
|
+
let nextRowNode = list.firstChild;
|
|
1183
|
+
for (const group of groups.slice(0, 30)) {
|
|
1184
|
+
renderedKeys.add(group.selector);
|
|
1185
|
+
let rowElement = listRowElements.get(group.selector);
|
|
1186
|
+
if (rowElement == null) {
|
|
1187
|
+
rowElement = createListRowElement(group.selector);
|
|
1188
|
+
listRowElements.set(group.selector, rowElement);
|
|
1189
|
+
}
|
|
1190
|
+
updateListRowElement(rowElement, group);
|
|
1191
|
+
if (rowElement.row !== nextRowNode) list.insertBefore(rowElement.row, nextRowNode);
|
|
1192
|
+
nextRowNode = rowElement.row.nextSibling;
|
|
1193
|
+
}
|
|
1194
|
+
for (const [selector, rowElement] of listRowElements) {
|
|
1195
|
+
if (renderedKeys.has(selector)) continue;
|
|
1196
|
+
rowElement.row.remove();
|
|
1197
|
+
listRowElements.delete(selector);
|
|
1198
|
+
}
|
|
1199
|
+
body.scrollTop = previousScrollTop;
|
|
1200
|
+
}
|
|
1201
|
+
let renderedHeaderCheckIcon = "";
|
|
1202
|
+
function syncListHeader(groups) {
|
|
1203
|
+
const visible = groups.length > 0 || filters.elementSearch.trim() !== "";
|
|
1204
|
+
listHeader.classList.toggle("sdt-hm-list-header-visible", visible);
|
|
1205
|
+
if (!visible) return;
|
|
1206
|
+
const selectedCount = selectedGroupSelectors.size;
|
|
1207
|
+
const allSelected = selectedCount > 0 && groups.every((group) => selectedGroupSelectors.has(group.selector));
|
|
1208
|
+
listHeaderCheck.setAttribute("aria-checked", allSelected ? "true" : selectedCount > 0 ? "mixed" : "false");
|
|
1209
|
+
listHeaderCheck.setAttribute("aria-label", allSelected ? "Clear selection" : "Select all elements");
|
|
1210
|
+
const headerCheckIcon = allSelected ? checkIconSvg : selectedCount > 0 ? dashIconSvg : "";
|
|
1211
|
+
if (renderedHeaderCheckIcon !== headerCheckIcon) {
|
|
1212
|
+
renderedHeaderCheckIcon = headerCheckIcon;
|
|
1213
|
+
setHtml(listHeaderCheck, headerCheckIcon);
|
|
1214
|
+
}
|
|
1215
|
+
listHeaderSummary.textContent = selectedCount > 0 ? `${formatClickmapCount(selectedCount)} of ${formatClickmapCount(groups.length)} selected` : `${formatClickmapCount(groups.length)} element${groups.length === 1 ? "" : "s"}`;
|
|
1216
|
+
const bulkTargets = getBulkActionGroups();
|
|
1217
|
+
const bulkScope = selectedCount > 0 ? "selected" : "all";
|
|
1218
|
+
listShowButton.textContent = `Show ${bulkScope}`;
|
|
1219
|
+
listHideButton.textContent = `Hide ${bulkScope}`;
|
|
1220
|
+
listShowButton.disabled = bulkTargets.every((group) => !mutedGroupSelectors.has(group.selector));
|
|
1221
|
+
listHideButton.disabled = bulkTargets.every((group) => mutedGroupSelectors.has(group.selector));
|
|
1222
|
+
}
|
|
1223
|
+
function render() {
|
|
1224
|
+
if (currentPath !== window.location.pathname) {
|
|
1225
|
+
currentPath = window.location.pathname;
|
|
1226
|
+
serverClickmap = {
|
|
1227
|
+
path: currentPath,
|
|
1228
|
+
totalClicks: 0,
|
|
1229
|
+
selectors: [],
|
|
1230
|
+
elements: []
|
|
1231
|
+
};
|
|
1232
|
+
serverClickmapError = null;
|
|
1233
|
+
clearClickmapListElements();
|
|
1234
|
+
syncAutoUrlPattern();
|
|
1235
|
+
runAsynchronously(loadServerClickmap());
|
|
1236
|
+
}
|
|
1237
|
+
const groups = getGroups();
|
|
1238
|
+
latestGroups = groups;
|
|
1239
|
+
const groupKeys = new Set(groups.map((group) => group.selector));
|
|
1240
|
+
for (const mutedGroupSelector of mutedGroupSelectors) if (!groupKeys.has(mutedGroupSelector)) mutedGroupSelectors.delete(mutedGroupSelector);
|
|
1241
|
+
for (const selectedGroupSelector of selectedGroupSelectors) if (!groupKeys.has(selectedGroupSelector)) selectedGroupSelectors.delete(selectedGroupSelector);
|
|
1242
|
+
if (selectionAnchorSelector != null && !groupKeys.has(selectionAnchorSelector)) selectionAnchorSelector = null;
|
|
1243
|
+
if (highlightedGroupSelector != null && !groupKeys.has(highlightedGroupSelector)) highlightedGroupSelector = null;
|
|
1244
|
+
if (hoveredGroupSelector != null && !groupKeys.has(hoveredGroupSelector)) hoveredGroupSelector = null;
|
|
1245
|
+
const mappedClicks = groups.reduce((sum, group) => sum + getGroupDisplayCount(group), 0);
|
|
1246
|
+
const aggregateClicks = serverClickmap.path === currentPath ? serverClickmap.totalClicks : 0;
|
|
1247
|
+
const viewport = getClickmapViewportSize();
|
|
1248
|
+
const roundedViewportWidth = Math.round(viewport.width);
|
|
1249
|
+
const roundedViewportHeight = Math.round(viewport.height);
|
|
1250
|
+
const selectedViewportBucket = getClickmapViewportBucket(filters.device);
|
|
1251
|
+
const viewportFilterMatches = selectedViewportBucket == null || isClickmapViewportWidthInBucket(roundedViewportWidth, selectedViewportBucket);
|
|
1252
|
+
statsCount.textContent = formatClickmapCount(aggregateClicks);
|
|
1253
|
+
selectorCount.textContent = formatClickmapCount(groups.length);
|
|
1254
|
+
viewportValue.textContent = `${roundedViewportWidth}x${roundedViewportHeight}`;
|
|
1255
|
+
overlayToggle.textContent = overlayVisible ? "Hide overlay" : "Show overlay";
|
|
1256
|
+
syncOverlayMiniToggle();
|
|
1257
|
+
viewportWarning.classList.toggle("sdt-hm-viewport-warning-visible", !viewportFilterMatches);
|
|
1258
|
+
if (selectedViewportBucket != null && !viewportFilterMatches) {
|
|
1259
|
+
const recommendedWidth = getClickmapRecommendedViewportWidth(selectedViewportBucket);
|
|
1260
|
+
const recommendedHeight = Math.max(1, roundedViewportHeight);
|
|
1261
|
+
viewportWarningTitle.textContent = "Viewport filter mismatch";
|
|
1262
|
+
viewportWarningBody.textContent = `This page is ${roundedViewportWidth}px wide, but ${filters.device} is ${formatClickmapViewportBucket(selectedViewportBucket)}. Resize the window or use the DevTools device toolbar before comparing this clickmap.`;
|
|
1263
|
+
viewportWarningWidthValue.textContent = String(recommendedWidth);
|
|
1264
|
+
viewportWarningHeightValue.textContent = String(recommendedHeight);
|
|
1265
|
+
}
|
|
1266
|
+
const effectiveUrlPattern = getEffectiveUrlPattern();
|
|
1267
|
+
const urlPatternMatchesPath = patternMatchesPath(effectiveUrlPattern, currentPath);
|
|
1268
|
+
syncUrlPatternResetVisibility();
|
|
1269
|
+
const token = getClickmapTokenFromStorage();
|
|
1270
|
+
const tokenOrigin = getClickmapOriginFromStorage();
|
|
1271
|
+
if (token == null) status.textContent = serverClickmapError ?? "No clickmap token in sessionStorage. Paste one from the dashboard to load this page.";
|
|
1272
|
+
else if (tokenOrigin != null && tokenOrigin !== window.location.origin) status.textContent = `Token was minted for ${tokenOrigin}, but this page is ${window.location.origin}. Generate a token for this exact origin.`;
|
|
1273
|
+
else if (loadingServerClickmap) status.textContent = "Loading aggregate clickmap...";
|
|
1274
|
+
else if (serverClickmapError != null) status.textContent = serverClickmapError;
|
|
1275
|
+
else {
|
|
1276
|
+
const scope = effectiveUrlPattern !== "" && effectiveUrlPattern !== currentPath ? effectiveUrlPattern : currentPath;
|
|
1277
|
+
let message = `Loaded ${formatClickmapCount(aggregateClicks)} aggregate clicks for ${scope}.`;
|
|
1278
|
+
if (aggregateClicks === 0) message = `No clicks recorded for ${scope} in this range.`;
|
|
1279
|
+
else if (!urlPatternMatchesPath) message += " This page isn’t covered by the pattern — reset it or open a matching page to see the overlay.";
|
|
1280
|
+
else if (groups.length === 0) message += " No matching elements found on this page yet.";
|
|
1281
|
+
else if (mappedClicks < aggregateClicks) message += ` ${formatClickmapCount(mappedClicks)} mapped to elements on this page.`;
|
|
1282
|
+
status.textContent = message;
|
|
1283
|
+
}
|
|
1284
|
+
status.classList.toggle("sdt-hm-token-status-error", serverClickmapError != null || token != null && tokenOrigin != null && tokenOrigin !== window.location.origin);
|
|
1285
|
+
miniClicks.textContent = formatClickmapCount(aggregateClicks);
|
|
1286
|
+
miniElements.textContent = formatClickmapCount(groups.length);
|
|
1287
|
+
container.classList.toggle("sdt-hm-expanded", expanded);
|
|
1288
|
+
expandButton.setAttribute("aria-expanded", String(expanded));
|
|
1289
|
+
expandButton.setAttribute("aria-label", expanded ? "Collapse clickmap options" : "Expand clickmap options");
|
|
1290
|
+
expandButton.setAttribute("data-sdt-tip", expanded ? "Collapse clickmap options" : "Expand clickmap options");
|
|
1291
|
+
syncExpandIcon();
|
|
1292
|
+
positionDeviceThumb();
|
|
1293
|
+
renderOverlay(groups);
|
|
1294
|
+
syncListHeader(groups);
|
|
1295
|
+
renderList(groups);
|
|
1296
|
+
}
|
|
1297
|
+
async function loadServerClickmap() {
|
|
1298
|
+
const requestId = serverClickmapRequestId + 1;
|
|
1299
|
+
serverClickmapRequestId = requestId;
|
|
1300
|
+
const isLatestRequest = () => requestId === serverClickmapRequestId;
|
|
1301
|
+
const token = getClickmapTokenFromStorage();
|
|
1302
|
+
if (token == null) {
|
|
1303
|
+
serverClickmap = {
|
|
1304
|
+
path: currentPath,
|
|
1305
|
+
totalClicks: 0,
|
|
1306
|
+
selectors: [],
|
|
1307
|
+
elements: []
|
|
1308
|
+
};
|
|
1309
|
+
serverClickmapError = null;
|
|
1310
|
+
loadingServerClickmap = false;
|
|
1311
|
+
render();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const tokenOrigin = getClickmapOriginFromStorage();
|
|
1315
|
+
if (tokenOrigin != null && tokenOrigin !== window.location.origin) {
|
|
1316
|
+
serverClickmap = {
|
|
1317
|
+
path: currentPath,
|
|
1318
|
+
totalClicks: 0,
|
|
1319
|
+
selectors: [],
|
|
1320
|
+
elements: []
|
|
1321
|
+
};
|
|
1322
|
+
serverClickmapError = null;
|
|
1323
|
+
loadingServerClickmap = false;
|
|
1324
|
+
render();
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
loadingServerClickmap = true;
|
|
1328
|
+
serverClickmapError = null;
|
|
1329
|
+
render();
|
|
1330
|
+
try {
|
|
1331
|
+
const until = /* @__PURE__ */ new Date();
|
|
1332
|
+
const since = new Date(until.getTime() - CLICKMAP_RANGE_MS[filters.range]);
|
|
1333
|
+
const requestedPath = window.location.pathname;
|
|
1334
|
+
const effectiveUrlPattern = getEffectiveUrlPattern();
|
|
1335
|
+
const body = {
|
|
1336
|
+
clickmap_token: token,
|
|
1337
|
+
origin: window.location.origin,
|
|
1338
|
+
since: since.toISOString(),
|
|
1339
|
+
until: until.toISOString()
|
|
1340
|
+
};
|
|
1341
|
+
if (effectiveUrlPattern !== "") body.url_pattern = effectiveUrlPattern;
|
|
1342
|
+
else body.route_path = requestedPath;
|
|
1343
|
+
if (filters.device !== "all") body.device = filters.device;
|
|
1344
|
+
const response = await app[hexclaveAppInternalsSymbol].sendRequest("/analytics/clickmap", {
|
|
1345
|
+
method: "POST",
|
|
1346
|
+
headers: { "content-type": "application/json" },
|
|
1347
|
+
body: JSON.stringify(body)
|
|
1348
|
+
}, "client");
|
|
1349
|
+
if (!response.ok) throw new Error(`Clickmap request failed with HTTP ${response.status}`);
|
|
1350
|
+
const responseBody = await response.json();
|
|
1351
|
+
if (!isLatestRequest()) return;
|
|
1352
|
+
serverClickmap = parseServerClickmapResponse(responseBody, requestedPath);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
if (!isLatestRequest()) return;
|
|
1355
|
+
serverClickmap = {
|
|
1356
|
+
path: currentPath,
|
|
1357
|
+
totalClicks: 0,
|
|
1358
|
+
selectors: [],
|
|
1359
|
+
elements: []
|
|
1360
|
+
};
|
|
1361
|
+
if (error instanceof Error && error.message.includes("Clickmap token does not belong to this project")) {
|
|
1362
|
+
clearClickmapTokenStorage();
|
|
1363
|
+
serverClickmapError = "The stored clickmap token belongs to another project. Generate a fresh token for this project.";
|
|
1364
|
+
} else serverClickmapError = error instanceof Error ? error.message : "Failed to load clickmap data";
|
|
1365
|
+
} finally {
|
|
1366
|
+
if (isLatestRequest()) {
|
|
1367
|
+
loadingServerClickmap = false;
|
|
1368
|
+
render();
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
const onBeforeUnloadResume = () => {
|
|
1373
|
+
const token = getClickmapTokenFromStorage();
|
|
1374
|
+
const tokenOrigin = getClickmapOriginFromStorage();
|
|
1375
|
+
if (token == null || tokenOrigin != null && tokenOrigin !== window.location.origin) return;
|
|
1376
|
+
try {
|
|
1377
|
+
sessionStorage.setItem(CLICKMAP_OVERLAY_RESUME_STORAGE_KEY, "1");
|
|
1378
|
+
} catch {}
|
|
1379
|
+
};
|
|
1380
|
+
const pinPressToButton = (button) => {
|
|
1381
|
+
button.addEventListener("pointerdown", (event) => {
|
|
1382
|
+
try {
|
|
1383
|
+
button.setPointerCapture(event.pointerId);
|
|
1384
|
+
} catch {}
|
|
1385
|
+
});
|
|
1386
|
+
};
|
|
1387
|
+
pinPressToButton(overlayToggle);
|
|
1388
|
+
pinPressToButton(closeButton);
|
|
1389
|
+
pinPressToButton(expandButton);
|
|
1390
|
+
pinPressToButton(showDeadMiniToggle);
|
|
1391
|
+
pinPressToButton(showDeadToggle);
|
|
1392
|
+
pinPressToButton(overlayMiniToggle);
|
|
1393
|
+
pinPressToButton(listHeaderCheck);
|
|
1394
|
+
pinPressToButton(listShowButton);
|
|
1395
|
+
pinPressToButton(listHideButton);
|
|
1396
|
+
overlayToggle.addEventListener("click", () => {
|
|
1397
|
+
overlayVisible = !overlayVisible;
|
|
1398
|
+
render();
|
|
1399
|
+
});
|
|
1400
|
+
closeButton.addEventListener("click", onClose);
|
|
1401
|
+
expandButton.addEventListener("click", () => {
|
|
1402
|
+
expanded = !expanded;
|
|
1403
|
+
render();
|
|
1404
|
+
});
|
|
1405
|
+
const onTokenUpdated = () => {
|
|
1406
|
+
runAsynchronously(loadServerClickmap());
|
|
1407
|
+
};
|
|
1408
|
+
const routePollInterval = window.setInterval(scheduleRender, 500);
|
|
1409
|
+
const isSelfMutationTarget = (target) => {
|
|
1410
|
+
const element = target instanceof Element ? target : target?.parentElement ?? null;
|
|
1411
|
+
if (element == null) return false;
|
|
1412
|
+
return overlayRoot.contains(element) || element.closest(`#${cssEscapeIdent(CLICKMAP_ROOT_ID)}, #${cssEscapeIdent(DEV_TOOL_ROOT_ID)}`) != null;
|
|
1413
|
+
};
|
|
1414
|
+
const mutationObserver = new MutationObserver((mutations) => {
|
|
1415
|
+
if (mutations.every((mutation) => isSelfMutationTarget(mutation.target))) return;
|
|
1416
|
+
scheduleDomIndexInvalidation();
|
|
1417
|
+
scheduleRender();
|
|
1418
|
+
});
|
|
1419
|
+
const visualViewport = window.visualViewport;
|
|
1420
|
+
mutationObserver.observe(document.body, {
|
|
1421
|
+
attributes: true,
|
|
1422
|
+
childList: true,
|
|
1423
|
+
subtree: true
|
|
1424
|
+
});
|
|
1425
|
+
(document.getElementById(CLICKMAP_ROOT_ID) ?? document.body).appendChild(overlayRoot);
|
|
1426
|
+
rebuildDomIndex();
|
|
1427
|
+
scheduleRender();
|
|
1428
|
+
window.addEventListener("beforeunload", onBeforeUnloadResume);
|
|
1429
|
+
const onWindowResize = () => {
|
|
1430
|
+
scheduleRender();
|
|
1431
|
+
};
|
|
1432
|
+
document.addEventListener("scroll", scheduleRender, true);
|
|
1433
|
+
window.addEventListener("resize", onWindowResize);
|
|
1434
|
+
visualViewport?.addEventListener("resize", scheduleRender);
|
|
1435
|
+
visualViewport?.addEventListener("scroll", scheduleRender);
|
|
1436
|
+
window.addEventListener(CLICKMAP_OVERLAY_TOKEN_UPDATED_EVENT, onTokenUpdated);
|
|
1437
|
+
const onDocumentPointerDown = (event) => {
|
|
1438
|
+
if (!urlHelpOpen) return;
|
|
1439
|
+
if (event.target instanceof Node && urlPatternHelp.contains(event.target)) return;
|
|
1440
|
+
if (event.target instanceof Node && urlPatternInfo.contains(event.target)) return;
|
|
1441
|
+
setUrlHelpOpen(false);
|
|
1442
|
+
};
|
|
1443
|
+
const onDocumentKeyDown = (event) => {
|
|
1444
|
+
if (event.key !== "Escape") return;
|
|
1445
|
+
if (urlHelpOpen) {
|
|
1446
|
+
setUrlHelpOpen(false);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
clearSelection();
|
|
1450
|
+
};
|
|
1451
|
+
document.addEventListener("mousedown", onDocumentPointerDown, true);
|
|
1452
|
+
document.addEventListener("keydown", onDocumentKeyDown, true);
|
|
1453
|
+
render();
|
|
1454
|
+
runAsynchronously(loadServerClickmap());
|
|
1455
|
+
container.append(details, toolbar);
|
|
1456
|
+
return {
|
|
1457
|
+
element: container,
|
|
1458
|
+
cleanup: () => {
|
|
1459
|
+
cancelAnimationFrame(renderFrame);
|
|
1460
|
+
if (domIndexDebounce !== 0) window.clearTimeout(domIndexDebounce);
|
|
1461
|
+
if (filterReloadDebounce !== 0) window.clearTimeout(filterReloadDebounce);
|
|
1462
|
+
if (elementSearchDebounce !== 0) window.clearTimeout(elementSearchDebounce);
|
|
1463
|
+
window.clearInterval(routePollInterval);
|
|
1464
|
+
mutationObserver.disconnect();
|
|
1465
|
+
clearClickmapOverlayElements();
|
|
1466
|
+
domIndex.clear();
|
|
1467
|
+
window.removeEventListener("beforeunload", onBeforeUnloadResume);
|
|
1468
|
+
document.removeEventListener("scroll", scheduleRender, true);
|
|
1469
|
+
window.removeEventListener("resize", onWindowResize);
|
|
1470
|
+
visualViewport?.removeEventListener("resize", scheduleRender);
|
|
1471
|
+
visualViewport?.removeEventListener("scroll", scheduleRender);
|
|
1472
|
+
window.removeEventListener(CLICKMAP_OVERLAY_TOKEN_UPDATED_EVENT, onTokenUpdated);
|
|
1473
|
+
document.removeEventListener("mousedown", onDocumentPointerDown, true);
|
|
1474
|
+
document.removeEventListener("keydown", onDocumentKeyDown, true);
|
|
1475
|
+
overlayRoot.remove();
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
const GLOBAL_INSTANCE_KEY = "__hexclave-clickmap-instance";
|
|
1480
|
+
/**
|
|
1481
|
+
* Opens the clickmap overlay: mounts its own root element, injects its own
|
|
1482
|
+
* styles, and shows the bottom-centered panel.
|
|
1483
|
+
*
|
|
1484
|
+
* Returns a cleanup that tears everything down. `onClosed` fires exactly once,
|
|
1485
|
+
* when the overlay is closed (by the user or via the returned cleanup), so the
|
|
1486
|
+
* caller can let a later token event reopen it.
|
|
1487
|
+
*/
|
|
1488
|
+
function openClickmapOverlay(app, onClosed) {
|
|
1489
|
+
if (typeof document === "undefined" || typeof document.createElement !== "function") return () => {};
|
|
1490
|
+
const body = Reflect.get(document, "body");
|
|
1491
|
+
if (!hasAppendChild(body)) return () => {};
|
|
1492
|
+
getGlobalUiInstance(GLOBAL_INSTANCE_KEY)?.cleanup();
|
|
1493
|
+
let existingRoot = document.getElementById(CLICKMAP_ROOT_ID);
|
|
1494
|
+
while (existingRoot !== null) {
|
|
1495
|
+
existingRoot.remove();
|
|
1496
|
+
existingRoot = document.getElementById(CLICKMAP_ROOT_ID);
|
|
1497
|
+
}
|
|
1498
|
+
const root = document.createElement("div");
|
|
1499
|
+
root.id = CLICKMAP_ROOT_ID;
|
|
1500
|
+
body.appendChild(root);
|
|
1501
|
+
const wrapper = h("div", { className: "hexclave-clickmap" });
|
|
1502
|
+
root.appendChild(wrapper);
|
|
1503
|
+
const style = document.createElement("style");
|
|
1504
|
+
style.textContent = clickmapCSS;
|
|
1505
|
+
wrapper.appendChild(style);
|
|
1506
|
+
const panel = createClickmapPanel(app, () => instance.cleanup());
|
|
1507
|
+
wrapper.appendChild(h("div", { className: "sdt-hm-panel" }, h("div", { className: "sdt-hm-panel-inner" }, panel.element)));
|
|
1508
|
+
let didCleanup = false;
|
|
1509
|
+
const instance = { cleanup: () => {
|
|
1510
|
+
if (didCleanup) return;
|
|
1511
|
+
didCleanup = true;
|
|
1512
|
+
if (getGlobalUiInstance(GLOBAL_INSTANCE_KEY) === instance) setGlobalUiInstance(GLOBAL_INSTANCE_KEY, null);
|
|
1513
|
+
panel.cleanup?.();
|
|
1514
|
+
if (root.parentNode) root.parentNode.removeChild(root);
|
|
1515
|
+
onClosed();
|
|
1516
|
+
} };
|
|
1517
|
+
setGlobalUiInstance(GLOBAL_INSTANCE_KEY, instance);
|
|
1518
|
+
return () => {
|
|
1519
|
+
instance.cleanup();
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
//#endregion
|
|
1524
|
+
export { openClickmapOverlay };
|
|
1525
|
+
//# sourceMappingURL=clickmap-core.js.map
|