-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathbrowser.ts
152 lines (136 loc) · 4.39 KB
/
browser.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import { isString } from './is';
import { GLOBAL_OBJ } from './worldwide';
/**
* TODO: Move me to @sentry/browser when @sentry/utils no longer contains any browser code
*/
export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
/**
* Given a child DOM element, returns a query-selector statement describing that
* and its ancestors
* e.g. [HTMLElement] => body > div > input#foo.btn[name=baz]
* @returns generated DOM path
*/
export function htmlTreeAsString(elem: unknown, keyAttrs?: string[]): string {
type SimpleNode = {
parentNode: SimpleNode;
} | null;
// try/catch both:
// - accessing event.target (see getsentry/raven-js#838, #768)
// - `htmlTreeAsString` because it's complex, and just accessing the DOM incorrectly
// - can throw an exception in some circumstances.
try {
let currentElem = elem as SimpleNode;
const MAX_TRAVERSE_HEIGHT = 5;
const MAX_OUTPUT_LEN = 80;
const out = [];
let height = 0;
let len = 0;
const separator = ' > ';
const sepLength = separator.length;
let nextStr;
// eslint-disable-next-line no-plusplus
while (currentElem && height++ < MAX_TRAVERSE_HEIGHT) {
nextStr = _htmlElementAsString(currentElem, keyAttrs);
// bail out if
// - nextStr is the 'html' element
// - the length of the string that would be created exceeds MAX_OUTPUT_LEN
// (ignore this limit if we are on the first iteration)
if (nextStr === 'html' || (height > 1 && len + out.length * sepLength + nextStr.length >= MAX_OUTPUT_LEN)) {
break;
}
out.push(nextStr);
len += nextStr.length;
currentElem = currentElem.parentNode;
}
return out.reverse().join(separator);
} catch (_oO) {
return '<unknown>';
}
}
/**
* Returns a simple, query-selector representation of a DOM element
* e.g. [HTMLElement] => input#foo.btn[name=baz]
* @returns generated DOM path
*/
function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string {
const elem = el as {
tagName?: string;
id?: string;
className?: string;
getAttribute(key: string): string;
};
const out = [];
let className;
let classes;
let key;
let attr;
let i;
if (!elem || !elem.tagName) {
return '';
}
out.push(elem.tagName.toLowerCase());
// Pairs of attribute keys defined in `serializeAttribute` and their values on element.
const keyAttrPairs =
keyAttrs && keyAttrs.length
? keyAttrs.filter(keyAttr => elem.getAttribute(keyAttr)).map(keyAttr => [keyAttr, elem.getAttribute(keyAttr)])
: null;
if (keyAttrPairs && keyAttrPairs.length) {
keyAttrPairs.forEach(keyAttrPair => {
out.push(`[${keyAttrPair[0]}="${keyAttrPair[1]}"]`);
});
} else {
if (elem.id) {
out.push(`#${elem.id}`);
}
// eslint-disable-next-line prefer-const
className = elem.className;
if (className && isString(className)) {
classes = className.split(/\s+/);
for (i = 0; i < classes.length; i++) {
out.push(`.${classes[i]}`);
}
}
}
const allowedAttrs = ['type', 'name', 'title', 'alt'];
for (i = 0; i < allowedAttrs.length; i++) {
key = allowedAttrs[i];
attr = elem.getAttribute(key);
if (attr) {
out.push(`[${key}="${attr}"]`);
}
}
return out.join('');
}
/**
* A safe form of location.href
*/
export function getLocationHref(): string {
try {
return WINDOW.document.location.href;
} catch (oO) {
return '';
}
}
/**
* Gets a DOM element by using document.querySelector.
*
* This wrapper will first check for the existance of the function before
* actually calling it so that we don't have to take care of this check,
* every time we want to access the DOM.
*
* Reason: DOM/querySelector is not available in all environments.
*
* We have to cast to any because utils can be consumed by a variety of environments,
* and we don't want to break TS users. If you know what element will be selected by
* `document.querySelector`, specify it as part of the generic call. For example,
* `const element = getDomElement<Element>('selector');`
*
* @param selector the selector string passed on to document.querySelector
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getDomElement<E = any>(selector: string): E | null {
if (WINDOW.document && WINDOW.document.querySelector) {
return WINDOW.document.querySelector(selector) as unknown as E;
}
return null;
}