clarity-js 0.8.42 → 0.8.43
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/README.md +26 -26
- package/build/clarity.extended.js +1 -1
- package/build/clarity.insight.js +1 -1
- package/build/clarity.js +6043 -6030
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +6043 -6030
- package/build/clarity.performance.js +1 -1
- package/package.json +70 -70
- package/rollup.config.ts +161 -161
- package/src/clarity.ts +65 -65
- package/src/core/api.ts +8 -8
- package/src/core/config.ts +29 -29
- package/src/core/copy.ts +3 -3
- package/src/core/dynamic.ts +13 -7
- package/src/core/event.ts +53 -53
- package/src/core/hash.ts +19 -19
- package/src/core/history.ts +71 -71
- package/src/core/index.ts +81 -81
- package/src/core/measure.ts +19 -19
- package/src/core/report.ts +28 -28
- package/src/core/scrub.ts +204 -202
- package/src/core/task.ts +181 -181
- package/src/core/throttle.ts +46 -46
- package/src/core/time.ts +26 -26
- package/src/core/timeout.ts +10 -10
- package/src/core/version.ts +2 -2
- package/src/data/baseline.ts +162 -162
- package/src/data/compress.ts +31 -31
- package/src/data/consent.ts +77 -77
- package/src/data/custom.ts +23 -23
- package/src/data/dimension.ts +53 -53
- package/src/data/encode.ts +155 -155
- package/src/data/envelope.ts +53 -53
- package/src/data/extract.ts +211 -211
- package/src/data/index.ts +50 -50
- package/src/data/limit.ts +44 -44
- package/src/data/metadata.ts +411 -408
- package/src/data/metric.ts +51 -51
- package/src/data/ping.ts +36 -36
- package/src/data/signal.ts +30 -30
- package/src/data/summary.ts +34 -34
- package/src/data/token.ts +39 -39
- package/src/data/upgrade.ts +44 -44
- package/src/data/upload.ts +333 -333
- package/src/data/variable.ts +83 -83
- package/src/diagnostic/encode.ts +40 -40
- package/src/diagnostic/fraud.ts +36 -36
- package/src/diagnostic/index.ts +13 -13
- package/src/diagnostic/internal.ts +28 -28
- package/src/diagnostic/script.ts +35 -35
- package/src/dynamic/agent/blank.ts +2 -2
- package/src/dynamic/agent/crisp.ts +40 -40
- package/src/dynamic/agent/encode.ts +25 -25
- package/src/dynamic/agent/index.ts +8 -8
- package/src/dynamic/agent/livechat.ts +58 -58
- package/src/dynamic/agent/tidio.ts +44 -44
- package/src/global.ts +6 -6
- package/src/index.ts +9 -9
- package/src/insight/blank.ts +14 -14
- package/src/insight/encode.ts +60 -60
- package/src/insight/snapshot.ts +114 -114
- package/src/interaction/change.ts +38 -38
- package/src/interaction/click.ts +173 -173
- package/src/interaction/clipboard.ts +32 -32
- package/src/interaction/encode.ts +210 -210
- package/src/interaction/index.ts +60 -60
- package/src/interaction/input.ts +57 -57
- package/src/interaction/pointer.ts +137 -137
- package/src/interaction/resize.ts +50 -50
- package/src/interaction/scroll.ts +129 -129
- package/src/interaction/selection.ts +66 -66
- package/src/interaction/submit.ts +30 -30
- package/src/interaction/timeline.ts +69 -69
- package/src/interaction/unload.ts +26 -26
- package/src/interaction/visibility.ts +27 -27
- package/src/layout/animation.ts +133 -133
- package/src/layout/custom.ts +42 -42
- package/src/layout/discover.ts +31 -31
- package/src/layout/document.ts +46 -46
- package/src/layout/dom.ts +439 -439
- package/src/layout/encode.ts +154 -154
- package/src/layout/index.ts +42 -42
- package/src/layout/mutation.ts +411 -411
- package/src/layout/node.ts +294 -294
- package/src/layout/offset.ts +19 -19
- package/src/layout/region.ts +151 -151
- package/src/layout/schema.ts +63 -63
- package/src/layout/selector.ts +82 -82
- package/src/layout/style.ts +159 -159
- package/src/layout/target.ts +32 -32
- package/src/layout/traverse.ts +27 -27
- package/src/performance/blank.ts +9 -9
- package/src/performance/encode.ts +31 -31
- package/src/performance/index.ts +12 -12
- package/src/performance/interaction.ts +125 -125
- package/src/performance/navigation.ts +31 -31
- package/src/performance/observer.ts +112 -112
- package/src/queue.ts +33 -33
- package/test/core.test.ts +139 -139
- package/test/helper.ts +162 -162
- package/test/html/core.html +27 -27
- package/test/stub.test.ts +7 -7
- package/test/tsconfig.test.json +5 -5
- package/tsconfig.json +21 -21
- package/tslint.json +32 -32
- package/types/agent.d.ts +39 -39
- package/types/core.d.ts +150 -150
- package/types/data.d.ts +572 -571
- package/types/diagnostic.d.ts +24 -24
- package/types/global.d.ts +30 -30
- package/types/index.d.ts +40 -40
- package/types/interaction.d.ts +177 -177
- package/types/layout.d.ts +276 -276
- package/types/performance.d.ts +31 -31
package/src/layout/region.ts
CHANGED
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
import { Event, Setting } from "@clarity-types/data";
|
|
2
|
-
import { InteractionState, RegionData, RegionState, RegionQueue, RegionVisibility } from "@clarity-types/layout";
|
|
3
|
-
import { time } from "@src/core/time";
|
|
4
|
-
import * as dom from "@src/layout/dom";
|
|
5
|
-
import encode from "@src/layout/encode";
|
|
6
|
-
|
|
7
|
-
export let state: RegionState[] = [];
|
|
8
|
-
let regionMap: WeakMap<Node, string> = null; // Maps region nodes => region name
|
|
9
|
-
let regions: { [key: number]: RegionData } = {};
|
|
10
|
-
let queue: RegionQueue[] = [];
|
|
11
|
-
let watch = false;
|
|
12
|
-
let observer: IntersectionObserver = null;
|
|
13
|
-
|
|
14
|
-
export function start(): void {
|
|
15
|
-
reset();
|
|
16
|
-
observer = null;
|
|
17
|
-
regionMap = new WeakMap();
|
|
18
|
-
regions = {};
|
|
19
|
-
queue = [];
|
|
20
|
-
watch = window["IntersectionObserver"] ? true : false;
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function observe(node: Node, name: string): void {
|
|
25
|
-
if (regionMap.has(node) === false) {
|
|
26
|
-
regionMap.set(node, name);
|
|
27
|
-
observer = observer === null && watch ? new IntersectionObserver(handler, {
|
|
28
|
-
// Get notified as intersection continues to change
|
|
29
|
-
// This allows us to process regions that get partially hidden during the lifetime of the page
|
|
30
|
-
// See: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#creating_an_intersection_observer
|
|
31
|
-
// By default, intersection observers only fire an event when even a single pixel is visible and not thereafter.
|
|
32
|
-
threshold: [0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
|
|
33
|
-
}) : observer;
|
|
34
|
-
if (observer && node && node.nodeType === Node.ELEMENT_NODE) {
|
|
35
|
-
observer.observe(node as Element);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function exists(node: Node): boolean {
|
|
41
|
-
// Check if regionMap is not null before looking up a node
|
|
42
|
-
// Since, dom module stops after region module, it's possible that we may set regionMap to be null
|
|
43
|
-
// and still attempt to call exists on a late coming DOM mutation (or addition), effectively causing a script error
|
|
44
|
-
return regionMap && regionMap.has(node);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function track(id: number, event: Event): void {
|
|
48
|
-
let node = dom.getNode(id);
|
|
49
|
-
let data = id in regions ? regions[id] : { id, visibility: RegionVisibility.Rendered, interaction: InteractionState.None, name: regionMap.get(node) };
|
|
50
|
-
|
|
51
|
-
// Determine the interaction state based on incoming event
|
|
52
|
-
let interaction = InteractionState.None;
|
|
53
|
-
switch (event) {
|
|
54
|
-
case Event.Click: interaction = InteractionState.Clicked; break;
|
|
55
|
-
case Event.Input: interaction = InteractionState.Input; break;
|
|
56
|
-
}
|
|
57
|
-
// Process updates to this region, if applicable
|
|
58
|
-
process(node, data, interaction, data.visibility);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function compute(): void {
|
|
62
|
-
// Process any regions where we couldn't resolve an "id" for at the time of last intersection observer event
|
|
63
|
-
// This could happen in cases where elements are not yet processed by Clarity's virtual DOM but browser reports a change, regardless.
|
|
64
|
-
// For those cases we add them to the queue and re-process them below
|
|
65
|
-
let q = [];
|
|
66
|
-
for (let r of queue) {
|
|
67
|
-
let id = dom.getId(r.node);
|
|
68
|
-
if (id) {
|
|
69
|
-
r.state.data.id = id;
|
|
70
|
-
regions[id] = r.state.data;
|
|
71
|
-
state.push(r.state);
|
|
72
|
-
} else { q.push(r); }
|
|
73
|
-
}
|
|
74
|
-
queue = q;
|
|
75
|
-
|
|
76
|
-
// Schedule encode only when we have at least one valid data entry
|
|
77
|
-
if (state.length > 0) { encode(Event.Region); }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function handler(entries: IntersectionObserverEntry[]): void {
|
|
81
|
-
for (let entry of entries) {
|
|
82
|
-
let target = entry.target;
|
|
83
|
-
let rect = entry.boundingClientRect;
|
|
84
|
-
let overlap = entry.intersectionRect;
|
|
85
|
-
let viewport = entry.rootBounds;
|
|
86
|
-
// Only capture regions that have non-zero width or height to avoid tracking and sending regions
|
|
87
|
-
// that cannot ever be seen by the user. In some cases, websites will have a multiple copy of the same region
|
|
88
|
-
// like search box - one for desktop, and another for mobile. In those cases, CSS media queries determine which one should be visible.
|
|
89
|
-
// Also, if these regions ever become non-zero width or height (through AJAX, user action or orientation change) - we will automatically start monitoring them from that point onwards
|
|
90
|
-
if (regionMap.has(target) && rect.width + rect.height > 0 && viewport && viewport.width > 0 && viewport.height > 0) {
|
|
91
|
-
let id = target ? dom.getId(target) : null;
|
|
92
|
-
let data = id in regions ? regions[id] : { id, name: regionMap.get(target), interaction: InteractionState.None, visibility: RegionVisibility.Rendered };
|
|
93
|
-
|
|
94
|
-
// For regions that have relatively smaller area, we look at intersection ratio and see the overlap relative to element's area
|
|
95
|
-
// However, for larger regions, area of regions could be bigger than viewport and therefore comparison is relative to visible area
|
|
96
|
-
let viewportRatio = overlap ? (overlap.width * overlap.height * 1.0) / (viewport.width * viewport.height) : 0;
|
|
97
|
-
let visible = viewportRatio > Setting.ViewportIntersectionRatio || entry.intersectionRatio > Setting.IntersectionRatio;
|
|
98
|
-
// If an element is either visible or was visible and has been scrolled to the end
|
|
99
|
-
// i.e. Scrolled to end is determined by if the starting position of the element + the window height is more than the total element height.
|
|
100
|
-
// starting position is relative to the viewport - so Intersection observer returns a negative value for rect.top to indicate that the element top is above the viewport
|
|
101
|
-
let scrolledToEnd = (visible || data.visibility == RegionVisibility.Visible) && Math.abs(rect.top) + viewport.height > rect.height;
|
|
102
|
-
// Process updates to this region, if applicable
|
|
103
|
-
process(target, data, data.interaction,
|
|
104
|
-
(scrolledToEnd ?
|
|
105
|
-
RegionVisibility.ScrolledToEnd :
|
|
106
|
-
(visible ? RegionVisibility.Visible : RegionVisibility.Rendered)));
|
|
107
|
-
|
|
108
|
-
// Stop observing this element now that we have already received scrolled signal
|
|
109
|
-
if (data.visibility >= RegionVisibility.ScrolledToEnd && observer) { observer.unobserve(target); }
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (state.length > 0) { encode(Event.Region); }
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function process(n: Node, d: RegionData, s: InteractionState, v: RegionVisibility): void {
|
|
116
|
-
// Check if received a state that supersedes existing state
|
|
117
|
-
let updated = s > d.interaction || v > d.visibility;
|
|
118
|
-
d.interaction = s > d.interaction ? s : d.interaction;
|
|
119
|
-
d.visibility = v > d.visibility ? v : d.visibility;
|
|
120
|
-
// If the corresponding node is already discovered, update the internal state
|
|
121
|
-
// Otherwise, track it in a queue to reprocess later.
|
|
122
|
-
if (d.id) {
|
|
123
|
-
if ((d.id in regions && updated) || !(d.id in regions)) {
|
|
124
|
-
regions[d.id] = d;
|
|
125
|
-
state.push(clone(d));
|
|
126
|
-
}
|
|
127
|
-
} else {
|
|
128
|
-
// Get the time before adding to queue to ensure accurate event time
|
|
129
|
-
queue.push({node: n, state: clone(d)});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function clone(r: RegionData): RegionState {
|
|
134
|
-
return { time: time(), data: { id: r.id, interaction: r.interaction, visibility: r.visibility, name: r.name }};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function reset(): void {
|
|
138
|
-
state = [];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function stop(): void {
|
|
142
|
-
reset();
|
|
143
|
-
regionMap = null;
|
|
144
|
-
regions = {};
|
|
145
|
-
queue = [];
|
|
146
|
-
if (observer) {
|
|
147
|
-
observer.disconnect();
|
|
148
|
-
observer = null;
|
|
149
|
-
}
|
|
150
|
-
watch = false;
|
|
151
|
-
}
|
|
1
|
+
import { Event, Setting } from "@clarity-types/data";
|
|
2
|
+
import { InteractionState, RegionData, RegionState, RegionQueue, RegionVisibility } from "@clarity-types/layout";
|
|
3
|
+
import { time } from "@src/core/time";
|
|
4
|
+
import * as dom from "@src/layout/dom";
|
|
5
|
+
import encode from "@src/layout/encode";
|
|
6
|
+
|
|
7
|
+
export let state: RegionState[] = [];
|
|
8
|
+
let regionMap: WeakMap<Node, string> = null; // Maps region nodes => region name
|
|
9
|
+
let regions: { [key: number]: RegionData } = {};
|
|
10
|
+
let queue: RegionQueue[] = [];
|
|
11
|
+
let watch = false;
|
|
12
|
+
let observer: IntersectionObserver = null;
|
|
13
|
+
|
|
14
|
+
export function start(): void {
|
|
15
|
+
reset();
|
|
16
|
+
observer = null;
|
|
17
|
+
regionMap = new WeakMap();
|
|
18
|
+
regions = {};
|
|
19
|
+
queue = [];
|
|
20
|
+
watch = window["IntersectionObserver"] ? true : false;
|
|
21
|
+
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function observe(node: Node, name: string): void {
|
|
25
|
+
if (regionMap.has(node) === false) {
|
|
26
|
+
regionMap.set(node, name);
|
|
27
|
+
observer = observer === null && watch ? new IntersectionObserver(handler, {
|
|
28
|
+
// Get notified as intersection continues to change
|
|
29
|
+
// This allows us to process regions that get partially hidden during the lifetime of the page
|
|
30
|
+
// See: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#creating_an_intersection_observer
|
|
31
|
+
// By default, intersection observers only fire an event when even a single pixel is visible and not thereafter.
|
|
32
|
+
threshold: [0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
|
|
33
|
+
}) : observer;
|
|
34
|
+
if (observer && node && node.nodeType === Node.ELEMENT_NODE) {
|
|
35
|
+
observer.observe(node as Element);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function exists(node: Node): boolean {
|
|
41
|
+
// Check if regionMap is not null before looking up a node
|
|
42
|
+
// Since, dom module stops after region module, it's possible that we may set regionMap to be null
|
|
43
|
+
// and still attempt to call exists on a late coming DOM mutation (or addition), effectively causing a script error
|
|
44
|
+
return regionMap && regionMap.has(node);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function track(id: number, event: Event): void {
|
|
48
|
+
let node = dom.getNode(id);
|
|
49
|
+
let data = id in regions ? regions[id] : { id, visibility: RegionVisibility.Rendered, interaction: InteractionState.None, name: regionMap.get(node) };
|
|
50
|
+
|
|
51
|
+
// Determine the interaction state based on incoming event
|
|
52
|
+
let interaction = InteractionState.None;
|
|
53
|
+
switch (event) {
|
|
54
|
+
case Event.Click: interaction = InteractionState.Clicked; break;
|
|
55
|
+
case Event.Input: interaction = InteractionState.Input; break;
|
|
56
|
+
}
|
|
57
|
+
// Process updates to this region, if applicable
|
|
58
|
+
process(node, data, interaction, data.visibility);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function compute(): void {
|
|
62
|
+
// Process any regions where we couldn't resolve an "id" for at the time of last intersection observer event
|
|
63
|
+
// This could happen in cases where elements are not yet processed by Clarity's virtual DOM but browser reports a change, regardless.
|
|
64
|
+
// For those cases we add them to the queue and re-process them below
|
|
65
|
+
let q = [];
|
|
66
|
+
for (let r of queue) {
|
|
67
|
+
let id = dom.getId(r.node);
|
|
68
|
+
if (id) {
|
|
69
|
+
r.state.data.id = id;
|
|
70
|
+
regions[id] = r.state.data;
|
|
71
|
+
state.push(r.state);
|
|
72
|
+
} else { q.push(r); }
|
|
73
|
+
}
|
|
74
|
+
queue = q;
|
|
75
|
+
|
|
76
|
+
// Schedule encode only when we have at least one valid data entry
|
|
77
|
+
if (state.length > 0) { encode(Event.Region); }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handler(entries: IntersectionObserverEntry[]): void {
|
|
81
|
+
for (let entry of entries) {
|
|
82
|
+
let target = entry.target;
|
|
83
|
+
let rect = entry.boundingClientRect;
|
|
84
|
+
let overlap = entry.intersectionRect;
|
|
85
|
+
let viewport = entry.rootBounds;
|
|
86
|
+
// Only capture regions that have non-zero width or height to avoid tracking and sending regions
|
|
87
|
+
// that cannot ever be seen by the user. In some cases, websites will have a multiple copy of the same region
|
|
88
|
+
// like search box - one for desktop, and another for mobile. In those cases, CSS media queries determine which one should be visible.
|
|
89
|
+
// Also, if these regions ever become non-zero width or height (through AJAX, user action or orientation change) - we will automatically start monitoring them from that point onwards
|
|
90
|
+
if (regionMap.has(target) && rect.width + rect.height > 0 && viewport && viewport.width > 0 && viewport.height > 0) {
|
|
91
|
+
let id = target ? dom.getId(target) : null;
|
|
92
|
+
let data = id in regions ? regions[id] : { id, name: regionMap.get(target), interaction: InteractionState.None, visibility: RegionVisibility.Rendered };
|
|
93
|
+
|
|
94
|
+
// For regions that have relatively smaller area, we look at intersection ratio and see the overlap relative to element's area
|
|
95
|
+
// However, for larger regions, area of regions could be bigger than viewport and therefore comparison is relative to visible area
|
|
96
|
+
let viewportRatio = overlap ? (overlap.width * overlap.height * 1.0) / (viewport.width * viewport.height) : 0;
|
|
97
|
+
let visible = viewportRatio > Setting.ViewportIntersectionRatio || entry.intersectionRatio > Setting.IntersectionRatio;
|
|
98
|
+
// If an element is either visible or was visible and has been scrolled to the end
|
|
99
|
+
// i.e. Scrolled to end is determined by if the starting position of the element + the window height is more than the total element height.
|
|
100
|
+
// starting position is relative to the viewport - so Intersection observer returns a negative value for rect.top to indicate that the element top is above the viewport
|
|
101
|
+
let scrolledToEnd = (visible || data.visibility == RegionVisibility.Visible) && Math.abs(rect.top) + viewport.height > rect.height;
|
|
102
|
+
// Process updates to this region, if applicable
|
|
103
|
+
process(target, data, data.interaction,
|
|
104
|
+
(scrolledToEnd ?
|
|
105
|
+
RegionVisibility.ScrolledToEnd :
|
|
106
|
+
(visible ? RegionVisibility.Visible : RegionVisibility.Rendered)));
|
|
107
|
+
|
|
108
|
+
// Stop observing this element now that we have already received scrolled signal
|
|
109
|
+
if (data.visibility >= RegionVisibility.ScrolledToEnd && observer) { observer.unobserve(target); }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (state.length > 0) { encode(Event.Region); }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function process(n: Node, d: RegionData, s: InteractionState, v: RegionVisibility): void {
|
|
116
|
+
// Check if received a state that supersedes existing state
|
|
117
|
+
let updated = s > d.interaction || v > d.visibility;
|
|
118
|
+
d.interaction = s > d.interaction ? s : d.interaction;
|
|
119
|
+
d.visibility = v > d.visibility ? v : d.visibility;
|
|
120
|
+
// If the corresponding node is already discovered, update the internal state
|
|
121
|
+
// Otherwise, track it in a queue to reprocess later.
|
|
122
|
+
if (d.id) {
|
|
123
|
+
if ((d.id in regions && updated) || !(d.id in regions)) {
|
|
124
|
+
regions[d.id] = d;
|
|
125
|
+
state.push(clone(d));
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// Get the time before adding to queue to ensure accurate event time
|
|
129
|
+
queue.push({node: n, state: clone(d)});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function clone(r: RegionData): RegionState {
|
|
134
|
+
return { time: time(), data: { id: r.id, interaction: r.interaction, visibility: r.visibility, name: r.name }};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function reset(): void {
|
|
138
|
+
state = [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function stop(): void {
|
|
142
|
+
reset();
|
|
143
|
+
regionMap = null;
|
|
144
|
+
regions = {};
|
|
145
|
+
queue = [];
|
|
146
|
+
if (observer) {
|
|
147
|
+
observer.disconnect();
|
|
148
|
+
observer = null;
|
|
149
|
+
}
|
|
150
|
+
watch = false;
|
|
151
|
+
}
|
package/src/layout/schema.ts
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
import { Dimension, Metric, Setting } from "@clarity-types/data";
|
|
2
|
-
import { Constant, JsonLD } from "@clarity-types/layout";
|
|
3
|
-
import * as dimension from "@src/data/dimension";
|
|
4
|
-
import * as metric from "@src/data/metric";
|
|
5
|
-
|
|
6
|
-
const digitsRegex = /[^0-9\.]/g;
|
|
7
|
-
|
|
8
|
-
/* JSON+LD (Linked Data) Recursive Parser */
|
|
9
|
-
export function ld(json: any): void {
|
|
10
|
-
for (let key of Object.keys(json)) {
|
|
11
|
-
let value = json[key];
|
|
12
|
-
if (key === JsonLD.Type && typeof value === "string") {
|
|
13
|
-
value = value.toLowerCase();
|
|
14
|
-
/* Normalizations */
|
|
15
|
-
value = value.indexOf(JsonLD.Article) >= 0 || value.indexOf(JsonLD.Posting) >= 0 ? JsonLD.Article : value;
|
|
16
|
-
switch (value) {
|
|
17
|
-
case JsonLD.Article:
|
|
18
|
-
case JsonLD.Recipe:
|
|
19
|
-
dimension.log(Dimension.SchemaType, json[key]);
|
|
20
|
-
dimension.log(Dimension.AuthorName, json[JsonLD.Creator]);
|
|
21
|
-
dimension.log(Dimension.Headline, json[JsonLD.Headline]);
|
|
22
|
-
break;
|
|
23
|
-
case JsonLD.Product:
|
|
24
|
-
dimension.log(Dimension.SchemaType, json[key]);
|
|
25
|
-
dimension.log(Dimension.ProductName, json[JsonLD.Name]);
|
|
26
|
-
dimension.log(Dimension.ProductSku, json[JsonLD.Sku]);
|
|
27
|
-
if (json[JsonLD.Brand]) { dimension.log(Dimension.ProductBrand, json[JsonLD.Brand][JsonLD.Name]); }
|
|
28
|
-
break;
|
|
29
|
-
case JsonLD.AggregateRating:
|
|
30
|
-
if (json[JsonLD.RatingValue]) {
|
|
31
|
-
metric.max(Metric.RatingValue, num(json[JsonLD.RatingValue], Setting.RatingScale));
|
|
32
|
-
metric.max(Metric.BestRating, num(json[JsonLD.BestRating]));
|
|
33
|
-
metric.max(Metric.WorstRating, num(json[JsonLD.WorstRating]));
|
|
34
|
-
}
|
|
35
|
-
metric.max(Metric.RatingCount, num(json[JsonLD.RatingCount]));
|
|
36
|
-
metric.max(Metric.ReviewCount, num(json[JsonLD.ReviewCount]));
|
|
37
|
-
break;
|
|
38
|
-
case JsonLD.Offer:
|
|
39
|
-
dimension.log(Dimension.ProductAvailability, json[JsonLD.Availability]);
|
|
40
|
-
dimension.log(Dimension.ProductCondition, json[JsonLD.ItemCondition]);
|
|
41
|
-
dimension.log(Dimension.ProductCurrency, json[JsonLD.PriceCurrency]);
|
|
42
|
-
dimension.log(Dimension.ProductSku, json[JsonLD.Sku]);
|
|
43
|
-
metric.max(Metric.ProductPrice, num(json[JsonLD.Price]));
|
|
44
|
-
break;
|
|
45
|
-
case JsonLD.Brand:
|
|
46
|
-
dimension.log(Dimension.ProductBrand, json[JsonLD.Name]);
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
// Continue parsing nested objects
|
|
51
|
-
if (value !== null && typeof(value) === Constant.Object) { ld(value); }
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function num(input: string | number, scale: number = 1): number {
|
|
56
|
-
if (input !== null) {
|
|
57
|
-
switch (typeof input) {
|
|
58
|
-
case Constant.Number: return Math.round((input as number) * scale);
|
|
59
|
-
case Constant.String: return Math.round(parseFloat((input as string).replace(digitsRegex, Constant.Empty)) * scale);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
1
|
+
import { Dimension, Metric, Setting } from "@clarity-types/data";
|
|
2
|
+
import { Constant, JsonLD } from "@clarity-types/layout";
|
|
3
|
+
import * as dimension from "@src/data/dimension";
|
|
4
|
+
import * as metric from "@src/data/metric";
|
|
5
|
+
|
|
6
|
+
const digitsRegex = /[^0-9\.]/g;
|
|
7
|
+
|
|
8
|
+
/* JSON+LD (Linked Data) Recursive Parser */
|
|
9
|
+
export function ld(json: any): void {
|
|
10
|
+
for (let key of Object.keys(json)) {
|
|
11
|
+
let value = json[key];
|
|
12
|
+
if (key === JsonLD.Type && typeof value === "string") {
|
|
13
|
+
value = value.toLowerCase();
|
|
14
|
+
/* Normalizations */
|
|
15
|
+
value = value.indexOf(JsonLD.Article) >= 0 || value.indexOf(JsonLD.Posting) >= 0 ? JsonLD.Article : value;
|
|
16
|
+
switch (value) {
|
|
17
|
+
case JsonLD.Article:
|
|
18
|
+
case JsonLD.Recipe:
|
|
19
|
+
dimension.log(Dimension.SchemaType, json[key]);
|
|
20
|
+
dimension.log(Dimension.AuthorName, json[JsonLD.Creator]);
|
|
21
|
+
dimension.log(Dimension.Headline, json[JsonLD.Headline]);
|
|
22
|
+
break;
|
|
23
|
+
case JsonLD.Product:
|
|
24
|
+
dimension.log(Dimension.SchemaType, json[key]);
|
|
25
|
+
dimension.log(Dimension.ProductName, json[JsonLD.Name]);
|
|
26
|
+
dimension.log(Dimension.ProductSku, json[JsonLD.Sku]);
|
|
27
|
+
if (json[JsonLD.Brand]) { dimension.log(Dimension.ProductBrand, json[JsonLD.Brand][JsonLD.Name]); }
|
|
28
|
+
break;
|
|
29
|
+
case JsonLD.AggregateRating:
|
|
30
|
+
if (json[JsonLD.RatingValue]) {
|
|
31
|
+
metric.max(Metric.RatingValue, num(json[JsonLD.RatingValue], Setting.RatingScale));
|
|
32
|
+
metric.max(Metric.BestRating, num(json[JsonLD.BestRating]));
|
|
33
|
+
metric.max(Metric.WorstRating, num(json[JsonLD.WorstRating]));
|
|
34
|
+
}
|
|
35
|
+
metric.max(Metric.RatingCount, num(json[JsonLD.RatingCount]));
|
|
36
|
+
metric.max(Metric.ReviewCount, num(json[JsonLD.ReviewCount]));
|
|
37
|
+
break;
|
|
38
|
+
case JsonLD.Offer:
|
|
39
|
+
dimension.log(Dimension.ProductAvailability, json[JsonLD.Availability]);
|
|
40
|
+
dimension.log(Dimension.ProductCondition, json[JsonLD.ItemCondition]);
|
|
41
|
+
dimension.log(Dimension.ProductCurrency, json[JsonLD.PriceCurrency]);
|
|
42
|
+
dimension.log(Dimension.ProductSku, json[JsonLD.Sku]);
|
|
43
|
+
metric.max(Metric.ProductPrice, num(json[JsonLD.Price]));
|
|
44
|
+
break;
|
|
45
|
+
case JsonLD.Brand:
|
|
46
|
+
dimension.log(Dimension.ProductBrand, json[JsonLD.Name]);
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Continue parsing nested objects
|
|
51
|
+
if (value !== null && typeof(value) === Constant.Object) { ld(value); }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function num(input: string | number, scale: number = 1): number {
|
|
56
|
+
if (input !== null) {
|
|
57
|
+
switch (typeof input) {
|
|
58
|
+
case Constant.Number: return Math.round((input as number) * scale);
|
|
59
|
+
case Constant.String: return Math.round(parseFloat((input as string).replace(digitsRegex, Constant.Empty)) * scale);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
package/src/layout/selector.ts
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
import { Character } from "../../types/data";
|
|
2
|
-
import { Constant, Selector, SelectorInput } from "../../types/layout";
|
|
3
|
-
|
|
4
|
-
const excludeClassNames = Constant.ExcludeClassNames.split(Constant.Comma);
|
|
5
|
-
let selectorMap: { [selector: string]: number[] } = {};
|
|
6
|
-
|
|
7
|
-
export function reset(): void {
|
|
8
|
-
selectorMap = {};
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function get(input: SelectorInput, type: Selector): string {
|
|
12
|
-
let a = input.attributes;
|
|
13
|
-
let prefix = input.prefix ? input.prefix[type] : null;
|
|
14
|
-
let suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position-1}` : `:nth-of-type(${input.position})`;
|
|
15
|
-
switch (input.tag) {
|
|
16
|
-
case "STYLE":
|
|
17
|
-
case "TITLE":
|
|
18
|
-
case "LINK":
|
|
19
|
-
case "META":
|
|
20
|
-
case Constant.TextTag:
|
|
21
|
-
case Constant.DocumentTag:
|
|
22
|
-
return Constant.Empty;
|
|
23
|
-
case "HTML":
|
|
24
|
-
return Constant.HTML;
|
|
25
|
-
default:
|
|
26
|
-
if (prefix === null) { return Constant.Empty; }
|
|
27
|
-
prefix = `${prefix}${Constant.Separator}`;
|
|
28
|
-
input.tag = input.tag.indexOf(Constant.SvgPrefix) === 0 ? input.tag.substr(Constant.SvgPrefix.length) : input.tag;
|
|
29
|
-
let selector = `${prefix}${input.tag}${suffix}`;
|
|
30
|
-
let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
|
|
31
|
-
let classes = input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class].trim().split(/\s+/).filter(c => filter(c)).join(Constant.Period) : null;
|
|
32
|
-
if (classes && classes.length > 0) {
|
|
33
|
-
if (type === Selector.Alpha) {
|
|
34
|
-
// In Alpha mode, update selector to use class names, with relative positioning within the parent id container.
|
|
35
|
-
// If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple.
|
|
36
|
-
let key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`;
|
|
37
|
-
if (!(key in selectorMap)) { selectorMap[key] = []; }
|
|
38
|
-
if (selectorMap[key].indexOf(input.id) < 0) { selectorMap[key].push(input.id); }
|
|
39
|
-
selector = `${key}${Constant.Tilde}${selectorMap[key].indexOf(input.id)}`;
|
|
40
|
-
} else {
|
|
41
|
-
// In Beta mode, we continue to look at query selectors in context of the full page
|
|
42
|
-
selector = `${prefix}${input.tag}.${classes}${suffix}`
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
// Update selector to use "id" field when available. There are two exceptions:
|
|
46
|
-
// (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
|
|
47
|
-
// (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
|
|
48
|
-
selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector;
|
|
49
|
-
return selector;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function getDomPrefix(prefix: string): string {
|
|
54
|
-
const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag);
|
|
55
|
-
const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`);
|
|
56
|
-
const domStart = Math.max(shadowDomStart, iframeDomStart);
|
|
57
|
-
|
|
58
|
-
if (domStart < 0) { return Constant.Empty; }
|
|
59
|
-
|
|
60
|
-
return prefix.substring(0, prefix.indexOf(Constant.Separator, domStart) + 1);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getDomPath(input: string): string {
|
|
64
|
-
let parts = input.split(Constant.Separator);
|
|
65
|
-
for (let i = 0; i < parts.length; i++) {
|
|
66
|
-
let tIndex = parts[i].indexOf(Constant.Tilde);
|
|
67
|
-
let dIndex = parts[i].indexOf(Constant.Dot);
|
|
68
|
-
parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : (tIndex > 0 ? tIndex : parts[i].length));
|
|
69
|
-
}
|
|
70
|
-
return parts.join(Constant.Separator);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Check if the given input string has digits or excluded class names
|
|
74
|
-
function filter(value: string): boolean {
|
|
75
|
-
if (!value) { return false; } // Do not process empty strings
|
|
76
|
-
if (excludeClassNames.some(x => value.toLowerCase().indexOf(x) >= 0)) { return false; }
|
|
77
|
-
for (let i = 0; i < value.length; i++) {
|
|
78
|
-
let c = value.charCodeAt(i);
|
|
79
|
-
if (c >= Character.Zero && c <= Character.Nine) { return false };
|
|
80
|
-
}
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
1
|
+
import { Character } from "../../types/data";
|
|
2
|
+
import { Constant, Selector, SelectorInput } from "../../types/layout";
|
|
3
|
+
|
|
4
|
+
const excludeClassNames = Constant.ExcludeClassNames.split(Constant.Comma);
|
|
5
|
+
let selectorMap: { [selector: string]: number[] } = {};
|
|
6
|
+
|
|
7
|
+
export function reset(): void {
|
|
8
|
+
selectorMap = {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function get(input: SelectorInput, type: Selector): string {
|
|
12
|
+
let a = input.attributes;
|
|
13
|
+
let prefix = input.prefix ? input.prefix[type] : null;
|
|
14
|
+
let suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position-1}` : `:nth-of-type(${input.position})`;
|
|
15
|
+
switch (input.tag) {
|
|
16
|
+
case "STYLE":
|
|
17
|
+
case "TITLE":
|
|
18
|
+
case "LINK":
|
|
19
|
+
case "META":
|
|
20
|
+
case Constant.TextTag:
|
|
21
|
+
case Constant.DocumentTag:
|
|
22
|
+
return Constant.Empty;
|
|
23
|
+
case "HTML":
|
|
24
|
+
return Constant.HTML;
|
|
25
|
+
default:
|
|
26
|
+
if (prefix === null) { return Constant.Empty; }
|
|
27
|
+
prefix = `${prefix}${Constant.Separator}`;
|
|
28
|
+
input.tag = input.tag.indexOf(Constant.SvgPrefix) === 0 ? input.tag.substr(Constant.SvgPrefix.length) : input.tag;
|
|
29
|
+
let selector = `${prefix}${input.tag}${suffix}`;
|
|
30
|
+
let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
|
|
31
|
+
let classes = input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class].trim().split(/\s+/).filter(c => filter(c)).join(Constant.Period) : null;
|
|
32
|
+
if (classes && classes.length > 0) {
|
|
33
|
+
if (type === Selector.Alpha) {
|
|
34
|
+
// In Alpha mode, update selector to use class names, with relative positioning within the parent id container.
|
|
35
|
+
// If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple.
|
|
36
|
+
let key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`;
|
|
37
|
+
if (!(key in selectorMap)) { selectorMap[key] = []; }
|
|
38
|
+
if (selectorMap[key].indexOf(input.id) < 0) { selectorMap[key].push(input.id); }
|
|
39
|
+
selector = `${key}${Constant.Tilde}${selectorMap[key].indexOf(input.id)}`;
|
|
40
|
+
} else {
|
|
41
|
+
// In Beta mode, we continue to look at query selectors in context of the full page
|
|
42
|
+
selector = `${prefix}${input.tag}.${classes}${suffix}`
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Update selector to use "id" field when available. There are two exceptions:
|
|
46
|
+
// (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
|
|
47
|
+
// (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
|
|
48
|
+
selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector;
|
|
49
|
+
return selector;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getDomPrefix(prefix: string): string {
|
|
54
|
+
const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag);
|
|
55
|
+
const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`);
|
|
56
|
+
const domStart = Math.max(shadowDomStart, iframeDomStart);
|
|
57
|
+
|
|
58
|
+
if (domStart < 0) { return Constant.Empty; }
|
|
59
|
+
|
|
60
|
+
return prefix.substring(0, prefix.indexOf(Constant.Separator, domStart) + 1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getDomPath(input: string): string {
|
|
64
|
+
let parts = input.split(Constant.Separator);
|
|
65
|
+
for (let i = 0; i < parts.length; i++) {
|
|
66
|
+
let tIndex = parts[i].indexOf(Constant.Tilde);
|
|
67
|
+
let dIndex = parts[i].indexOf(Constant.Dot);
|
|
68
|
+
parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : (tIndex > 0 ? tIndex : parts[i].length));
|
|
69
|
+
}
|
|
70
|
+
return parts.join(Constant.Separator);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check if the given input string has digits or excluded class names
|
|
74
|
+
function filter(value: string): boolean {
|
|
75
|
+
if (!value) { return false; } // Do not process empty strings
|
|
76
|
+
if (excludeClassNames.some(x => value.toLowerCase().indexOf(x) >= 0)) { return false; }
|
|
77
|
+
for (let i = 0; i < value.length; i++) {
|
|
78
|
+
let c = value.charCodeAt(i);
|
|
79
|
+
if (c >= Character.Zero && c <= Character.Nine) { return false };
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|