import Agent from 'react-devtools-shared/src/backend/agent';
import {hideOverlay, showOverlay} from './Highlighter';
import {isReactNativeEnvironment} from 'react-devtools-shared/src/backend/utils';
import type {HostInstance} from 'react-devtools-shared/src/backend/types';
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
import type {RendererInterface} from '../../types';
let iframesListeningTo: Set<HTMLIFrameElement> = new Set();
let inspectOnlySuspenseNodes = false;
export default function setupHighlighter(
bridge: BackendBridge,
agent: Agent,
): void {
bridge.addListener('clearHostInstanceHighlight', clearHostInstanceHighlight);
bridge.addListener('highlightHostInstance', highlightHostInstance);
bridge.addListener('highlightHostInstances', highlightHostInstances);
bridge.addListener('scrollToHostInstance', scrollToHostInstance);
bridge.addListener('shutdown', stopInspectingHost);
bridge.addListener('startInspectingHost', startInspectingHost);
bridge.addListener('stopInspectingHost', stopInspectingHost);
bridge.addListener('scrollTo', scrollDocumentTo);
bridge.addListener('requestScrollPosition', sendScroll);
let applyingScroll = false;
function scrollDocumentTo({
left,
top,
right,
bottom,
}: {
left: number,
top: number,
right: number,
bottom: number,
}) {
if (isReactNativeEnvironment()) {
return;
}
if (
left === Math.round(window.scrollX) &&
top === Math.round(window.scrollY)
) {
return;
}
applyingScroll = true;
window.scrollTo({
top: top,
left: left,
behavior: 'smooth',
});
}
let scrollTimer = null;
function sendScroll() {
if (isReactNativeEnvironment()) {
return;
}
if (scrollTimer) {
clearTimeout(scrollTimer);
scrollTimer = null;
}
if (applyingScroll) {
return;
}
const left = window.scrollX;
const top = window.scrollY;
const right = left + window.innerWidth;
const bottom = top + window.innerHeight;
bridge.send('scrollTo', {left, top, right, bottom});
}
function scrollEnd() {
sendScroll();
applyingScroll = false;
}
if (
typeof document === 'object' &&
typeof document.addEventListener === 'function'
) {
document.addEventListener('scroll', () => {
if (!scrollTimer) {
scrollTimer = setTimeout(sendScroll, 400);
}
});
document.addEventListener('scrollend', scrollEnd);
}
function startInspectingHost(onlySuspenseNodes: boolean) {
inspectOnlySuspenseNodes = onlySuspenseNodes;
registerListenersOnWindow(window);
}
function registerListenersOnWindow(window: any) {
if (window && typeof window.addEventListener === 'function') {
window.addEventListener('click', onClick, true);
window.addEventListener('mousedown', onMouseEvent, true);
window.addEventListener('mouseover', onMouseEvent, true);
window.addEventListener('mouseup', onMouseEvent, true);
window.addEventListener('pointerdown', onPointerDown, true);
window.addEventListener('pointermove', onPointerMove, true);
window.addEventListener('pointerup', onPointerUp, true);
} else {
agent.emit('startInspectingNative');
}
}
function stopInspectingHost() {
hideOverlay(agent);
removeListenersOnWindow(window);
iframesListeningTo.forEach(function (frame) {
try {
removeListenersOnWindow(frame.contentWindow);
} catch (error) {
}
});
iframesListeningTo = new Set();
}
function removeListenersOnWindow(window: any) {
if (window && typeof window.removeEventListener === 'function') {
window.removeEventListener('click', onClick, true);
window.removeEventListener('mousedown', onMouseEvent, true);
window.removeEventListener('mouseover', onMouseEvent, true);
window.removeEventListener('mouseup', onMouseEvent, true);
window.removeEventListener('pointerdown', onPointerDown, true);
window.removeEventListener('pointermove', onPointerMove, true);
window.removeEventListener('pointerup', onPointerUp, true);
} else {
agent.emit('stopInspectingNative');
}
}
function clearHostInstanceHighlight() {
hideOverlay(agent);
}
function highlightHostInstance({
displayName,
hideAfterTimeout,
id,
openBuiltinElementsPanel,
rendererID,
scrollIntoView,
}: {
displayName: string | null,
hideAfterTimeout: boolean,
id: number,
openBuiltinElementsPanel: boolean,
rendererID: number,
scrollIntoView: boolean,
...
}) {
const renderer = agent.rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
hideOverlay(agent);
return;
}
if (!renderer.hasElementWithId(id)) {
hideOverlay(agent);
return;
}
const nodes = renderer.findHostInstancesForElementID(id);
if (nodes != null) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node === null) {
continue;
}
const nodeRects =
typeof node.getClientRects === 'function'
? node.getClientRects()
: [];
if (
typeof node.getClientRects === 'undefined' ||
(nodeRects.length > 0 &&
(nodeRects.length > 2 ||
nodeRects[0].width > 0 ||
nodeRects[0].height > 0))
) {
if (scrollIntoView && typeof node.scrollIntoView === 'function') {
if (scrollDelayTimer) {
clearTimeout(scrollDelayTimer);
scrollDelayTimer = null;
}
node.scrollIntoView({block: 'nearest', inline: 'nearest'});
}
showOverlay(nodes, displayName, agent, hideAfterTimeout);
if (openBuiltinElementsPanel) {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = node;
bridge.send('syncSelectionToBuiltinElementsPanel');
}
return;
}
}
}
hideOverlay(agent);
}
function highlightHostInstances({
displayName,
hideAfterTimeout,
elements,
scrollIntoView,
}: {
displayName: string | null,
hideAfterTimeout: boolean,
elements: Array<{rendererID: number, id: number}>,
scrollIntoView: boolean,
}) {
const nodes: Array<HostInstance> = [];
for (let i = 0; i < elements.length; i++) {
const {id, rendererID} = elements[i];
const renderer = agent.rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
continue;
}
if (!renderer.hasElementWithId(id)) {
continue;
}
const hostInstances = renderer.findHostInstancesForElementID(id);
if (hostInstances !== null) {
for (let j = 0; j < hostInstances.length; j++) {
nodes.push(hostInstances[j]);
}
}
}
if (nodes.length > 0) {
const node = nodes[0];
if (scrollIntoView && typeof node.scrollIntoView === 'function') {
node.scrollIntoView({block: 'nearest', inline: 'nearest'});
}
}
showOverlay(nodes, displayName, agent, hideAfterTimeout);
}
function attemptScrollToHostInstance(
renderer: RendererInterface,
id: number,
) {
const nodes = renderer.findHostInstancesForElementID(id);
if (nodes != null) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node === null) {
continue;
}
const nodeRects =
typeof node.getClientRects === 'function'
? node.getClientRects()
: [];
if (
nodeRects.length > 0 &&
(nodeRects.length > 2 ||
nodeRects[0].width > 0 ||
nodeRects[0].height > 0)
) {
if (typeof node.scrollIntoView === 'function') {
node.scrollIntoView({
block: 'nearest',
inline: 'nearest',
behavior: 'smooth',
});
return true;
}
}
}
}
return false;
}
let scrollDelayTimer = null;
function scrollToHostInstance({
id,
rendererID,
}: {
id: number,
rendererID: number,
}) {
hideOverlay(agent);
if (isReactNativeEnvironment()) {
return;
}
if (scrollDelayTimer) {
clearTimeout(scrollDelayTimer);
scrollDelayTimer = null;
}
const renderer = agent.rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
return;
}
if (!renderer.hasElementWithId(id)) {
return;
}
if (attemptScrollToHostInstance(renderer, id)) {
return;
}
const rects = renderer.findLastKnownRectsForID(id);
if (rects !== null && rects.length > 0) {
let x = Infinity;
let y = Infinity;
for (let i = 0; i < rects.length; i++) {
const rect = rects[i];
if (rect.x < x) {
x = rect.x;
}
if (rect.y < y) {
y = rect.y;
}
}
const element = document.documentElement;
if (!element) {
return;
}
if (
x < window.scrollX ||
y < window.scrollY ||
x > window.scrollX + element.clientWidth ||
y > window.scrollY + element.clientHeight
) {
window.scrollTo({
top: y,
left: x,
behavior: 'smooth',
});
}
scrollDelayTimer = setTimeout(() => {
attemptScrollToHostInstance(renderer, id);
}, 100);
}
}
function onClick(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
stopInspectingHost();
bridge.send('stopInspectingHost', true);
}
function onMouseEvent(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
}
function onPointerDown(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
selectElementForNode(getEventTarget(event));
}
let lastHoveredNode: HTMLElement | null = null;
function onPointerMove(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
const target: HTMLElement = getEventTarget(event);
if (lastHoveredNode === target) return;
lastHoveredNode = target;
if (target.tagName === 'IFRAME') {
const iframe: HTMLIFrameElement = (target: any);
try {
if (!iframesListeningTo.has(iframe)) {
const window = iframe.contentWindow;
registerListenersOnWindow(window);
iframesListeningTo.add(iframe);
}
} catch (error) {
}
}
if (inspectOnlySuspenseNodes) {
const match = agent.getIDForHostInstance(
target,
inspectOnlySuspenseNodes,
);
if (match !== null) {
const renderer = agent.rendererInterfaces[match.rendererID];
if (renderer == null) {
console.warn(
`Invalid renderer id "${match.rendererID}" for element "${match.id}"`,
);
return;
}
highlightHostInstance({
displayName: renderer.getDisplayNameForElementID(match.id),
hideAfterTimeout: false,
id: match.id,
openBuiltinElementsPanel: false,
rendererID: match.rendererID,
scrollIntoView: false,
});
}
} else {
showOverlay([target], null, agent, false);
}
}
function onPointerUp(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
}
const selectElementForNode = (node: HTMLElement) => {
const match = agent.getIDForHostInstance(node, inspectOnlySuspenseNodes);
if (match !== null) {
bridge.send('selectElement', match.id);
}
};
function getEventTarget(event: MouseEvent): HTMLElement {
if (event.composed) {
return (event.composedPath()[0]: any);
}
return (event.target: any);
}
}