noibu-react-native 0.0.1
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 +155 -0
- package/dist/api/clientConfig.js +416 -0
- package/dist/api/helpCode.js +106 -0
- package/dist/api/inputManager.js +233 -0
- package/dist/api/metroplexSocket.js +882 -0
- package/dist/api/storedMetrics.js +201 -0
- package/dist/api/storedPageVisit.js +235 -0
- package/dist/const_matchers.js +260 -0
- package/dist/constants.d.ts +264 -0
- package/dist/constants.js +528 -0
- package/dist/entry/index.d.ts +8 -0
- package/dist/entry/index.js +15 -0
- package/dist/entry/init.js +91 -0
- package/dist/monitors/clickMonitor.js +284 -0
- package/dist/monitors/elementMonitor.js +174 -0
- package/dist/monitors/errorMonitor.js +295 -0
- package/dist/monitors/gqlErrorValidator.js +306 -0
- package/dist/monitors/httpDataBundler.js +665 -0
- package/dist/monitors/inputMonitor.js +130 -0
- package/dist/monitors/keyboardInputMonitor.js +67 -0
- package/dist/monitors/locationChangeMonitor.js +30 -0
- package/dist/monitors/pageMonitor.js +119 -0
- package/dist/monitors/requestMonitor.js +679 -0
- package/dist/pageVisit/pageVisit.js +172 -0
- package/dist/pageVisit/pageVisitEventError/pageVisitEventError.js +313 -0
- package/dist/pageVisit/pageVisitEventHTTP/pageVisitEventHTTP.js +115 -0
- package/dist/pageVisit/userStep/userStep.js +20 -0
- package/dist/react/ErrorBoundary.d.ts +72 -0
- package/dist/react/ErrorBoundary.js +102 -0
- package/dist/storage/localStorageProvider.js +23 -0
- package/dist/storage/rnStorageProvider.js +62 -0
- package/dist/storage/sessionStorageProvider.js +23 -0
- package/dist/storage/storage.js +119 -0
- package/dist/storage/storageProvider.js +83 -0
- package/dist/utils/date.js +62 -0
- package/dist/utils/eventlistener.js +67 -0
- package/dist/utils/function.js +398 -0
- package/dist/utils/object.js +144 -0
- package/dist/utils/performance.js +21 -0
- package/package.json +57 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import Pressability from 'react-native/Libraries/Pressability/Pressability';
|
|
2
|
+
import { WHITELIST_HTML_ID_TEXT_REGEX, USERSTEP_EVENT_TYPE, SOURCE_ATT_NAME, TEXT_ATT_NAME, TAGNAME_ATT_NAME, HTMLID_ATT_NAME, TYPE_ATT_NAME, CLICK_EVENT_TYPE, CSS_CLASS_ATT_NAME } from '../constants.js';
|
|
3
|
+
import { PageVisit } from '../pageVisit/pageVisit.js';
|
|
4
|
+
import { updatePayload } from '../pageVisit/userStep/userStep.js';
|
|
5
|
+
import StoredMetrics from '../api/storedMetrics.js';
|
|
6
|
+
import { WHITELIST_TEXT_REGEX_STRING } from '../const_matchers.js';
|
|
7
|
+
import { getBlockedCSSForCurrentDomain, maskTextInput } from '../utils/function.js';
|
|
8
|
+
import { timestampWrapper } from '../utils/date.js';
|
|
9
|
+
|
|
10
|
+
/** @module ClickMonitor */
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const maxParentIteration = 5;
|
|
14
|
+
|
|
15
|
+
/** Monitors the clicks which we capture and later process */
|
|
16
|
+
class ClickMonitor {
|
|
17
|
+
/**
|
|
18
|
+
* Creates an instance of the ClickMonitor instance
|
|
19
|
+
*/
|
|
20
|
+
constructor() {
|
|
21
|
+
// compile white list regex only once
|
|
22
|
+
this.textCapturedWhiteListRegex = new RegExp(
|
|
23
|
+
WHITELIST_TEXT_REGEX_STRING(),
|
|
24
|
+
'i',
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
this.htmlIDAllowListRegex = new RegExp(WHITELIST_HTML_ID_TEXT_REGEX, 'i');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** gets the singleton instance
|
|
31
|
+
* @returns {ClickMonitor}
|
|
32
|
+
*/
|
|
33
|
+
static getInstance() {
|
|
34
|
+
if (!this.instance) {
|
|
35
|
+
this.instance = new ClickMonitor();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return this.instance;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Starts monitoring clicks on the document */
|
|
42
|
+
monitorClicks() {
|
|
43
|
+
const onClickHandler = this._onClickHandle.bind(this);
|
|
44
|
+
// addSafeEventListener(window, 'click', onClickHandler, true);
|
|
45
|
+
|
|
46
|
+
if (!Pressability.prototype.originalCreateEventHandlers) {
|
|
47
|
+
Pressability.prototype.originalCreateEventHandlers =
|
|
48
|
+
Pressability.prototype.getEventHandlers;
|
|
49
|
+
|
|
50
|
+
Pressability.prototype.getEventHandlers = function () {
|
|
51
|
+
const ehs =
|
|
52
|
+
Pressability.prototype.originalCreateEventHandlers.call(this);
|
|
53
|
+
|
|
54
|
+
return Object.fromEntries(
|
|
55
|
+
Object.entries(ehs).map(([key, handler]) => [
|
|
56
|
+
key,
|
|
57
|
+
(event, ...args) => {
|
|
58
|
+
if (key === 'onResponderRelease') {
|
|
59
|
+
onClickHandler(event);
|
|
60
|
+
}
|
|
61
|
+
return handler(event, ...args);
|
|
62
|
+
},
|
|
63
|
+
]),
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handles a single click event
|
|
71
|
+
* @param {} event
|
|
72
|
+
*/
|
|
73
|
+
_onClickHandle(event) {
|
|
74
|
+
const blockedCSS = getBlockedCSSForCurrentDomain();
|
|
75
|
+
if (event) {
|
|
76
|
+
const { _targetInst: target } = event;
|
|
77
|
+
const targetClassName = target.elementType;
|
|
78
|
+
|
|
79
|
+
let text = '';
|
|
80
|
+
// if the tag name of the src element is image, then we need
|
|
81
|
+
// to process the image name, else we need to get the textual content
|
|
82
|
+
// todo process images
|
|
83
|
+
|
|
84
|
+
text = this._getTextualContentFromEl(target, false, blockedCSS);
|
|
85
|
+
|
|
86
|
+
let textFromElement = this._trimText(text);
|
|
87
|
+
|
|
88
|
+
let tagName = '';
|
|
89
|
+
if (targetClassName) {
|
|
90
|
+
tagName = targetClassName.toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// id of element
|
|
94
|
+
let hid = target.memoizedProps.testID || '';
|
|
95
|
+
// in some bizarre cases, the html id of an element gets overriden
|
|
96
|
+
// to contain jquery objects. If the hid is an object, it's of no
|
|
97
|
+
// use to us.
|
|
98
|
+
if (typeof hid !== 'string') {
|
|
99
|
+
hid = '';
|
|
100
|
+
}
|
|
101
|
+
// if we find that the text matches analytic data used
|
|
102
|
+
// to find checkout starts, add to cart clicks, etc.
|
|
103
|
+
// we do not mask it.
|
|
104
|
+
if (
|
|
105
|
+
!this.textCapturedWhiteListRegex.test(textFromElement) &&
|
|
106
|
+
!this.htmlIDAllowListRegex.test(hid)
|
|
107
|
+
) {
|
|
108
|
+
if (tagName === 'input') {
|
|
109
|
+
if (
|
|
110
|
+
event.type &&
|
|
111
|
+
(event.type === 'button' || event.type === 'submit')
|
|
112
|
+
) ; else {
|
|
113
|
+
textFromElement = '*';
|
|
114
|
+
}
|
|
115
|
+
} else if (tagName === 'textarea') {
|
|
116
|
+
textFromElement = '*';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
textFromElement = maskTextInput(textFromElement);
|
|
121
|
+
const tPayload = {
|
|
122
|
+
[SOURCE_ATT_NAME]: '',
|
|
123
|
+
[TEXT_ATT_NAME]: textFromElement,
|
|
124
|
+
[TAGNAME_ATT_NAME]: tagName,
|
|
125
|
+
[HTMLID_ATT_NAME]: hid,
|
|
126
|
+
[TYPE_ATT_NAME]: CLICK_EVENT_TYPE,
|
|
127
|
+
[CSS_CLASS_ATT_NAME]: '',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
StoredMetrics.getInstance().addPvClick();
|
|
131
|
+
|
|
132
|
+
PageVisit.getInstance().addPageVisitEvents(
|
|
133
|
+
[
|
|
134
|
+
{
|
|
135
|
+
event: updatePayload(tPayload),
|
|
136
|
+
occurredAt: new Date(timestampWrapper(Date.now())).toISOString(),
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
USERSTEP_EVENT_TYPE,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* parseTextFromParentElement will parse the parents of an element to try
|
|
146
|
+
* and find textual content if no text can be extracted from the clicked element
|
|
147
|
+
* @param {} element
|
|
148
|
+
* @param {Array.<String>} blockedCSS
|
|
149
|
+
*/
|
|
150
|
+
_parseTextFromParentElement(element, blockedCSS) {
|
|
151
|
+
let iteratableElement = element;
|
|
152
|
+
const parentElements = [];
|
|
153
|
+
let parentIterations = 0;
|
|
154
|
+
while (iteratableElement) {
|
|
155
|
+
if (
|
|
156
|
+
parentIterations >= maxParentIteration ||
|
|
157
|
+
!iteratableElement.parentNode
|
|
158
|
+
) {
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
iteratableElement = iteratableElement.parentNode;
|
|
162
|
+
parentElements.push(iteratableElement);
|
|
163
|
+
parentIterations += 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (let i = 0; i < parentElements.length; i += 1) {
|
|
167
|
+
const el = parentElements[i];
|
|
168
|
+
// we only get the text content if the clicked element has a button parent
|
|
169
|
+
if (el && el.tagName === 'BUTTON') {
|
|
170
|
+
// we disable the linter because we use 1 level recursion.
|
|
171
|
+
// eslint-disable-next-line no-use-before-define
|
|
172
|
+
return this._getTextualContentFromEl(el, false, blockedCSS);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return '';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Gets the textual content from an element, if any
|
|
180
|
+
* @param {} element
|
|
181
|
+
* @param {} parseParent
|
|
182
|
+
* @param {Array.<String>} blockedCSS
|
|
183
|
+
*/
|
|
184
|
+
_getTextualContentFromEl(element, parseParent, blockedCSS) {
|
|
185
|
+
return this._parseInnerContent(
|
|
186
|
+
element,
|
|
187
|
+
'',
|
|
188
|
+
100,
|
|
189
|
+
{ value: 0, limit: 100 },
|
|
190
|
+
blockedCSS,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Parse and trim text
|
|
195
|
+
* @param {} text
|
|
196
|
+
*/
|
|
197
|
+
_trimText(text) {
|
|
198
|
+
// regex to replace all whitespace with a single space and trim text
|
|
199
|
+
let parsedText = text.trim().replace(/\s+/g, ' ');
|
|
200
|
+
|
|
201
|
+
// if there is text that is longer than average sentence length,
|
|
202
|
+
// chances are that it is not a valid text so we need to further process it
|
|
203
|
+
if (parsedText.length > 100) {
|
|
204
|
+
const index = parsedText.lastIndexOf(' ', 97);
|
|
205
|
+
// We can not chop the text at exactly 97 characters since chopping in the
|
|
206
|
+
// middle of an encoded value may cause deserialization issues. Chop at the
|
|
207
|
+
// last ' ' character before the 97th character.
|
|
208
|
+
if (index > 0) {
|
|
209
|
+
parsedText = `${parsedText.substring(0, index)}...`;
|
|
210
|
+
} else {
|
|
211
|
+
// If the are no ' ' characters then the text is likely not valid so just
|
|
212
|
+
// return '...'.
|
|
213
|
+
parsedText = '...';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return parsedText;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Recursively parses element's inner content and masks blocked css classes
|
|
221
|
+
* @param {HTMLElement} element
|
|
222
|
+
* @param {String} text
|
|
223
|
+
* @param {Number} textLimit
|
|
224
|
+
* @param {Object} counter
|
|
225
|
+
* @param {Array.<String>} blockedCSS
|
|
226
|
+
*/
|
|
227
|
+
_parseInnerContent(element, text, textLimit, counter, blockedCSS) {
|
|
228
|
+
/* eslint-disable no-restricted-syntax */
|
|
229
|
+
/* eslint-disable no-param-reassign */
|
|
230
|
+
|
|
231
|
+
if (text.length >= textLimit) {
|
|
232
|
+
return text;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (counter.value >= counter.limit) {
|
|
236
|
+
return text;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
counter.value += 1;
|
|
240
|
+
|
|
241
|
+
if (blockedCSS.includes(element.memoizedProps.testID)) {
|
|
242
|
+
return `${text}${text ? ' ' : ''}*`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const walk = node => {
|
|
246
|
+
if (typeof node.memoizedProps === 'string') {
|
|
247
|
+
text = this._parseAndAppendText(text, [node.memoizedProps]);
|
|
248
|
+
if (text.length >= textLimit) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (node.child) {
|
|
253
|
+
walk(node.child);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
walk(element);
|
|
257
|
+
|
|
258
|
+
return text;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* normalize value and append to the resulting text if not empty
|
|
263
|
+
* @param {String} text
|
|
264
|
+
* @param {Array.<String>} values
|
|
265
|
+
*/
|
|
266
|
+
_parseAndAppendText(text, values) {
|
|
267
|
+
const goodValues = [];
|
|
268
|
+
for (const v of values) {
|
|
269
|
+
if (Number.isFinite(v) || typeof v === 'string') {
|
|
270
|
+
goodValues.push(v);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (let value of goodValues) {
|
|
275
|
+
value = `${value}`.trim().replace(/\s+/g, ' ');
|
|
276
|
+
if (value.length > 0) {
|
|
277
|
+
return text + (text ? ' ' : '') + value;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return text;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export { ClickMonitor };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { GET_ATTRIBUTE_SELECTORS } from '../constants.js';
|
|
2
|
+
import InputManager from '../api/inputManager.js';
|
|
3
|
+
|
|
4
|
+
/** @module ElementMonitor */
|
|
5
|
+
|
|
6
|
+
/** Monitors the elements matching query selectors and passes them to the InputManager */
|
|
7
|
+
class ElementMonitor {
|
|
8
|
+
/** gets the singleton instance */
|
|
9
|
+
static getInstance() {
|
|
10
|
+
if (!this.instance) {
|
|
11
|
+
this.instance = new ElementMonitor();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return this.instance;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Safely calls `querySelectorAll` on the given node and returns an array of
|
|
19
|
+
* matching child elements. If `querySelectorAll` returns a falsy value,
|
|
20
|
+
* an empty array is returned.
|
|
21
|
+
*
|
|
22
|
+
* @private
|
|
23
|
+
* @param {Node} node - The node on which to call `querySelectorAll`.
|
|
24
|
+
* @param {string} selector - The CSS selector used to match child elements.
|
|
25
|
+
* @returns {Array<Element>} An array of matching child elements.
|
|
26
|
+
*/
|
|
27
|
+
_safeQueryAll(node, selector) {
|
|
28
|
+
const matchingChildElements = node.querySelectorAll(selector);
|
|
29
|
+
if (!matchingChildElements) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return Array.from(matchingChildElements);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Processes the given elements by adding their custom attributes to the InputManager.
|
|
38
|
+
*
|
|
39
|
+
* @private
|
|
40
|
+
* @param {NodeList|Array} elements - The list of elements to be processed.
|
|
41
|
+
* @param {string} selector - The selector corresponding to the elements being processed.
|
|
42
|
+
*/
|
|
43
|
+
_processMatchingElements(elements, selector) {
|
|
44
|
+
elements.forEach(element => {
|
|
45
|
+
if (!element) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const selectorText = element.textContent;
|
|
50
|
+
|
|
51
|
+
if (!selectorText) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
InputManager.getInstance()._addCustomAttribute(selector, selectorText);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Finds and processes matching elements within the added nodes by checking if
|
|
61
|
+
* they match any of the attribute selectors from GET_ATTRIBUTE_SELECTORS().
|
|
62
|
+
* If a node or any of its descendants match, their custom attributes are added
|
|
63
|
+
* to the InputManager.
|
|
64
|
+
*
|
|
65
|
+
* @private
|
|
66
|
+
* @param {NodeList|Array} addedNodes - The list of nodes that were added or
|
|
67
|
+
* modified in the DOM. Each node in the list will be checked against the
|
|
68
|
+
* attribute selectors, and their matching descendants will also be processed.
|
|
69
|
+
*/
|
|
70
|
+
_findAndAddMatchingElementsInNodes(addedNodes) {
|
|
71
|
+
const attributeSelectorNames = Object.keys(GET_ATTRIBUTE_SELECTORS());
|
|
72
|
+
attributeSelectorNames.forEach(name => {
|
|
73
|
+
const selector = GET_ATTRIBUTE_SELECTORS()[name];
|
|
74
|
+
if (!selector) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
addedNodes.forEach(node => {
|
|
79
|
+
// move to the next node its not an element node
|
|
80
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let matchingElements = [];
|
|
85
|
+
|
|
86
|
+
if (node.matches(selector)) {
|
|
87
|
+
matchingElements.push(node);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const matchingChildElements = this._safeQueryAll(node, selector);
|
|
91
|
+
matchingElements = matchingElements.concat(matchingChildElements);
|
|
92
|
+
|
|
93
|
+
this._processMatchingElements(matchingElements, name);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Sets up a MutationObserver to monitor added elements and attribute changes */
|
|
99
|
+
_setupMutationObserver() {
|
|
100
|
+
const observer = new MutationObserver(mutations => {
|
|
101
|
+
mutations.forEach(mutation => {
|
|
102
|
+
// Handle childList mutations
|
|
103
|
+
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
|
|
104
|
+
this._findAndAddMatchingElementsInNodes(mutation.addedNodes);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle attribute mutations
|
|
108
|
+
if (mutation.type === 'attributes') {
|
|
109
|
+
const targetNode = mutation.target;
|
|
110
|
+
if (targetNode.nodeType === Node.ELEMENT_NODE) {
|
|
111
|
+
this._findAndAddMatchingElementsInNodes([targetNode]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// the observer will get called when children are added and changed
|
|
118
|
+
const observerConfig = { childList: true, subtree: true, attributes: true };
|
|
119
|
+
observer.observe(document.documentElement, observerConfig);
|
|
120
|
+
this.observer = observer;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Finds and adds matching elements to the InputManager */
|
|
124
|
+
_findAndAddMatchingElements() {
|
|
125
|
+
const attributeSelectorNames = Object.keys(GET_ATTRIBUTE_SELECTORS());
|
|
126
|
+
attributeSelectorNames.forEach(name => {
|
|
127
|
+
const selector = GET_ATTRIBUTE_SELECTORS()[name];
|
|
128
|
+
if (!selector) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const elements = this._safeQueryAll(document, selector);
|
|
132
|
+
this._processMatchingElements(elements, name);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Starts monitoring elements matching query selectors */
|
|
137
|
+
monitor() {
|
|
138
|
+
const attributeNames = Object.keys(GET_ATTRIBUTE_SELECTORS());
|
|
139
|
+
// if we have no attributes to look for, do not setup the listeners
|
|
140
|
+
if (attributeNames.length === 0) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this._findAndAddMatchingElements();
|
|
144
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
145
|
+
this._setupMutationObserver();
|
|
146
|
+
} else {
|
|
147
|
+
// Fallback using setInterval if MutationObserver is not supported
|
|
148
|
+
this.interval = setInterval(() => {
|
|
149
|
+
this._findAndAddMatchingElements();
|
|
150
|
+
}, 5000); // Check for changes every N seconds
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Disconnects the MutationObserver instance, if it exists and has a valid disconnect method.
|
|
156
|
+
* This is needed for testing since we want to properly tear down mutation listeners
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
_disconnectObserver() {
|
|
160
|
+
if (
|
|
161
|
+
this.observer &&
|
|
162
|
+
this.observer.disconnect &&
|
|
163
|
+
typeof this.observer.disconnect === 'function'
|
|
164
|
+
) {
|
|
165
|
+
this.observer.disconnect();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.interval) {
|
|
169
|
+
clearInterval(this.interval);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export { ElementMonitor };
|