@treelocator/runtime 0.3.2 → 0.4.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/dist/browserApi.d.ts +45 -0
- package/dist/browserApi.js +23 -1
- package/dist/components/RecordingOutline.d.ts +5 -0
- package/dist/components/RecordingOutline.js +53 -0
- package/dist/components/RecordingResults.d.ts +25 -0
- package/dist/components/RecordingResults.js +272 -0
- package/dist/components/Runtime.js +505 -70
- package/dist/dejitter/recorder.d.ts +91 -0
- package/dist/dejitter/recorder.js +904 -0
- package/dist/functions/enrichAncestrySourceMaps.js +9 -2
- package/dist/output.css +13 -0
- package/package.json +2 -2
- package/src/browserApi.ts +74 -1
- package/src/components/RecordingOutline.tsx +66 -0
- package/src/components/RecordingResults.tsx +287 -0
- package/src/components/Runtime.tsx +534 -80
- package/src/dejitter/recorder.ts +934 -0
- package/src/functions/enrichAncestrySourceMaps.ts +9 -2
- package/.turbo/turbo-build.log +0 -32
- package/.turbo/turbo-dev.log +0 -32
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-ts.log +0 -4
- package/LICENSE +0 -22
|
@@ -2,10 +2,14 @@ import { template as _$template } from "solid-js/web";
|
|
|
2
2
|
import { delegateEvents as _$delegateEvents } from "solid-js/web";
|
|
3
3
|
import { createComponent as _$createComponent } from "solid-js/web";
|
|
4
4
|
import { effect as _$effect } from "solid-js/web";
|
|
5
|
+
import { insert as _$insert } from "solid-js/web";
|
|
5
6
|
import { setAttribute as _$setAttribute } from "solid-js/web";
|
|
6
7
|
import { setStyleProperty as _$setStyleProperty } from "solid-js/web";
|
|
7
8
|
import { memo as _$memo } from "solid-js/web";
|
|
8
|
-
var _tmpl$ = /*#__PURE__*/_$template(`<div class="fixed pointer-events-auto"title="TreeLocatorJS - Component Ancestry Tracker"data-treelocator-api=window.__treelocator__ data-treelocator-help=window.__treelocator__.help() style=z-index:2147483646><
|
|
9
|
+
var _tmpl$ = /*#__PURE__*/_$template(`<div class="fixed pointer-events-auto"title="TreeLocatorJS - Component Ancestry Tracker"data-treelocator-api=window.__treelocator__ data-treelocator-help=window.__treelocator__.help() style=z-index:2147483646><style>\n @keyframes treelocator-rec-pulse \{\n 0%, 100% \{ opacity: 1; }\n 50% \{ opacity: 0.3; }\n }\n </style><div style=align-items:stretch;border-radius:27px><div aria-label="TreeLocatorJS: Get component paths using window.__treelocator__.getPath(selector)"role=button style="align-items:center;justify-content:center;border-right:1px solid rgba(0, 0, 0, 0.1)"><img alt=TreeLocatorJS width=44 height=44></div><div role=button style=align-items:center;justify-content:center></div></div><div role=note aria-label="TreeLocatorJS Developer Tool API — Run: window.__treelocator__.help() for full usage instructions. Quick start: window.__treelocator__.getPath('button.submit') returns component ancestry. window.__treelocator__.replayWithRecord('.my-element') replays recorded interactions while tracking visual changes."style=white-space:nowrap>TreeLocatorJS: Run window.__treelocator__.help() for API docs`),
|
|
10
|
+
_tmpl$2 = /*#__PURE__*/_$template(`<div style="z-index:2147483645;border-radius:50%;pointer-events:none;box-shadow:0 0 12px rgba(59, 130, 246, 0.5)">`),
|
|
11
|
+
_tmpl$3 = /*#__PURE__*/_$template(`<div style=border-radius:3px>`),
|
|
12
|
+
_tmpl$4 = /*#__PURE__*/_$template(`<div style=border-radius:50%>`);
|
|
9
13
|
import { createEffect, createSignal, onCleanup } from "solid-js";
|
|
10
14
|
import { render } from "solid-js/web";
|
|
11
15
|
import { isCombinationModifiersPressed } from "../functions/isCombinationModifiersPressed";
|
|
@@ -16,12 +20,57 @@ import { collectAncestry, formatAncestryChain } from "../functions/formatAncestr
|
|
|
16
20
|
import { enrichAncestryWithSourceMaps } from "../functions/enrichAncestrySourceMaps";
|
|
17
21
|
import { createTreeNode } from "../adapters/createTreeNode";
|
|
18
22
|
import treeIconUrl from "../_generated_tree_icon";
|
|
23
|
+
import { createDejitterRecorder } from "../dejitter/recorder";
|
|
24
|
+
import { RecordingOutline } from "./RecordingOutline";
|
|
25
|
+
import { RecordingResults } from "./RecordingResults";
|
|
19
26
|
function Runtime(props) {
|
|
20
27
|
const [holdingModKey, setHoldingModKey] = createSignal(false);
|
|
21
28
|
const [currentElement, setCurrentElement] = createSignal(null);
|
|
22
29
|
const [toastMessage, setToastMessage] = createSignal(null);
|
|
23
30
|
const [locatorActive, setLocatorActive] = createSignal(false);
|
|
24
|
-
|
|
31
|
+
|
|
32
|
+
// Recording state machine: idle -> selecting -> recording -> results -> idle
|
|
33
|
+
|
|
34
|
+
// --- localStorage persistence ---
|
|
35
|
+
const STORAGE_KEY = '__treelocator_recording__';
|
|
36
|
+
function loadFromStorage() {
|
|
37
|
+
try {
|
|
38
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
39
|
+
if (raw) return JSON.parse(raw);
|
|
40
|
+
} catch {}
|
|
41
|
+
return {
|
|
42
|
+
last: null,
|
|
43
|
+
previous: null
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function saveToStorage(current) {
|
|
47
|
+
try {
|
|
48
|
+
const stored = loadFromStorage();
|
|
49
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
50
|
+
last: current,
|
|
51
|
+
previous: stored.last
|
|
52
|
+
}));
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Restore last results on mount
|
|
57
|
+
const restored = loadFromStorage();
|
|
58
|
+
const restoredLast = restored.last;
|
|
59
|
+
const [recordingState, setRecordingState] = createSignal(restoredLast ? 'results' : 'idle');
|
|
60
|
+
const [recordedElement, setRecordedElement] = createSignal(null);
|
|
61
|
+
const [recordingFindings, setRecordingFindings] = createSignal(restoredLast?.findings ?? []);
|
|
62
|
+
const [recordingSummary, setRecordingSummary] = createSignal(restoredLast?.summary ?? null);
|
|
63
|
+
const [interactionLog, setInteractionLog] = createSignal(restoredLast?.interactions ?? []);
|
|
64
|
+
const [recordingData, setRecordingData] = createSignal(restoredLast?.data ?? null);
|
|
65
|
+
const [recordingElementPath, setRecordingElementPath] = createSignal(restoredLast?.elementPath ?? "");
|
|
66
|
+
const [replayBox, setReplayBox] = createSignal(null);
|
|
67
|
+
const [replaying, setReplaying] = createSignal(false);
|
|
68
|
+
const [viewingPrevious, setViewingPrevious] = createSignal(false);
|
|
69
|
+
let dejitterInstance = null;
|
|
70
|
+
let interactionClickHandler = null;
|
|
71
|
+
let recordingStartTime = 0;
|
|
72
|
+
let replayTimerId = null;
|
|
73
|
+
const isActive = () => (holdingModKey() || locatorActive() || recordingState() === 'selecting') && currentElement();
|
|
25
74
|
createEffect(() => {
|
|
26
75
|
if (isActive()) {
|
|
27
76
|
document.body.classList.add("locatorjs-active-pointer");
|
|
@@ -29,6 +78,12 @@ function Runtime(props) {
|
|
|
29
78
|
document.body.classList.remove("locatorjs-active-pointer");
|
|
30
79
|
}
|
|
31
80
|
});
|
|
81
|
+
|
|
82
|
+
// Expose replay functions on the browser API
|
|
83
|
+
if (typeof window !== "undefined" && window.__treelocator__) {
|
|
84
|
+
window.__treelocator__.replay = () => replayRecording();
|
|
85
|
+
window.__treelocator__.replayWithRecord = elementOrSelector => replayWithRecord(elementOrSelector);
|
|
86
|
+
}
|
|
32
87
|
function keyUpListener(e) {
|
|
33
88
|
setHoldingModKey(isCombinationModifiersPressed(e));
|
|
34
89
|
}
|
|
@@ -39,80 +94,353 @@ function Runtime(props) {
|
|
|
39
94
|
// Update modifier state from mouse events - more reliable than keydown/keyup
|
|
40
95
|
setHoldingModKey(e.altKey);
|
|
41
96
|
}
|
|
42
|
-
function
|
|
43
|
-
// Also update modifier state
|
|
44
|
-
setHoldingModKey(e.altKey);
|
|
45
|
-
|
|
46
|
-
// Use elementsFromPoint to find elements including ones with pointer-events-none
|
|
97
|
+
function findElementAtPoint(e) {
|
|
47
98
|
const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
|
|
48
|
-
|
|
49
|
-
// Find the topmost element with locator data for highlighting
|
|
50
|
-
let element = null;
|
|
51
99
|
for (const el of elementsAtPoint) {
|
|
52
|
-
if (isLocatorsOwnElement(el))
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
100
|
+
if (isLocatorsOwnElement(el)) continue;
|
|
55
101
|
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
56
102
|
const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
|
|
57
|
-
if (withLocator)
|
|
58
|
-
element = withLocator;
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
103
|
+
if (withLocator) return withLocator;
|
|
61
104
|
}
|
|
62
105
|
}
|
|
63
|
-
|
|
64
106
|
// Fallback to e.target
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
107
|
+
const target = e.target;
|
|
108
|
+
if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
|
|
109
|
+
const el = target instanceof SVGElement ? target.closest('[data-locatorjs-id], [data-locatorjs]') ?? target.closest('svg') ?? target : target;
|
|
110
|
+
if (el && !isLocatorsOwnElement(el)) return el;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Recording lifecycle ---
|
|
116
|
+
|
|
117
|
+
function handleRecordClick() {
|
|
118
|
+
switch (recordingState()) {
|
|
119
|
+
case 'idle':
|
|
120
|
+
setRecordingState('selecting');
|
|
121
|
+
break;
|
|
122
|
+
case 'selecting':
|
|
123
|
+
setRecordingState('idle');
|
|
124
|
+
break;
|
|
125
|
+
case 'recording':
|
|
126
|
+
stopRecording();
|
|
127
|
+
break;
|
|
128
|
+
case 'results':
|
|
129
|
+
dismissResults();
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function startRecording(element) {
|
|
134
|
+
element.setAttribute('data-treelocator-recording', 'true');
|
|
135
|
+
setRecordedElement(element);
|
|
136
|
+
dejitterInstance = createDejitterRecorder();
|
|
137
|
+
dejitterInstance.configure({
|
|
138
|
+
selector: '[data-treelocator-recording]',
|
|
139
|
+
props: ['opacity', 'transform', 'boundingRect', 'width', 'height'],
|
|
140
|
+
sampleRate: 15,
|
|
141
|
+
maxDuration: 30000,
|
|
142
|
+
idleTimeout: 0,
|
|
143
|
+
mutations: true
|
|
144
|
+
});
|
|
145
|
+
dejitterInstance.start();
|
|
146
|
+
startInteractionTracker();
|
|
147
|
+
setRecordingState('recording');
|
|
148
|
+
}
|
|
149
|
+
function stopRecording() {
|
|
150
|
+
if (!dejitterInstance) return;
|
|
151
|
+
dejitterInstance.stop();
|
|
152
|
+
const findings = dejitterInstance.findings(true);
|
|
153
|
+
const summary = dejitterInstance.summary(true);
|
|
154
|
+
const data = dejitterInstance.getData();
|
|
155
|
+
|
|
156
|
+
// Collect ancestry path from treelocator before clearing
|
|
157
|
+
const el = recordedElement();
|
|
158
|
+
let elementPath = "";
|
|
159
|
+
if (el) {
|
|
160
|
+
const treeNode = createTreeNode(el, props.adapterId);
|
|
161
|
+
if (treeNode) {
|
|
162
|
+
const ancestry = collectAncestry(treeNode);
|
|
163
|
+
elementPath = formatAncestryChain(ancestry);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
setRecordingFindings(findings);
|
|
167
|
+
setRecordingSummary(summary);
|
|
168
|
+
setRecordingData(data);
|
|
169
|
+
setRecordingElementPath(elementPath);
|
|
170
|
+
stopInteractionTracker();
|
|
171
|
+
|
|
172
|
+
// Persist to localStorage (moves previous "last" to "previous")
|
|
173
|
+
saveToStorage({
|
|
174
|
+
findings,
|
|
175
|
+
summary,
|
|
176
|
+
data,
|
|
177
|
+
elementPath,
|
|
178
|
+
interactions: interactionLog()
|
|
179
|
+
});
|
|
180
|
+
el?.removeAttribute('data-treelocator-recording');
|
|
181
|
+
setRecordingState('results');
|
|
182
|
+
dejitterInstance = null;
|
|
183
|
+
}
|
|
184
|
+
function replayRecording() {
|
|
185
|
+
const events = interactionLog();
|
|
186
|
+
if (events.length === 0) return;
|
|
187
|
+
stopReplay();
|
|
188
|
+
setReplaying(true);
|
|
189
|
+
let eventIdx = 0;
|
|
190
|
+
function scheduleNext() {
|
|
191
|
+
if (eventIdx >= events.length) {
|
|
192
|
+
stopReplay();
|
|
193
|
+
return;
|
|
69
194
|
}
|
|
195
|
+
const evt = events[eventIdx];
|
|
196
|
+
const delay = eventIdx === 0 ? 100 : evt.t - events[eventIdx - 1].t;
|
|
197
|
+
replayTimerId = window.setTimeout(() => {
|
|
198
|
+
// Show click indicator
|
|
199
|
+
setReplayBox({
|
|
200
|
+
x: evt.x - 12,
|
|
201
|
+
y: evt.y - 12,
|
|
202
|
+
w: 24,
|
|
203
|
+
h: 24
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Dispatch a real click at the recorded position
|
|
207
|
+
const target = document.elementFromPoint(evt.x, evt.y);
|
|
208
|
+
if (target) {
|
|
209
|
+
target.dispatchEvent(new MouseEvent('click', {
|
|
210
|
+
bubbles: true,
|
|
211
|
+
cancelable: true,
|
|
212
|
+
clientX: evt.x,
|
|
213
|
+
clientY: evt.y,
|
|
214
|
+
view: window
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Clear click indicator after a short flash
|
|
219
|
+
window.setTimeout(() => setReplayBox(null), 200);
|
|
220
|
+
eventIdx++;
|
|
221
|
+
scheduleNext();
|
|
222
|
+
}, Math.max(delay, 50));
|
|
70
223
|
}
|
|
71
|
-
|
|
224
|
+
scheduleNext();
|
|
225
|
+
}
|
|
226
|
+
function stopReplay() {
|
|
227
|
+
if (replayTimerId) {
|
|
228
|
+
clearTimeout(replayTimerId);
|
|
229
|
+
replayTimerId = null;
|
|
230
|
+
}
|
|
231
|
+
setReplaying(false);
|
|
232
|
+
setReplayBox(null);
|
|
233
|
+
}
|
|
234
|
+
function replayWithRecord(elementOrSelector) {
|
|
235
|
+
// Resolve element
|
|
236
|
+
let element;
|
|
237
|
+
if (typeof elementOrSelector === 'string') {
|
|
238
|
+
const found = document.querySelector(elementOrSelector);
|
|
239
|
+
element = found instanceof HTMLElement ? found : null;
|
|
240
|
+
} else {
|
|
241
|
+
element = elementOrSelector;
|
|
242
|
+
}
|
|
243
|
+
if (!element) return Promise.resolve(null);
|
|
244
|
+
|
|
245
|
+
// Get stored interactions to replay
|
|
246
|
+
const stored = loadFromStorage();
|
|
247
|
+
const events = stored.last?.interactions ?? interactionLog();
|
|
248
|
+
if (events.length === 0) return Promise.resolve(null);
|
|
249
|
+
return new Promise(resolve => {
|
|
250
|
+
// Start recording on the element
|
|
251
|
+
element.setAttribute('data-treelocator-recording', 'true');
|
|
252
|
+
setRecordedElement(element);
|
|
253
|
+
dejitterInstance = createDejitterRecorder();
|
|
254
|
+
dejitterInstance.configure({
|
|
255
|
+
selector: '[data-treelocator-recording]',
|
|
256
|
+
props: ['opacity', 'transform', 'boundingRect', 'width', 'height'],
|
|
257
|
+
sampleRate: 15,
|
|
258
|
+
maxDuration: 30000,
|
|
259
|
+
idleTimeout: 0,
|
|
260
|
+
mutations: true
|
|
261
|
+
});
|
|
262
|
+
dejitterInstance.start();
|
|
263
|
+
setRecordingState('recording');
|
|
264
|
+
setReplaying(true);
|
|
265
|
+
let eventIdx = 0;
|
|
266
|
+
function finishRecording() {
|
|
267
|
+
setReplaying(false);
|
|
268
|
+
setReplayBox(null);
|
|
269
|
+
if (!dejitterInstance) {
|
|
270
|
+
resolve(null);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
dejitterInstance.stop();
|
|
274
|
+
const findings = dejitterInstance.findings(true);
|
|
275
|
+
const summary = dejitterInstance.summary(true);
|
|
276
|
+
const data = dejitterInstance.getData();
|
|
277
|
+
const el = recordedElement();
|
|
278
|
+
let elementPath = "";
|
|
279
|
+
if (el) {
|
|
280
|
+
const treeNode = createTreeNode(el, props.adapterId);
|
|
281
|
+
if (treeNode) {
|
|
282
|
+
const ancestry = collectAncestry(treeNode);
|
|
283
|
+
elementPath = formatAncestryChain(ancestry);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
setRecordingFindings(findings);
|
|
287
|
+
setRecordingSummary(summary);
|
|
288
|
+
setRecordingData(data);
|
|
289
|
+
setRecordingElementPath(elementPath);
|
|
290
|
+
setInteractionLog(events);
|
|
291
|
+
saveToStorage({
|
|
292
|
+
findings,
|
|
293
|
+
summary,
|
|
294
|
+
data,
|
|
295
|
+
elementPath,
|
|
296
|
+
interactions: events
|
|
297
|
+
});
|
|
298
|
+
el?.removeAttribute('data-treelocator-recording');
|
|
299
|
+
setRecordingState('results');
|
|
300
|
+
dejitterInstance = null;
|
|
301
|
+
resolve({
|
|
302
|
+
path: elementPath,
|
|
303
|
+
findings,
|
|
304
|
+
summary,
|
|
305
|
+
data,
|
|
306
|
+
interactions: events
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function scheduleNext() {
|
|
310
|
+
if (eventIdx >= events.length) {
|
|
311
|
+
// Wait for CSS transitions to settle before stopping recording
|
|
312
|
+
replayTimerId = window.setTimeout(finishRecording, 500);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const evt = events[eventIdx];
|
|
316
|
+
const delay = eventIdx === 0 ? 100 : evt.t - events[eventIdx - 1].t;
|
|
317
|
+
replayTimerId = window.setTimeout(() => {
|
|
318
|
+
setReplayBox({
|
|
319
|
+
x: evt.x - 12,
|
|
320
|
+
y: evt.y - 12,
|
|
321
|
+
w: 24,
|
|
322
|
+
h: 24
|
|
323
|
+
});
|
|
324
|
+
const target = document.elementFromPoint(evt.x, evt.y);
|
|
325
|
+
if (target) {
|
|
326
|
+
target.dispatchEvent(new MouseEvent('click', {
|
|
327
|
+
bubbles: true,
|
|
328
|
+
cancelable: true,
|
|
329
|
+
clientX: evt.x,
|
|
330
|
+
clientY: evt.y,
|
|
331
|
+
view: window
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
window.setTimeout(() => setReplayBox(null), 200);
|
|
335
|
+
eventIdx++;
|
|
336
|
+
scheduleNext();
|
|
337
|
+
}, Math.max(delay, 50));
|
|
338
|
+
}
|
|
339
|
+
scheduleNext();
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
function dismissResults() {
|
|
343
|
+
stopReplay();
|
|
344
|
+
setRecordingFindings([]);
|
|
345
|
+
setRecordingSummary(null);
|
|
346
|
+
setRecordingData(null);
|
|
347
|
+
setRecordingElementPath("");
|
|
348
|
+
setInteractionLog([]);
|
|
349
|
+
setRecordedElement(null);
|
|
350
|
+
setViewingPrevious(false);
|
|
351
|
+
setRecordingState('idle');
|
|
352
|
+
}
|
|
353
|
+
function hasPreviousRecording() {
|
|
354
|
+
return loadFromStorage().previous !== null;
|
|
355
|
+
}
|
|
356
|
+
function loadPreviousRecording() {
|
|
357
|
+
const stored = loadFromStorage();
|
|
358
|
+
if (!stored.previous) return;
|
|
359
|
+
const prev = stored.previous;
|
|
360
|
+
setRecordingFindings(prev.findings);
|
|
361
|
+
setRecordingSummary(prev.summary);
|
|
362
|
+
setRecordingData(prev.data);
|
|
363
|
+
setRecordingElementPath(prev.elementPath);
|
|
364
|
+
setInteractionLog(prev.interactions);
|
|
365
|
+
setViewingPrevious(true);
|
|
366
|
+
setRecordingState('results');
|
|
367
|
+
}
|
|
368
|
+
function loadLatestRecording() {
|
|
369
|
+
const stored = loadFromStorage();
|
|
370
|
+
if (!stored.last) return;
|
|
371
|
+
const last = stored.last;
|
|
372
|
+
setRecordingFindings(last.findings);
|
|
373
|
+
setRecordingSummary(last.summary);
|
|
374
|
+
setRecordingData(last.data);
|
|
375
|
+
setRecordingElementPath(last.elementPath);
|
|
376
|
+
setInteractionLog(last.interactions);
|
|
377
|
+
setViewingPrevious(false);
|
|
378
|
+
setRecordingState('results');
|
|
379
|
+
}
|
|
380
|
+
function startInteractionTracker() {
|
|
381
|
+
recordingStartTime = performance.now();
|
|
382
|
+
setInteractionLog([]);
|
|
383
|
+
interactionClickHandler = e => {
|
|
384
|
+
if (isLocatorsOwnElement(e.target)) return;
|
|
385
|
+
const el = e.target;
|
|
386
|
+
const tag = el.tagName?.toLowerCase() || 'unknown';
|
|
387
|
+
const id = el.id ? '#' + el.id : '';
|
|
388
|
+
const cls = el.className && typeof el.className === 'string' ? '.' + el.className.split(' ')[0] : '';
|
|
389
|
+
setInteractionLog(prev => [...prev, {
|
|
390
|
+
t: Math.round(performance.now() - recordingStartTime),
|
|
391
|
+
type: 'click',
|
|
392
|
+
target: `${tag}${id}${cls}`,
|
|
393
|
+
x: e.clientX,
|
|
394
|
+
y: e.clientY
|
|
395
|
+
}]);
|
|
396
|
+
};
|
|
397
|
+
document.addEventListener('click', interactionClickHandler, {
|
|
398
|
+
capture: true
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
function stopInteractionTracker() {
|
|
402
|
+
if (interactionClickHandler) {
|
|
403
|
+
document.removeEventListener('click', interactionClickHandler, {
|
|
404
|
+
capture: true
|
|
405
|
+
});
|
|
406
|
+
interactionClickHandler = null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function mouseOverListener(e) {
|
|
410
|
+
setHoldingModKey(e.altKey);
|
|
411
|
+
|
|
412
|
+
// Don't update hovered element while recording -- highlight is sticky
|
|
413
|
+
if (recordingState() === 'recording') return;
|
|
414
|
+
const element = findElementAtPoint(e);
|
|
415
|
+
if (element) {
|
|
72
416
|
setCurrentElement(element);
|
|
73
417
|
}
|
|
74
418
|
}
|
|
75
419
|
function mouseDownUpListener(e) {
|
|
76
|
-
// Update modifier state
|
|
77
420
|
setHoldingModKey(e.altKey);
|
|
78
|
-
if (e.altKey || locatorActive()) {
|
|
421
|
+
if (e.altKey || locatorActive() || recordingState() === 'selecting') {
|
|
79
422
|
e.preventDefault();
|
|
80
423
|
e.stopPropagation();
|
|
81
424
|
}
|
|
82
425
|
}
|
|
83
426
|
function clickListener(e) {
|
|
84
|
-
//
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
|
|
92
|
-
|
|
93
|
-
// Find the topmost element with locator data
|
|
94
|
-
let element = null;
|
|
95
|
-
for (const el of elementsAtPoint) {
|
|
96
|
-
if (isLocatorsOwnElement(el)) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (el instanceof HTMLElement || el instanceof SVGElement) {
|
|
100
|
-
// Check if this element or its closest ancestor has locator data
|
|
101
|
-
const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
|
|
102
|
-
if (withLocator) {
|
|
103
|
-
element = withLocator;
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
427
|
+
// Handle recording element selection
|
|
428
|
+
if (recordingState() === 'selecting') {
|
|
429
|
+
e.preventDefault();
|
|
430
|
+
e.stopPropagation();
|
|
431
|
+
const element = findElementAtPoint(e);
|
|
432
|
+
if (element && !isLocatorsOwnElement(element)) {
|
|
433
|
+
startRecording(element);
|
|
106
434
|
}
|
|
435
|
+
return;
|
|
107
436
|
}
|
|
108
437
|
|
|
109
|
-
//
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
element = target instanceof SVGElement ? target.closest('[data-locatorjs-id], [data-locatorjs]') ?? target.closest('svg') ?? target : target;
|
|
114
|
-
}
|
|
438
|
+
// During recording, let clicks pass through (tracked by interaction logger)
|
|
439
|
+
if (recordingState() === 'recording') return;
|
|
440
|
+
if (!e.altKey && !isCombinationModifiersPressed(e) && !locatorActive()) {
|
|
441
|
+
return;
|
|
115
442
|
}
|
|
443
|
+
const element = findElementAtPoint(e);
|
|
116
444
|
if (!element) {
|
|
117
445
|
return;
|
|
118
446
|
}
|
|
@@ -213,6 +541,62 @@ function Runtime(props) {
|
|
|
213
541
|
get targets() {
|
|
214
542
|
return props.targets;
|
|
215
543
|
}
|
|
544
|
+
}) : null), _$memo(() => _$memo(() => !!(recordingState() === 'recording' && recordedElement()))() ? _$createComponent(RecordingOutline, {
|
|
545
|
+
get element() {
|
|
546
|
+
return recordedElement();
|
|
547
|
+
}
|
|
548
|
+
}) : null), _$memo(() => _$memo(() => !!replayBox())() ? (() => {
|
|
549
|
+
var _el$8 = _tmpl$2();
|
|
550
|
+
_$setStyleProperty(_el$8, "position", "fixed");
|
|
551
|
+
_$setStyleProperty(_el$8, "background", "rgba(59, 130, 246, 0.4)");
|
|
552
|
+
_$setStyleProperty(_el$8, "border", "2px solid #3b82f6");
|
|
553
|
+
_$effect(_p$ => {
|
|
554
|
+
var _v$4 = replayBox().x + "px",
|
|
555
|
+
_v$5 = replayBox().y + "px",
|
|
556
|
+
_v$6 = replayBox().w + "px",
|
|
557
|
+
_v$7 = replayBox().h + "px";
|
|
558
|
+
_v$4 !== _p$.e && _$setStyleProperty(_el$8, "left", _p$.e = _v$4);
|
|
559
|
+
_v$5 !== _p$.t && _$setStyleProperty(_el$8, "top", _p$.t = _v$5);
|
|
560
|
+
_v$6 !== _p$.a && _$setStyleProperty(_el$8, "width", _p$.a = _v$6);
|
|
561
|
+
_v$7 !== _p$.o && _$setStyleProperty(_el$8, "height", _p$.o = _v$7);
|
|
562
|
+
return _p$;
|
|
563
|
+
}, {
|
|
564
|
+
e: undefined,
|
|
565
|
+
t: undefined,
|
|
566
|
+
a: undefined,
|
|
567
|
+
o: undefined
|
|
568
|
+
});
|
|
569
|
+
return _el$8;
|
|
570
|
+
})() : null), _$memo(() => _$memo(() => recordingState() === 'results')() ? _$createComponent(RecordingResults, {
|
|
571
|
+
get findings() {
|
|
572
|
+
return recordingFindings();
|
|
573
|
+
},
|
|
574
|
+
get summary() {
|
|
575
|
+
return recordingSummary();
|
|
576
|
+
},
|
|
577
|
+
get data() {
|
|
578
|
+
return recordingData();
|
|
579
|
+
},
|
|
580
|
+
get elementPath() {
|
|
581
|
+
return recordingElementPath();
|
|
582
|
+
},
|
|
583
|
+
get interactions() {
|
|
584
|
+
return interactionLog();
|
|
585
|
+
},
|
|
586
|
+
onDismiss: dismissResults,
|
|
587
|
+
onReplay: replayRecording,
|
|
588
|
+
get replaying() {
|
|
589
|
+
return replaying();
|
|
590
|
+
},
|
|
591
|
+
onToast: setToastMessage,
|
|
592
|
+
get hasPrevious() {
|
|
593
|
+
return _$memo(() => !!!viewingPrevious())() && hasPreviousRecording();
|
|
594
|
+
},
|
|
595
|
+
onLoadPrevious: loadPreviousRecording,
|
|
596
|
+
get hasNext() {
|
|
597
|
+
return viewingPrevious();
|
|
598
|
+
},
|
|
599
|
+
onLoadNext: loadLatestRecording
|
|
216
600
|
}) : null), _$memo(() => _$memo(() => !!toastMessage())() && _$createComponent(Toast, {
|
|
217
601
|
get message() {
|
|
218
602
|
return toastMessage();
|
|
@@ -221,26 +605,77 @@ function Runtime(props) {
|
|
|
221
605
|
})), (() => {
|
|
222
606
|
var _el$ = _tmpl$(),
|
|
223
607
|
_el$2 = _el$.firstChild,
|
|
224
|
-
_el$3 = _el$2.
|
|
225
|
-
_el$4 = _el$
|
|
608
|
+
_el$3 = _el$2.nextSibling,
|
|
609
|
+
_el$4 = _el$3.firstChild,
|
|
610
|
+
_el$5 = _el$4.firstChild,
|
|
611
|
+
_el$6 = _el$4.nextSibling,
|
|
612
|
+
_el$7 = _el$3.nextSibling;
|
|
226
613
|
_$setStyleProperty(_el$, "bottom", "20px");
|
|
227
614
|
_$setStyleProperty(_el$, "right", "20px");
|
|
228
|
-
_el$
|
|
229
|
-
_el$
|
|
230
|
-
_el$
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
_$
|
|
235
|
-
_$setStyleProperty(_el$4, "
|
|
236
|
-
_$setStyleProperty(_el$4, "
|
|
237
|
-
_$setStyleProperty(_el$4, "
|
|
238
|
-
_$setStyleProperty(_el$4, "
|
|
239
|
-
_$setStyleProperty(_el$4, "margin", "-1px");
|
|
615
|
+
_$setStyleProperty(_el$3, "display", "flex");
|
|
616
|
+
_$setStyleProperty(_el$3, "overflow", "hidden");
|
|
617
|
+
_$setStyleProperty(_el$3, "transition", "box-shadow 0.2s ease-in-out");
|
|
618
|
+
_el$4.$$click = () => setLocatorActive(!locatorActive());
|
|
619
|
+
_el$4.addEventListener("mouseleave", e => e.currentTarget.style.background = "#ffffff");
|
|
620
|
+
_el$4.addEventListener("mouseenter", e => e.currentTarget.style.background = "#f0f0f0");
|
|
621
|
+
_$setStyleProperty(_el$4, "width", "54px");
|
|
622
|
+
_$setStyleProperty(_el$4, "height", "54px");
|
|
623
|
+
_$setStyleProperty(_el$4, "background", "#ffffff");
|
|
624
|
+
_$setStyleProperty(_el$4, "display", "flex");
|
|
625
|
+
_$setStyleProperty(_el$4, "cursor", "pointer");
|
|
240
626
|
_$setStyleProperty(_el$4, "overflow", "hidden");
|
|
241
|
-
_$setStyleProperty(_el$4, "
|
|
242
|
-
_$
|
|
243
|
-
|
|
627
|
+
_$setStyleProperty(_el$4, "transition", "background 0.15s ease-in-out");
|
|
628
|
+
_$setAttribute(_el$5, "src", treeIconUrl);
|
|
629
|
+
_el$6.$$click = handleRecordClick;
|
|
630
|
+
_el$6.addEventListener("mouseleave", e => {
|
|
631
|
+
if (recordingState() !== 'recording') e.currentTarget.style.background = "#ffffff";
|
|
632
|
+
});
|
|
633
|
+
_el$6.addEventListener("mouseenter", e => {
|
|
634
|
+
if (recordingState() !== 'recording') e.currentTarget.style.background = "#f0f0f0";
|
|
635
|
+
});
|
|
636
|
+
_$setStyleProperty(_el$6, "width", "54px");
|
|
637
|
+
_$setStyleProperty(_el$6, "height", "54px");
|
|
638
|
+
_$setStyleProperty(_el$6, "display", "flex");
|
|
639
|
+
_$setStyleProperty(_el$6, "cursor", "pointer");
|
|
640
|
+
_$setStyleProperty(_el$6, "transition", "background 0.15s ease-in-out");
|
|
641
|
+
_$insert(_el$6, (() => {
|
|
642
|
+
var _c$ = _$memo(() => recordingState() === 'recording');
|
|
643
|
+
return () => _c$() ? (() => {
|
|
644
|
+
var _el$9 = _tmpl$3();
|
|
645
|
+
_$setStyleProperty(_el$9, "width", "18px");
|
|
646
|
+
_$setStyleProperty(_el$9, "height", "18px");
|
|
647
|
+
_$setStyleProperty(_el$9, "background", "#fff");
|
|
648
|
+
return _el$9;
|
|
649
|
+
})() : (() => {
|
|
650
|
+
var _el$0 = _tmpl$4();
|
|
651
|
+
_$setStyleProperty(_el$0, "width", "18px");
|
|
652
|
+
_$setStyleProperty(_el$0, "height", "18px");
|
|
653
|
+
_$setStyleProperty(_el$0, "background", "#ef4444");
|
|
654
|
+
_$effect(_$p => _$setStyleProperty(_el$0, "animation", recordingState() === 'selecting' ? "treelocator-rec-pulse 1s ease-in-out infinite" : "none"));
|
|
655
|
+
return _el$0;
|
|
656
|
+
})();
|
|
657
|
+
})());
|
|
658
|
+
_$setStyleProperty(_el$7, "position", "absolute");
|
|
659
|
+
_$setStyleProperty(_el$7, "width", "1px");
|
|
660
|
+
_$setStyleProperty(_el$7, "height", "1px");
|
|
661
|
+
_$setStyleProperty(_el$7, "padding", "0");
|
|
662
|
+
_$setStyleProperty(_el$7, "margin", "-1px");
|
|
663
|
+
_$setStyleProperty(_el$7, "overflow", "hidden");
|
|
664
|
+
_$setStyleProperty(_el$7, "clip", "rect(0,0,0,0)");
|
|
665
|
+
_$setStyleProperty(_el$7, "border", "0");
|
|
666
|
+
_$effect(_p$ => {
|
|
667
|
+
var _v$ = locatorActive() ? "0 0 0 3px #3b82f6, 0 4px 14px rgba(0, 0, 0, 0.25)" : recordingState() === 'selecting' ? "0 0 0 3px #3b82f6, 0 4px 14px rgba(0, 0, 0, 0.25)" : recordingState() === 'recording' ? "0 0 0 3px #ef4444, 0 4px 14px rgba(0, 0, 0, 0.25)" : "0 4px 14px rgba(0, 0, 0, 0.25)",
|
|
668
|
+
_v$2 = recordingState() === 'recording' ? "#ef4444" : "#ffffff",
|
|
669
|
+
_v$3 = recordingState() === 'idle' ? "Record element changes. API: window.__treelocator__.replayWithRecord(selector)" : recordingState() === 'selecting' ? "Cancel recording selection" : recordingState() === 'recording' ? "Stop recording" : "Dismiss results";
|
|
670
|
+
_v$ !== _p$.e && _$setStyleProperty(_el$3, "box-shadow", _p$.e = _v$);
|
|
671
|
+
_v$2 !== _p$.t && _$setStyleProperty(_el$6, "background", _p$.t = _v$2);
|
|
672
|
+
_v$3 !== _p$.a && _$setAttribute(_el$6, "aria-label", _p$.a = _v$3);
|
|
673
|
+
return _p$;
|
|
674
|
+
}, {
|
|
675
|
+
e: undefined,
|
|
676
|
+
t: undefined,
|
|
677
|
+
a: undefined
|
|
678
|
+
});
|
|
244
679
|
return _el$;
|
|
245
680
|
})()];
|
|
246
681
|
}
|