@gem-sdk/clarity-visualize 0.8.94
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/CHANGELOG.md +7 -0
- package/README.md +1 -0
- package/build/clarity.visualize.js +2753 -0
- package/build/clarity.visualize.min.js +1 -0
- package/build/clarity.visualize.module.js +2748 -0
- package/package.json +45 -0
- package/rollup.config.ts +79 -0
- package/src/clarity.ts +3 -0
- package/src/custom/README.md +87 -0
- package/src/custom/canvas-layer.ts +355 -0
- package/src/custom/dialog.ts +242 -0
- package/src/custom/index.ts +7 -0
- package/src/data.ts +103 -0
- package/src/enrich.ts +80 -0
- package/src/global.ts +9 -0
- package/src/heatmap.ts +512 -0
- package/src/index.ts +4 -0
- package/src/interaction.ts +524 -0
- package/src/layout.ts +805 -0
- package/src/styles/blobUnavailable/chineseSimplified.svg +5 -0
- package/src/styles/blobUnavailable/chineseTraditional.svg +5 -0
- package/src/styles/blobUnavailable/dutch.svg +5 -0
- package/src/styles/blobUnavailable/english.svg +5 -0
- package/src/styles/blobUnavailable/french.svg +5 -0
- package/src/styles/blobUnavailable/german.svg +5 -0
- package/src/styles/blobUnavailable/iconOnly.svg +4 -0
- package/src/styles/blobUnavailable/italian.svg +5 -0
- package/src/styles/blobUnavailable/japanese.svg +5 -0
- package/src/styles/blobUnavailable/korean.svg +5 -0
- package/src/styles/blobUnavailable/portuguese.svg +5 -0
- package/src/styles/blobUnavailable/russian.svg +5 -0
- package/src/styles/blobUnavailable/spanish.svg +5 -0
- package/src/styles/blobUnavailable/turkish.svg +5 -0
- package/src/styles/iframeUnavailable/chineseSimplified.svg +5 -0
- package/src/styles/iframeUnavailable/chineseTraditional.svg +5 -0
- package/src/styles/iframeUnavailable/dutch.svg +5 -0
- package/src/styles/iframeUnavailable/english.svg +5 -0
- package/src/styles/iframeUnavailable/french.svg +5 -0
- package/src/styles/iframeUnavailable/german.svg +5 -0
- package/src/styles/iframeUnavailable/iconOnly.svg +4 -0
- package/src/styles/iframeUnavailable/italian.svg +5 -0
- package/src/styles/iframeUnavailable/japanese.svg +5 -0
- package/src/styles/iframeUnavailable/korean.svg +5 -0
- package/src/styles/iframeUnavailable/portuguese.svg +5 -0
- package/src/styles/iframeUnavailable/russian.svg +5 -0
- package/src/styles/iframeUnavailable/spanish.svg +5 -0
- package/src/styles/iframeUnavailable/turkish.svg +5 -0
- package/src/styles/imageMasked/chineseSimplified.svg +5 -0
- package/src/styles/imageMasked/chineseTraditional.svg +5 -0
- package/src/styles/imageMasked/dutch.svg +5 -0
- package/src/styles/imageMasked/english.svg +5 -0
- package/src/styles/imageMasked/french.svg +5 -0
- package/src/styles/imageMasked/german.svg +5 -0
- package/src/styles/imageMasked/iconOnly.svg +4 -0
- package/src/styles/imageMasked/italian.svg +5 -0
- package/src/styles/imageMasked/japanese.svg +5 -0
- package/src/styles/imageMasked/korean.svg +5 -0
- package/src/styles/imageMasked/portuguese.svg +5 -0
- package/src/styles/imageMasked/russian.svg +5 -0
- package/src/styles/imageMasked/spanish.svg +5 -0
- package/src/styles/imageMasked/turkish.svg +5 -0
- package/src/styles/pointer/click.css +31 -0
- package/src/styles/pointer/pointerIcon.svg +18 -0
- package/src/styles/shared.css +6 -0
- package/src/visualizer.ts +308 -0
- package/tsconfig.json +13 -0
- package/tslint.json +33 -0
- package/types/heatmap.d.ts +18 -0
- package/types/index.d.ts +11 -0
- package/types/layout.d.ts +12 -0
- package/types/string-import.d.ts +9 -0
- package/types/visualize.d.ts +253 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom module for rendering HTML Dialog elements
|
|
3
|
+
* Handles proper visualization of modal dialogs in the top-layer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Constant as LayoutConstants } from "@gem-sdk/clarity-js/types/layout";
|
|
7
|
+
|
|
8
|
+
export interface DialogRenderOptions {
|
|
9
|
+
isModal: boolean;
|
|
10
|
+
shouldBeOpen: boolean;
|
|
11
|
+
isExistingDialog: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Pending dialogs tracking for synchronization
|
|
16
|
+
* Used to track when all dialogs have finished rendering
|
|
17
|
+
* Uses Set to avoid counting the same dialog multiple times
|
|
18
|
+
*/
|
|
19
|
+
let pendingDialogs: Set<HTMLDialogElement> = new Set();
|
|
20
|
+
let resolveDialogsRendered: (() => void) | null = null;
|
|
21
|
+
let dialogsRenderedPromise: Promise<void> | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns a promise that resolves when all pending dialogs are rendered
|
|
25
|
+
*/
|
|
26
|
+
export function waitForDialogsRendered(): Promise<void> {
|
|
27
|
+
if (pendingDialogs.size === 0) {
|
|
28
|
+
// No pending dialogs, resolve immediately
|
|
29
|
+
return Promise.resolve();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Return existing promise if already waiting
|
|
33
|
+
if (dialogsRenderedPromise) {
|
|
34
|
+
return dialogsRenderedPromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create new promise
|
|
38
|
+
dialogsRenderedPromise = new Promise<void>((resolve) => {
|
|
39
|
+
resolveDialogsRendered = resolve;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return dialogsRenderedPromise;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resets the dialog rendering state
|
|
47
|
+
* Should be called before starting a new render cycle
|
|
48
|
+
*/
|
|
49
|
+
export function resetDialogRenderState(): void {
|
|
50
|
+
pendingDialogs.clear();
|
|
51
|
+
resolveDialogsRendered = null;
|
|
52
|
+
dialogsRenderedPromise = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extracts dialog rendering options from node attributes
|
|
57
|
+
*
|
|
58
|
+
* @param attributes - Node attributes containing dialog state
|
|
59
|
+
* @param dialogElement - The dialog element being rendered
|
|
60
|
+
* @returns Dialog render options
|
|
61
|
+
*/
|
|
62
|
+
export function getDialogRenderOptions(
|
|
63
|
+
attributes: { [key: string]: string },
|
|
64
|
+
dialogElement: HTMLDialogElement | null
|
|
65
|
+
): DialogRenderOptions {
|
|
66
|
+
return {
|
|
67
|
+
isModal: attributes[LayoutConstants.GXDialogModal] === "true",
|
|
68
|
+
shouldBeOpen: attributes["open"] !== undefined,
|
|
69
|
+
isExistingDialog: !!dialogElement
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Shows a dialog element with proper modal/non-modal handling
|
|
75
|
+
* Modal dialogs are shown via showModal() to render in top-layer with backdrop
|
|
76
|
+
* Non-modal dialogs are shown via show() method
|
|
77
|
+
*
|
|
78
|
+
* @param dialogElement - The dialog element to show
|
|
79
|
+
* @param isModal - Whether this is a modal dialog
|
|
80
|
+
* @param onError - Optional error callback
|
|
81
|
+
*/
|
|
82
|
+
export function showDialog(
|
|
83
|
+
dialogElement: HTMLDialogElement,
|
|
84
|
+
isModal: boolean,
|
|
85
|
+
onError?: (error: Error) => void
|
|
86
|
+
): void {
|
|
87
|
+
try {
|
|
88
|
+
if (!dialogElement.isConnected) {
|
|
89
|
+
notifyDialogComplete(dialogElement);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!dialogElement.open) {
|
|
93
|
+
notifyDialogComplete(dialogElement);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
dialogElement.close();
|
|
98
|
+
if (isModal) {
|
|
99
|
+
dialogElement.showModal();
|
|
100
|
+
} else {
|
|
101
|
+
dialogElement.show();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Wait for animations/transitions to complete
|
|
105
|
+
waitForDialogAnimation(dialogElement).then(() => {
|
|
106
|
+
notifyDialogComplete(dialogElement);
|
|
107
|
+
});
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error('❌ Error showing dialog:', e);
|
|
110
|
+
if (onError) {
|
|
111
|
+
onError(e as Error);
|
|
112
|
+
}
|
|
113
|
+
// Still mark as complete even on error
|
|
114
|
+
notifyDialogComplete(dialogElement);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Waits for dialog animations/transitions to complete
|
|
120
|
+
* Listens for animationend and transitionend events with timeout fallback
|
|
121
|
+
*/
|
|
122
|
+
function waitForDialogAnimation(dialogElement: HTMLDialogElement): Promise<void> {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
let resolved = false;
|
|
125
|
+
const timeoutMs = 1000; // Maximum wait time for animations
|
|
126
|
+
|
|
127
|
+
const complete = () => {
|
|
128
|
+
if (resolved) return;
|
|
129
|
+
resolved = true;
|
|
130
|
+
cleanup();
|
|
131
|
+
resolve();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Fallback timeout in case no animation events fire
|
|
135
|
+
const timeout = setTimeout(complete, timeoutMs);
|
|
136
|
+
|
|
137
|
+
// Listen for animation end
|
|
138
|
+
const onAnimationEnd = (e: AnimationEvent) => {
|
|
139
|
+
if (e.target === dialogElement) {
|
|
140
|
+
complete();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Listen for transition end
|
|
145
|
+
const onTransitionEnd = (TransitionEvent: TransitionEvent) => {
|
|
146
|
+
if (TransitionEvent.target === dialogElement) {
|
|
147
|
+
complete();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const cleanup = () => {
|
|
152
|
+
clearTimeout(timeout);
|
|
153
|
+
dialogElement.removeEventListener('animationend', onAnimationEnd as EventListener);
|
|
154
|
+
dialogElement.removeEventListener('transitionend', onTransitionEnd as EventListener);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
dialogElement.addEventListener('animationend', onAnimationEnd as EventListener, { once: true });
|
|
158
|
+
dialogElement.addEventListener('transitionend', onTransitionEnd as EventListener, { once: true });
|
|
159
|
+
|
|
160
|
+
// If no animations/transitions, resolve after one frame
|
|
161
|
+
requestAnimationFrame(() => {
|
|
162
|
+
const computedStyle = window.getComputedStyle(dialogElement);
|
|
163
|
+
const hasAnimation = computedStyle.animationName !== 'none';
|
|
164
|
+
const hasTransition = computedStyle.transitionDuration !== '0s';
|
|
165
|
+
|
|
166
|
+
if (!hasAnimation && !hasTransition) {
|
|
167
|
+
complete();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Internal: Notify that a dialog has completed rendering
|
|
175
|
+
*/
|
|
176
|
+
function notifyDialogComplete(dialogElement: HTMLDialogElement): void {
|
|
177
|
+
pendingDialogs.delete(dialogElement);
|
|
178
|
+
|
|
179
|
+
// If all dialogs are done, resolve the promise
|
|
180
|
+
if (pendingDialogs.size === 0 && resolveDialogsRendered) {
|
|
181
|
+
resolveDialogsRendered();
|
|
182
|
+
resolveDialogsRendered = null;
|
|
183
|
+
dialogsRenderedPromise = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Closes a dialog element safely
|
|
189
|
+
*
|
|
190
|
+
* @param dialogElement - The dialog element to close
|
|
191
|
+
*/
|
|
192
|
+
export function closeDialog(dialogElement: HTMLDialogElement): void {
|
|
193
|
+
try {
|
|
194
|
+
if (dialogElement.open) {
|
|
195
|
+
dialogElement.close();
|
|
196
|
+
}
|
|
197
|
+
} catch (e) {
|
|
198
|
+
console.warn('⚠️ Error closing dialog:', e);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Handles dialog rendering based on its state
|
|
204
|
+
* This is the main entry point for dialog rendering logic
|
|
205
|
+
*
|
|
206
|
+
* @param dialogElement - The dialog element to render
|
|
207
|
+
* @param options - Dialog render options
|
|
208
|
+
* @param onError - Optional error callback
|
|
209
|
+
*/
|
|
210
|
+
export function renderDialog(
|
|
211
|
+
dialogElement: HTMLDialogElement,
|
|
212
|
+
options: DialogRenderOptions,
|
|
213
|
+
onError?: (error: Error) => void
|
|
214
|
+
): void {
|
|
215
|
+
const { isModal, shouldBeOpen, isExistingDialog } = options;
|
|
216
|
+
if (!shouldBeOpen) return;
|
|
217
|
+
|
|
218
|
+
// Add dialog to pending set (Set automatically handles duplicates)
|
|
219
|
+
pendingDialogs.add(dialogElement);
|
|
220
|
+
|
|
221
|
+
const doShow = () => showDialog(dialogElement, isModal, onError);
|
|
222
|
+
if (isExistingDialog) {
|
|
223
|
+
doShow();
|
|
224
|
+
} else {
|
|
225
|
+
setTimeout(doShow, 0);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Removes custom tracking attributes before rendering
|
|
231
|
+
* These attributes are only for internal use
|
|
232
|
+
*
|
|
233
|
+
* @param attributes - Attributes object to clean
|
|
234
|
+
* @returns Cleaned attributes
|
|
235
|
+
*/
|
|
236
|
+
export function cleanDialogAttributes(
|
|
237
|
+
attributes: { [key: string]: string }
|
|
238
|
+
): { [key: string]: string } {
|
|
239
|
+
const cleaned = { ...attributes };
|
|
240
|
+
delete cleaned[LayoutConstants.GXDialogModal];
|
|
241
|
+
return cleaned;
|
|
242
|
+
}
|
package/src/data.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Data, Layout } from "@gem-sdk/clarity-js";
|
|
2
|
+
import type { Data as DecodedData, Layout as DecodedLayout } from "@gem-sdk/clarity-decode";
|
|
3
|
+
import { PlaybackState, RegionState } from "@clarity-types/visualize";
|
|
4
|
+
|
|
5
|
+
export class DataHelper {
|
|
6
|
+
regionMap = {};
|
|
7
|
+
regions: { [key: string]: RegionState} = {};
|
|
8
|
+
metrics: {[key: number]: number} = {};
|
|
9
|
+
lean = false;
|
|
10
|
+
state: PlaybackState;
|
|
11
|
+
|
|
12
|
+
constructor(state: PlaybackState) {
|
|
13
|
+
this.state = state;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static METRIC_MAP = {
|
|
17
|
+
[Data.Metric.TotalBytes]: { name: "Total Bytes", unit: "KB" },
|
|
18
|
+
[Data.Metric.TotalCost]: { name: "Total Cost", unit: "ms" },
|
|
19
|
+
[Data.Metric.LayoutCost]: { name: "Layout Cost", unit: "ms" },
|
|
20
|
+
[Data.Metric.LargestPaint]: { name: "LCP", unit: "s" },
|
|
21
|
+
[Data.Metric.CumulativeLayoutShift]: { name: "CLS", unit: "cls" },
|
|
22
|
+
[Data.Metric.LongTaskCount]: { name: "Long Tasks" },
|
|
23
|
+
[Data.Metric.CartTotal]: { name: "Cart Total", unit: "html-price" },
|
|
24
|
+
[Data.Metric.ProductPrice]: { name: "Product Price", unit: "ld-price" },
|
|
25
|
+
[Data.Metric.ThreadBlockedTime]: { name: "Thread Blocked", unit: "ms" },
|
|
26
|
+
[Data.Dimension.InteractionNextPaint]: { name: "INP", unit: "ms" }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
public reset = (): void => {
|
|
30
|
+
this.metrics = {};
|
|
31
|
+
this.lean = false;
|
|
32
|
+
this.regions = {};
|
|
33
|
+
this.regionMap = {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public metric = (event: DecodedData.MetricEvent): void => {
|
|
37
|
+
if (this.state.options.metadata) {
|
|
38
|
+
let metricMarkup = [];
|
|
39
|
+
let regionMarkup = [];
|
|
40
|
+
// Copy over metrics for future reference
|
|
41
|
+
for (let m in event.data) {
|
|
42
|
+
const eventType = typeof event.data[m];
|
|
43
|
+
if (eventType === "number" || (event.event === Data.Event.Dimension && m === Data.Dimension.InteractionNextPaint.toString())) {
|
|
44
|
+
if (!(m in this.metrics)) { this.metrics[m] = 0; }
|
|
45
|
+
let key = parseInt(m, 10);
|
|
46
|
+
let value = eventType === "object" ? Number(event.data[m]?.[0]) : event.data[m];
|
|
47
|
+
if (m in DataHelper.METRIC_MAP && (DataHelper.METRIC_MAP[m].unit === "html-price" ||DataHelper.METRIC_MAP[m].unit === "ld-price")) {
|
|
48
|
+
this.metrics[m] = value;
|
|
49
|
+
} else { this.metrics[m] += value; }
|
|
50
|
+
this.lean = key === Data.Metric.Playback && value === 0 ? true : this.lean;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (let entry in this.metrics) {
|
|
55
|
+
if (entry in DataHelper.METRIC_MAP) {
|
|
56
|
+
let m = this.metrics[entry];
|
|
57
|
+
let map = DataHelper.METRIC_MAP[entry];
|
|
58
|
+
let unit = "unit" in map ? map.unit : Data.Constant.Empty;
|
|
59
|
+
metricMarkup.push(`<li><h2>${this.value(m, unit)}<span>${this.key(unit)}</span></h2>${map.name}</li>`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Append region information to metadata
|
|
64
|
+
for (let name in this.regions) {
|
|
65
|
+
let r = this.regions[name];
|
|
66
|
+
let className = (r.visibility === Layout.RegionVisibility.Visible ? "visible" : (r.interaction === Layout.InteractionState.Clicked ? "clicked" : Data.Constant.Empty));
|
|
67
|
+
regionMarkup.push(`<span class="${className}">${name}</span>`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.state.options.metadata.innerHTML = `<ul>${metricMarkup.join(Data.Constant.Empty)}</ul><div>${regionMarkup.join(Data.Constant.Empty)}</div>`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public region(event: DecodedLayout.RegionEvent): void {
|
|
75
|
+
let data = event.data;
|
|
76
|
+
for (let r of data) {
|
|
77
|
+
if (!(r.name in this.regions)) {
|
|
78
|
+
this.regions[r.name] = { interaction: r.interaction , visibility: r.visibility }
|
|
79
|
+
}
|
|
80
|
+
this.regionMap[r.id] = r.name;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private key = (unit: string): string => {
|
|
85
|
+
switch (unit) {
|
|
86
|
+
case "html-price":
|
|
87
|
+
case "ld-price":
|
|
88
|
+
case "cls":
|
|
89
|
+
return Data.Constant.Empty;
|
|
90
|
+
default: return unit;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private value = (num: number, unit: string): number => {
|
|
95
|
+
switch (unit) {
|
|
96
|
+
case "KB": return Math.round(num / 1024);
|
|
97
|
+
case "s": return Math.round(num / 10) / 100;
|
|
98
|
+
case "cls": return num / 1000;
|
|
99
|
+
case "html-price": return num / 100;
|
|
100
|
+
default: return num;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/enrich.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { helper, Layout } from "@gem-sdk/clarity-js";
|
|
2
|
+
import { Layout as DecodedLayout } from "@gem-sdk/clarity-decode";
|
|
3
|
+
import { NodeData } from "@clarity-types/visualize";
|
|
4
|
+
|
|
5
|
+
export class EnrichHelper {
|
|
6
|
+
|
|
7
|
+
children: { [key: number]: number[] };
|
|
8
|
+
nodes: { [key: number]: NodeData };
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.reset();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public reset = (): void => {
|
|
15
|
+
this.children = {};
|
|
16
|
+
this.nodes = {};
|
|
17
|
+
helper.selector.reset();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public selectors = (event: DecodedLayout.DomEvent): DecodedLayout.DomEvent => {
|
|
21
|
+
event.data.forEach && event.data.forEach(d => {
|
|
22
|
+
let parent = this.nodes[d.parent];
|
|
23
|
+
let children = this.children[d.parent] || [];
|
|
24
|
+
let node = this.nodes[d.id] || { tag: d.tag, parent: d.parent, previous: d.previous };
|
|
25
|
+
let attributes = d.attributes || {};
|
|
26
|
+
|
|
27
|
+
/* Track parent-child relationship for this element */
|
|
28
|
+
if (node.parent !== d.parent) {
|
|
29
|
+
let childIndex = d.previous === null ? 0 : children.indexOf(d.previous) + 1;
|
|
30
|
+
children.splice(childIndex, 0, d.id);
|
|
31
|
+
|
|
32
|
+
// Stop tracking this node from children of previous parent
|
|
33
|
+
if (node.parent !== d.parent) {
|
|
34
|
+
let exParent = this.children[node.parent];
|
|
35
|
+
let nodeIndex = exParent ? exParent.indexOf(d.id) : -1;
|
|
36
|
+
if (nodeIndex >= 0) {
|
|
37
|
+
this.children[node.parent].splice(nodeIndex, 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
node.parent = d.parent;
|
|
41
|
+
} else if (children.indexOf(d.id) < 0) { children.push(d.id); }
|
|
42
|
+
|
|
43
|
+
/* Get current position */
|
|
44
|
+
node.position = this.position(d.id, d.tag, node, children, children.map(c => this.nodes[c]));
|
|
45
|
+
|
|
46
|
+
/* For backward compatibility, continue populating current selector and hash like before in addition to beta selector and hash */
|
|
47
|
+
let input: Layout.SelectorInput = { id: d.id, tag: d.tag, prefix: parent ? [parent.alpha, parent.beta] : null, position: node.position, attributes };
|
|
48
|
+
|
|
49
|
+
// Get stable selector
|
|
50
|
+
// We intentionally use "null" value for empty selectors to keep parity with v0.6.25 and before.
|
|
51
|
+
let selectorAlpha = helper.selector.get(input, Layout.Selector.Alpha);
|
|
52
|
+
d.selectorAlpha = selectorAlpha.length > 0 ? selectorAlpha : null;
|
|
53
|
+
d.hashAlpha = selectorAlpha.length > 0 ? helper.hash(d.selectorAlpha) : null;
|
|
54
|
+
|
|
55
|
+
// Get beta selector
|
|
56
|
+
let selectorBeta = helper.selector.get(input, Layout.Selector.Beta);
|
|
57
|
+
d.selectorBeta = selectorBeta.length > 0 ? selectorBeta : null;
|
|
58
|
+
d.hashBeta = selectorBeta.length > 0 ? helper.hash(d.selectorBeta) : null;
|
|
59
|
+
|
|
60
|
+
/* Track state for future reference */
|
|
61
|
+
node.alpha = selectorAlpha;
|
|
62
|
+
node.beta = selectorBeta;
|
|
63
|
+
this.nodes[d.id] = node;
|
|
64
|
+
if (d.parent) { this.children[d.parent] = children; }
|
|
65
|
+
});
|
|
66
|
+
return event;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private position = (id: number, tag: string, child: NodeData, children: number[], siblings: NodeData[]): number => {
|
|
70
|
+
child.position = 1;
|
|
71
|
+
let idx = children ? children.indexOf(id) : -1;
|
|
72
|
+
while (idx-- > 0) {
|
|
73
|
+
if (tag === siblings[idx].tag) {
|
|
74
|
+
child.position = siblings[idx].position + 1;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return child.position;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/global.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as visualize from "@src/clarity";
|
|
2
|
+
|
|
3
|
+
// Expose clarity variable globally to allow access to public interface in a browser
|
|
4
|
+
if (typeof window !== "undefined") {
|
|
5
|
+
if ((window as any).clarity === undefined || (window as any).clarity === null) {
|
|
6
|
+
(window as any).clarity = {};
|
|
7
|
+
}
|
|
8
|
+
(window as any).clarity.visualize = visualize;
|
|
9
|
+
}
|