119 lines
4.5 KiB
TypeScript
119 lines
4.5 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { finder } from '@medv/finder';
|
|
|
|
export function PickingHelper() {
|
|
const [pickingMode, setPickingMode] = useState<'click' | 'scroll' | null>(null);
|
|
const [hoveredElement, setHoveredElement] = useState<HTMLElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
const handleMessage = (e: MessageEvent) => {
|
|
if (e.data.type === 'START_PICKING') {
|
|
setPickingMode(e.data.mode);
|
|
} else if (e.data.type === 'STOP_PICKING') {
|
|
setPickingMode(null);
|
|
setHoveredElement(null);
|
|
} else if (e.data.type === 'SET_HOVER_SELECTOR') {
|
|
const selector = e.data.selector;
|
|
if (selector) {
|
|
const el = document.querySelector(selector) as HTMLElement;
|
|
setHoveredElement(el || null);
|
|
} else {
|
|
setHoveredElement(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', handleMessage);
|
|
return () => window.removeEventListener('message', handleMessage);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!pickingMode) return;
|
|
|
|
const handleMouseOver = (e: MouseEvent) => {
|
|
const target = e.target as HTMLElement;
|
|
if (target.closest('.record-mode-ignore') || target.closest('.feedback-ui-ignore')) return;
|
|
setHoveredElement(target);
|
|
};
|
|
|
|
const handleClick = (e: MouseEvent) => {
|
|
if (hoveredElement) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const selector = finder(hoveredElement, {
|
|
root: document.body,
|
|
seedMinLength: 3,
|
|
optimizedMinLength: 2,
|
|
className: (name) =>
|
|
!name.startsWith('record-mode-') &&
|
|
!name.startsWith('feedback-') &&
|
|
!name.includes('[') &&
|
|
!name.includes('/') &&
|
|
!name.match(/^[a-z]-[0-9]/) &&
|
|
!name.match(/[0-9]{4,}/), // Avoid dynamic IDs in classnames
|
|
idName: (name) => !name.startsWith('__next') && !name.includes(':') && !name.match(/[0-9]{5,}/),
|
|
});
|
|
const rect = hoveredElement.getBoundingClientRect();
|
|
|
|
window.parent.postMessage({
|
|
type: 'ELEMENT_SELECTED',
|
|
selector,
|
|
rect: {
|
|
x: rect.left,
|
|
y: rect.top,
|
|
width: rect.width,
|
|
height: rect.height
|
|
},
|
|
tagName: hoveredElement.tagName.toLowerCase()
|
|
}, '*');
|
|
|
|
setPickingMode(null);
|
|
setHoveredElement(null);
|
|
}
|
|
};
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
setPickingMode(null);
|
|
setHoveredElement(null);
|
|
window.parent.postMessage({ type: 'PICKING_CANCELLED' }, '*');
|
|
}
|
|
};
|
|
|
|
window.addEventListener('mouseover', handleMouseOver);
|
|
window.addEventListener('click', handleClick, true);
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
return () => {
|
|
window.removeEventListener('mouseover', handleMouseOver);
|
|
window.removeEventListener('click', handleClick, true);
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
};
|
|
}, [pickingMode, hoveredElement]);
|
|
|
|
if (!hoveredElement) return null;
|
|
// Don't show highlight if we are in picking mode but NOT hovering anything (handled by logic above)
|
|
// but DO show if we have a hoveredElement (from message or mouseover)
|
|
|
|
const rect = hoveredElement.getBoundingClientRect();
|
|
|
|
return (
|
|
<div
|
|
className="fixed pointer-events-none border-2 border-[#82ed20] bg-[#82ed20]/15 transition-all z-[9999] shadow-[0_0_20px_rgba(130,237,32,0.3)] rounded-sm"
|
|
style={{
|
|
top: rect.top,
|
|
left: rect.left,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
}}
|
|
>
|
|
<div className="absolute top-0 right-0 bg-[#82ed20] text-black text-[10px] font-black px-1.5 py-1 transform -translate-y-full uppercase tracking-tighter shadow-xl">
|
|
{hoveredElement.tagName.toLowerCase()}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|