@midscene/recorder-ui 0.0.0
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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/Button.js +19 -0
- package/dist/RecordTimeline.css +41 -0
- package/dist/RecordTimeline.js +549 -0
- package/dist/button.css +35 -0
- package/dist/components/shiny-text.css +73 -0
- package/dist/components/shiny-text.js +13 -0
- package/dist/index.js +4 -0
- package/dist/recorder-iife-index.js +2 -0
- package/dist/recorder-iife.js +455 -0
- package/dist/recorder.js +323 -0
- package/dist/types/src/Button.d.ts +10 -0
- package/dist/types/src/RecordTimeline.d.ts +9 -0
- package/dist/types/src/components/shiny-text.d.ts +12 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/recorder-iife-index.d.ts +6 -0
- package/dist/types/src/recorder.d.ts +41 -0
- package/package.json +43 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
.shiny-text {
|
|
2
|
+
color: #0000;
|
|
3
|
+
letter-spacing: .5px;
|
|
4
|
+
text-shadow: 0 1px 2px #0000000d;
|
|
5
|
+
background-image: linear-gradient(45deg, #2b83ff, #6a11cb, #2575fc, #4481eb);
|
|
6
|
+
background-size: 300%;
|
|
7
|
+
background-clip: text;
|
|
8
|
+
font-weight: 600;
|
|
9
|
+
animation: 8s infinite textGradient;
|
|
10
|
+
display: inline-block;
|
|
11
|
+
position: relative;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.shiny-text:after {
|
|
16
|
+
content: "";
|
|
17
|
+
width: 120%;
|
|
18
|
+
height: 120%;
|
|
19
|
+
animation: shine var(--animation-duration, 5s) cubic-bezier(.25, .1, .25, 1) infinite;
|
|
20
|
+
z-index: 1;
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
background: linear-gradient(90deg, #fff0 0%, #ffffff1a 10%, #fff9 50%, #ffffff1a 90%, #fff0 100%);
|
|
23
|
+
position: absolute;
|
|
24
|
+
top: -10%;
|
|
25
|
+
left: -150%;
|
|
26
|
+
transform: skewX(-20deg)translateY(0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.shiny-text.disabled {
|
|
30
|
+
background: #2b83ff;
|
|
31
|
+
background-clip: text;
|
|
32
|
+
animation: none;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.shiny-text.disabled:after {
|
|
36
|
+
animation: none;
|
|
37
|
+
display: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@keyframes shine {
|
|
41
|
+
0% {
|
|
42
|
+
opacity: .7;
|
|
43
|
+
left: -150%;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
20% {
|
|
47
|
+
opacity: 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
80% {
|
|
51
|
+
opacity: 1;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
100% {
|
|
55
|
+
opacity: .7;
|
|
56
|
+
left: 250%;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@keyframes textGradient {
|
|
61
|
+
0% {
|
|
62
|
+
background-position: 0%;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
50% {
|
|
66
|
+
background-position: 100%;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
100% {
|
|
70
|
+
background-position: 0%;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import "./shiny-text.css";
|
|
3
|
+
const ShinyText = ({ text, disabled = false, speed = 5, className = '' })=>{
|
|
4
|
+
const style = {
|
|
5
|
+
'--animation-duration': `${speed}s`
|
|
6
|
+
};
|
|
7
|
+
return /*#__PURE__*/ jsx("div", {
|
|
8
|
+
className: `shiny-text ${disabled ? 'disabled' : ''} ${className}`,
|
|
9
|
+
style: style,
|
|
10
|
+
children: text
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
export { ShinyText };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Button } from "./Button.js";
|
|
2
|
+
import { EventRecorder, convertToChromeEvent, convertToChromeEvents } from "./recorder.js";
|
|
3
|
+
import { RecordTimeline } from "./RecordTimeline.js";
|
|
4
|
+
export { Button, EventRecorder, RecordTimeline, convertToChromeEvent, convertToChromeEvents };
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
(()=>{
|
|
2
|
+
"use strict";
|
|
3
|
+
const RECORDER_SCROLL_BATCH_DELAY_MS = 200;
|
|
4
|
+
const RECORDER_INPUT_BATCH_DELAY_MS = 300;
|
|
5
|
+
function isFormElement(node) {
|
|
6
|
+
return node instanceof HTMLElement && ('input' === node.tagName.toLowerCase() || 'textarea' === node.tagName.toLowerCase() || 'select' === node.tagName.toLowerCase() || 'option' === node.tagName.toLowerCase());
|
|
7
|
+
}
|
|
8
|
+
function isButtonElement(node) {
|
|
9
|
+
return node instanceof HTMLElement && 'button' === node.tagName.toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
function isAElement(node) {
|
|
12
|
+
return node instanceof HTMLElement && 'a' === node.tagName.toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
function isSvgElement(node) {
|
|
15
|
+
return node instanceof SVGElement;
|
|
16
|
+
}
|
|
17
|
+
function isImgElement(node) {
|
|
18
|
+
if (!includeBaseElement(node) && node instanceof Element) {
|
|
19
|
+
const computedStyle = window.getComputedStyle(node);
|
|
20
|
+
const backgroundImage = computedStyle.getPropertyValue('background-image');
|
|
21
|
+
if ('none' !== backgroundImage) return true;
|
|
22
|
+
}
|
|
23
|
+
if (isIconfont(node)) return true;
|
|
24
|
+
return node instanceof HTMLElement && 'img' === node.tagName.toLowerCase() || node instanceof SVGElement && 'svg' === node.tagName.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
function isIconfont(node) {
|
|
27
|
+
if (node instanceof Element) {
|
|
28
|
+
const computedStyle = window.getComputedStyle(node);
|
|
29
|
+
const fontFamilyValue = computedStyle.fontFamily || '';
|
|
30
|
+
return fontFamilyValue.toLowerCase().indexOf('iconfont') >= 0;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
function isNotContainerElement(node) {
|
|
35
|
+
return isTextElement(node) || isIconfont(node) || isImgElement(node) || isButtonElement(node) || isAElement(node) || isFormElement(node);
|
|
36
|
+
}
|
|
37
|
+
function isTextElement(node) {
|
|
38
|
+
if (node instanceof Element) {
|
|
39
|
+
if (node?.childNodes?.length === 1 && node?.childNodes[0] instanceof Text) return true;
|
|
40
|
+
}
|
|
41
|
+
return node.nodeName?.toLowerCase?.() === '#text' && !isIconfont(node);
|
|
42
|
+
}
|
|
43
|
+
function includeBaseElement(node) {
|
|
44
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
45
|
+
if (node.innerText) return true;
|
|
46
|
+
const includeList = [
|
|
47
|
+
'svg',
|
|
48
|
+
'button',
|
|
49
|
+
'input',
|
|
50
|
+
'textarea',
|
|
51
|
+
'select',
|
|
52
|
+
'option',
|
|
53
|
+
'img',
|
|
54
|
+
'a'
|
|
55
|
+
];
|
|
56
|
+
for (const tagName of includeList){
|
|
57
|
+
const element = node.querySelectorAll(tagName);
|
|
58
|
+
if (element.length > 0) return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
let debugMode = false;
|
|
63
|
+
function util_logger(..._msg) {
|
|
64
|
+
if (!debugMode) return;
|
|
65
|
+
console.log(..._msg);
|
|
66
|
+
}
|
|
67
|
+
const SUB_XPATH_SEPARATOR = '|>>|';
|
|
68
|
+
const getElementXpathIndex = (element)=>{
|
|
69
|
+
let index = 1;
|
|
70
|
+
let prev = element.previousElementSibling;
|
|
71
|
+
while(prev){
|
|
72
|
+
if (prev.nodeName.toLowerCase() === element.nodeName.toLowerCase()) index++;
|
|
73
|
+
prev = prev.previousElementSibling;
|
|
74
|
+
}
|
|
75
|
+
return index;
|
|
76
|
+
};
|
|
77
|
+
const normalizeXpathText = (text)=>{
|
|
78
|
+
if ('string' != typeof text) return '';
|
|
79
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
80
|
+
};
|
|
81
|
+
const buildCurrentElementXpath = (element, isOrderSensitive, isLeafElement, limitToCurrentDocument = false)=>{
|
|
82
|
+
const parentPath = element.parentNode ? getElementXpath(element.parentNode, isOrderSensitive, false, limitToCurrentDocument) : '';
|
|
83
|
+
const prefix = parentPath ? `${parentPath}/` : '/';
|
|
84
|
+
const tagName = element.nodeName.toLowerCase();
|
|
85
|
+
const textContent = element.textContent?.trim();
|
|
86
|
+
const isSVGNamespace = 'http://www.w3.org/2000/svg' === element.namespaceURI;
|
|
87
|
+
const tagSelector = isSVGNamespace ? `*[name()="${tagName}"]` : tagName;
|
|
88
|
+
if (isOrderSensitive) {
|
|
89
|
+
const index = getElementXpathIndex(element);
|
|
90
|
+
return `${prefix}${tagSelector}[${index}]`;
|
|
91
|
+
}
|
|
92
|
+
if (isLeafElement && textContent) return `${prefix}${tagSelector}[normalize-space()="${normalizeXpathText(textContent)}"]`;
|
|
93
|
+
const index = getElementXpathIndex(element);
|
|
94
|
+
return `${prefix}${tagSelector}[${index}]`;
|
|
95
|
+
};
|
|
96
|
+
const getElementXpath = (element, isOrderSensitive = false, isLeafElement = false, limitToCurrentDocument = false)=>{
|
|
97
|
+
if (element.nodeType === Node.TEXT_NODE) {
|
|
98
|
+
const parentNode = element.parentNode;
|
|
99
|
+
if (parentNode && parentNode.nodeType === Node.ELEMENT_NODE) {
|
|
100
|
+
const parentXPath = getElementXpath(parentNode, isOrderSensitive, true, limitToCurrentDocument);
|
|
101
|
+
const textContent = element.textContent?.trim();
|
|
102
|
+
if (textContent) return `${parentXPath}/text()[normalize-space()="${normalizeXpathText(textContent)}"]`;
|
|
103
|
+
return `${parentXPath}/text()`;
|
|
104
|
+
}
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
if (element.nodeType !== Node.ELEMENT_NODE) return '';
|
|
108
|
+
const el = element;
|
|
109
|
+
try {
|
|
110
|
+
const nodeName = el.nodeName.toLowerCase();
|
|
111
|
+
if (el === el.ownerDocument?.documentElement || 'html' === nodeName) {
|
|
112
|
+
if (!limitToCurrentDocument) {
|
|
113
|
+
const frameElement = el.ownerDocument?.defaultView?.frameElement;
|
|
114
|
+
if (frameElement) {
|
|
115
|
+
const framePath = getElementXpath(frameElement, isOrderSensitive, false, limitToCurrentDocument);
|
|
116
|
+
return `${framePath}${SUB_XPATH_SEPARATOR}/html`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return '/html';
|
|
120
|
+
}
|
|
121
|
+
if (el === el.ownerDocument?.body || 'body' === nodeName) {
|
|
122
|
+
if (!limitToCurrentDocument) {
|
|
123
|
+
const frameElement = el.ownerDocument?.defaultView?.frameElement;
|
|
124
|
+
if (frameElement) {
|
|
125
|
+
const framePath = getElementXpath(frameElement, isOrderSensitive, false, limitToCurrentDocument);
|
|
126
|
+
return `${framePath}${SUB_XPATH_SEPARATOR}/html/body`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return '/html/body';
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
util_logger('[midscene:locator] ownerDocument access failed:', error);
|
|
133
|
+
if ('html' === el.nodeName.toLowerCase()) return '/html';
|
|
134
|
+
if ('body' === el.nodeName.toLowerCase()) return '/html/body';
|
|
135
|
+
}
|
|
136
|
+
if (isSvgElement(el)) {
|
|
137
|
+
const tagName = el.nodeName.toLowerCase();
|
|
138
|
+
if ('svg' === tagName) return buildCurrentElementXpath(el, isOrderSensitive, isLeafElement, limitToCurrentDocument);
|
|
139
|
+
let parent = el.parentNode;
|
|
140
|
+
while(parent && parent.nodeType === Node.ELEMENT_NODE){
|
|
141
|
+
const parentEl = parent;
|
|
142
|
+
if (!isSvgElement(parentEl)) return getElementXpath(parentEl, isOrderSensitive, isLeafElement, limitToCurrentDocument);
|
|
143
|
+
const parentTag = parentEl.nodeName.toLowerCase();
|
|
144
|
+
if ('svg' === parentTag) return getElementXpath(parentEl, isOrderSensitive, isLeafElement, limitToCurrentDocument);
|
|
145
|
+
parent = parent.parentNode;
|
|
146
|
+
}
|
|
147
|
+
const fallbackParent = el.parentNode;
|
|
148
|
+
if (fallbackParent && fallbackParent.nodeType === Node.ELEMENT_NODE) return getElementXpath(fallbackParent, isOrderSensitive, isLeafElement, limitToCurrentDocument);
|
|
149
|
+
return '';
|
|
150
|
+
}
|
|
151
|
+
return buildCurrentElementXpath(el, isOrderSensitive, isLeafElement, limitToCurrentDocument);
|
|
152
|
+
};
|
|
153
|
+
const DEBUG = 'undefined' != typeof localStorage && 'true' === localStorage.getItem('DEBUG');
|
|
154
|
+
function debugLog(...args) {
|
|
155
|
+
if (DEBUG) console.log('[EventRecorder]', ...args);
|
|
156
|
+
}
|
|
157
|
+
function recorder_generateHashId(type, elementRect) {
|
|
158
|
+
const rectStr = elementRect ? `${elementRect.left}_${elementRect.top}_${elementRect.width}_${elementRect.height}${void 0 !== elementRect.x ? `_${elementRect.x}` : ''}${void 0 !== elementRect.y ? `_${elementRect.y}` : ''}` : 'no_rect';
|
|
159
|
+
const combined = `${type}_${rectStr}_${Date.now()}`;
|
|
160
|
+
let hash = 0;
|
|
161
|
+
for(let i = 0; i < combined.length; i++){
|
|
162
|
+
const char = combined.charCodeAt(i);
|
|
163
|
+
hash = (hash << 5) - hash + char;
|
|
164
|
+
hash &= hash;
|
|
165
|
+
}
|
|
166
|
+
return Math.abs(hash).toString(36);
|
|
167
|
+
}
|
|
168
|
+
const isSameInputTarget = (event1, event2)=>event1.element === event2.element;
|
|
169
|
+
const isSameScrollTarget = (event1, event2)=>event1.element === event2.element;
|
|
170
|
+
const getLastLabelClick = (events)=>{
|
|
171
|
+
for(let i = events.length - 1; i >= 0; i--){
|
|
172
|
+
const event = events[i];
|
|
173
|
+
if ('click' === event.type && event.isLabelClick) return event;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
class EventRecorder {
|
|
177
|
+
isRecording = false;
|
|
178
|
+
eventCallback;
|
|
179
|
+
scrollThrottleTimer = null;
|
|
180
|
+
scrollThrottleDelay = RECORDER_SCROLL_BATCH_DELAY_MS;
|
|
181
|
+
inputThrottleTimer = null;
|
|
182
|
+
inputThrottleDelay = RECORDER_INPUT_BATCH_DELAY_MS;
|
|
183
|
+
lastViewportScroll = null;
|
|
184
|
+
sessionId;
|
|
185
|
+
mutationObserver = null;
|
|
186
|
+
constructor(eventCallback, sessionId){
|
|
187
|
+
this.eventCallback = eventCallback;
|
|
188
|
+
this.sessionId = sessionId;
|
|
189
|
+
}
|
|
190
|
+
createNavigationEvent(url, title) {
|
|
191
|
+
return {
|
|
192
|
+
type: 'navigation',
|
|
193
|
+
url,
|
|
194
|
+
title,
|
|
195
|
+
pageInfo: {
|
|
196
|
+
width: window.innerWidth,
|
|
197
|
+
height: window.innerHeight
|
|
198
|
+
},
|
|
199
|
+
timestamp: Date.now(),
|
|
200
|
+
hashId: `navigation_${Date.now()}`
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
start() {
|
|
204
|
+
if (this.isRecording) return void debugLog('Recording already active, ignoring start request');
|
|
205
|
+
this.isRecording = true;
|
|
206
|
+
debugLog('Starting event recording');
|
|
207
|
+
setTimeout(()=>{
|
|
208
|
+
const navigationEvent = this.createNavigationEvent(window.location.href, document.title);
|
|
209
|
+
this.eventCallback(navigationEvent);
|
|
210
|
+
debugLog('Added final navigation event', navigationEvent);
|
|
211
|
+
}, 0);
|
|
212
|
+
document.addEventListener('click', this.handleClick, true);
|
|
213
|
+
document.addEventListener('input', this.handleInput);
|
|
214
|
+
document.addEventListener('scroll', this.handleScroll, {
|
|
215
|
+
capture: true,
|
|
216
|
+
passive: true
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
stop() {
|
|
220
|
+
if (!this.isRecording) return void debugLog('Recording not active, ignoring stop request');
|
|
221
|
+
this.isRecording = false;
|
|
222
|
+
debugLog('Stopping event recording');
|
|
223
|
+
if (this.scrollThrottleTimer) {
|
|
224
|
+
clearTimeout(this.scrollThrottleTimer);
|
|
225
|
+
this.scrollThrottleTimer = null;
|
|
226
|
+
}
|
|
227
|
+
if (this.inputThrottleTimer) {
|
|
228
|
+
clearTimeout(this.inputThrottleTimer);
|
|
229
|
+
this.inputThrottleTimer = null;
|
|
230
|
+
}
|
|
231
|
+
document.removeEventListener('click', this.handleClick, true);
|
|
232
|
+
document.removeEventListener('input', this.handleInput);
|
|
233
|
+
document.removeEventListener('scroll', this.handleScroll, true);
|
|
234
|
+
debugLog('Removed all event listeners');
|
|
235
|
+
}
|
|
236
|
+
handleClick = (event)=>{
|
|
237
|
+
if (!this.isRecording) return;
|
|
238
|
+
const target = event.target;
|
|
239
|
+
const { isLabelClick, labelInfo } = this.checkLabelClick(target);
|
|
240
|
+
const rect = target.getBoundingClientRect();
|
|
241
|
+
const elementRect = {
|
|
242
|
+
x: Number(event.clientX.toFixed(2)),
|
|
243
|
+
y: Number(event.clientY.toFixed(2))
|
|
244
|
+
};
|
|
245
|
+
console.log('isNotContainerElement', isNotContainerElement(target));
|
|
246
|
+
if (isNotContainerElement(target)) {
|
|
247
|
+
elementRect.left = Number(rect.left.toFixed(2));
|
|
248
|
+
elementRect.top = Number(rect.top.toFixed(2));
|
|
249
|
+
elementRect.width = Number(rect.width.toFixed(2));
|
|
250
|
+
elementRect.height = Number(rect.height.toFixed(2));
|
|
251
|
+
}
|
|
252
|
+
const clickEvent = {
|
|
253
|
+
type: 'click',
|
|
254
|
+
elementRect,
|
|
255
|
+
pageInfo: {
|
|
256
|
+
width: window.innerWidth,
|
|
257
|
+
height: window.innerHeight
|
|
258
|
+
},
|
|
259
|
+
value: '',
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
hashId: recorder_generateHashId('click', {
|
|
262
|
+
...elementRect
|
|
263
|
+
}),
|
|
264
|
+
element: target,
|
|
265
|
+
isLabelClick,
|
|
266
|
+
labelInfo,
|
|
267
|
+
isTrusted: event.isTrusted,
|
|
268
|
+
detail: event.detail
|
|
269
|
+
};
|
|
270
|
+
this.eventCallback(clickEvent);
|
|
271
|
+
};
|
|
272
|
+
handleScroll = (event)=>{
|
|
273
|
+
if (!this.isRecording) return;
|
|
274
|
+
function isDocument(target) {
|
|
275
|
+
return target instanceof Document;
|
|
276
|
+
}
|
|
277
|
+
const target = event.target;
|
|
278
|
+
const scrollXTarget = isDocument(target) ? window.scrollX : target.scrollLeft;
|
|
279
|
+
const scrollYTarget = isDocument(target) ? window.scrollY : target.scrollTop;
|
|
280
|
+
const rect = isDocument(target) ? {
|
|
281
|
+
left: 0,
|
|
282
|
+
top: 0,
|
|
283
|
+
width: window.innerWidth,
|
|
284
|
+
height: window.innerHeight
|
|
285
|
+
} : target.getBoundingClientRect();
|
|
286
|
+
if (this.scrollThrottleTimer) clearTimeout(this.scrollThrottleTimer);
|
|
287
|
+
this.scrollThrottleTimer = window.setTimeout(()=>{
|
|
288
|
+
if (this.isRecording) {
|
|
289
|
+
const elementRect = {
|
|
290
|
+
left: isDocument(target) ? 0 : Number(rect.left.toFixed(2)),
|
|
291
|
+
top: isDocument(target) ? 0 : Number(rect.top.toFixed(2)),
|
|
292
|
+
width: isDocument(target) ? window.innerWidth : Number(rect.width.toFixed(2)),
|
|
293
|
+
height: isDocument(target) ? window.innerHeight : Number(rect.height.toFixed(2))
|
|
294
|
+
};
|
|
295
|
+
const scrollEvent = {
|
|
296
|
+
type: 'scroll',
|
|
297
|
+
elementRect,
|
|
298
|
+
pageInfo: {
|
|
299
|
+
width: window.innerWidth,
|
|
300
|
+
height: window.innerHeight
|
|
301
|
+
},
|
|
302
|
+
value: `${scrollXTarget.toFixed(2)},${scrollYTarget.toFixed(2)}`,
|
|
303
|
+
timestamp: Date.now(),
|
|
304
|
+
hashId: recorder_generateHashId('scroll', {
|
|
305
|
+
...elementRect
|
|
306
|
+
}),
|
|
307
|
+
element: target
|
|
308
|
+
};
|
|
309
|
+
this.eventCallback(scrollEvent);
|
|
310
|
+
}
|
|
311
|
+
this.scrollThrottleTimer = null;
|
|
312
|
+
}, this.scrollThrottleDelay);
|
|
313
|
+
};
|
|
314
|
+
handleInput = (event)=>{
|
|
315
|
+
if (!this.isRecording) return;
|
|
316
|
+
const target = event.target;
|
|
317
|
+
if ('checkbox' === target.type) return;
|
|
318
|
+
const rect = target.getBoundingClientRect();
|
|
319
|
+
const elementRect = {
|
|
320
|
+
left: Number(rect.left.toFixed(2)),
|
|
321
|
+
top: Number(rect.top.toFixed(2)),
|
|
322
|
+
width: Number(rect.width.toFixed(2)),
|
|
323
|
+
height: Number(rect.height.toFixed(2))
|
|
324
|
+
};
|
|
325
|
+
if (this.inputThrottleTimer) clearTimeout(this.inputThrottleTimer);
|
|
326
|
+
this.inputThrottleTimer = window.setTimeout(()=>{
|
|
327
|
+
if (this.isRecording) {
|
|
328
|
+
const inputEvent = {
|
|
329
|
+
type: 'input',
|
|
330
|
+
value: 'password' !== target.type ? target.value : '*****',
|
|
331
|
+
timestamp: Date.now(),
|
|
332
|
+
hashId: recorder_generateHashId('input', {
|
|
333
|
+
...elementRect
|
|
334
|
+
}),
|
|
335
|
+
element: target,
|
|
336
|
+
inputType: target.type || 'text',
|
|
337
|
+
elementRect,
|
|
338
|
+
pageInfo: {
|
|
339
|
+
width: window.innerWidth,
|
|
340
|
+
height: window.innerHeight
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
debugLog('Throttled input event:', {
|
|
344
|
+
value: inputEvent.value,
|
|
345
|
+
timestamp: inputEvent.timestamp,
|
|
346
|
+
target: target.tagName,
|
|
347
|
+
inputType: target.type
|
|
348
|
+
});
|
|
349
|
+
this.eventCallback(inputEvent);
|
|
350
|
+
}
|
|
351
|
+
this.inputThrottleTimer = null;
|
|
352
|
+
}, this.inputThrottleDelay);
|
|
353
|
+
};
|
|
354
|
+
checkLabelClick(target) {
|
|
355
|
+
let isLabelClick = false;
|
|
356
|
+
let labelInfo;
|
|
357
|
+
if (target) if ('LABEL' === target.tagName) {
|
|
358
|
+
isLabelClick = true;
|
|
359
|
+
labelInfo = {
|
|
360
|
+
htmlFor: target.htmlFor,
|
|
361
|
+
textContent: target.textContent?.trim(),
|
|
362
|
+
xpath: getElementXpath(target)
|
|
363
|
+
};
|
|
364
|
+
} else {
|
|
365
|
+
let parent = target.parentElement;
|
|
366
|
+
while(parent){
|
|
367
|
+
if ('LABEL' === parent.tagName) {
|
|
368
|
+
isLabelClick = true;
|
|
369
|
+
labelInfo = {
|
|
370
|
+
htmlFor: parent.htmlFor,
|
|
371
|
+
textContent: parent.textContent?.trim(),
|
|
372
|
+
xpath: getElementXpath(parent)
|
|
373
|
+
};
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
parent = parent.parentElement;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
isLabelClick,
|
|
381
|
+
labelInfo
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
isActive() {
|
|
385
|
+
return this.isRecording;
|
|
386
|
+
}
|
|
387
|
+
optimizeEvent(event, events) {
|
|
388
|
+
const lastEvent = events[events.length - 1];
|
|
389
|
+
if ('click' === event.type) {
|
|
390
|
+
const lastEvent = getLastLabelClick(events);
|
|
391
|
+
if (event.element) {
|
|
392
|
+
const { isLabelClick, labelInfo } = this.checkLabelClick(event.element);
|
|
393
|
+
if (lastEvent && isLabelClick && 'click' === lastEvent.type && lastEvent.isLabelClick && (lastEvent.labelInfo?.htmlFor && event.element.id && lastEvent.labelInfo?.htmlFor === event.element.id || labelInfo?.xpath && lastEvent.labelInfo?.xpath && lastEvent.labelInfo?.xpath === labelInfo?.xpath)) {
|
|
394
|
+
debugLog('Skip input event triggered by label click:', event.element);
|
|
395
|
+
return events;
|
|
396
|
+
}
|
|
397
|
+
return [
|
|
398
|
+
...events,
|
|
399
|
+
event
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if ('input' === event.type) {
|
|
404
|
+
if (lastEvent && 'click' === lastEvent.type && lastEvent.isLabelClick && lastEvent.labelInfo?.htmlFor === event.targetId) {
|
|
405
|
+
debugLog('Skipping input event - triggered by label click:', {
|
|
406
|
+
labelHtmlFor: getLastLabelClick(events)?.labelInfo?.htmlFor,
|
|
407
|
+
inputId: event.targetId,
|
|
408
|
+
element: event.element
|
|
409
|
+
});
|
|
410
|
+
return events;
|
|
411
|
+
}
|
|
412
|
+
if (lastEvent && 'input' === lastEvent.type && isSameInputTarget(lastEvent, event)) {
|
|
413
|
+
const oldInputEvent = events[events.length - 1];
|
|
414
|
+
const newEvents = [
|
|
415
|
+
...events
|
|
416
|
+
];
|
|
417
|
+
newEvents[events.length - 1] = {
|
|
418
|
+
value: event.element?.value,
|
|
419
|
+
...event
|
|
420
|
+
};
|
|
421
|
+
debugLog('Merging input event:', {
|
|
422
|
+
oldValue: oldInputEvent.value,
|
|
423
|
+
newValue: event.value,
|
|
424
|
+
oldTimestamp: oldInputEvent.timestamp,
|
|
425
|
+
newTimestamp: event.timestamp,
|
|
426
|
+
target: event.targetTagName
|
|
427
|
+
});
|
|
428
|
+
return newEvents;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if ('scroll' === event.type) {
|
|
432
|
+
if (lastEvent && 'scroll' === lastEvent.type && isSameScrollTarget(lastEvent, event)) {
|
|
433
|
+
const oldScrollEvent = events[events.length - 1];
|
|
434
|
+
const newEvents = [
|
|
435
|
+
...events
|
|
436
|
+
];
|
|
437
|
+
newEvents[events.length - 1] = event;
|
|
438
|
+
debugLog('Replacing last scroll event with new scroll event:', {
|
|
439
|
+
oldPosition: `${oldScrollEvent.elementRect?.left},${oldScrollEvent.elementRect?.top}`,
|
|
440
|
+
newPosition: `${event.elementRect?.left},${event.elementRect?.top}`,
|
|
441
|
+
oldTimestamp: oldScrollEvent.timestamp,
|
|
442
|
+
newTimestamp: event.timestamp,
|
|
443
|
+
target: event.targetTagName
|
|
444
|
+
});
|
|
445
|
+
return newEvents;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return [
|
|
449
|
+
...events,
|
|
450
|
+
event
|
|
451
|
+
];
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
window.EventRecorder = EventRecorder;
|
|
455
|
+
})();
|