@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.
@@ -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,2 @@
1
+ import { EventRecorder } from "./recorder.js";
2
+ window.EventRecorder = EventRecorder;
@@ -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
+ })();