export default class Inspector { constructor({ onInspect, onHighlight, onDisableHighlight }) { this.onMouseMove = (e) => { onDisableHighlight(); const noodlNode = this.findNoodlNode(e.target); if (noodlNode) { document.body.style.cursor = 'pointer'; onHighlight(noodlNode.id); } else { document.body.style.cursor = 'initial'; } e.stopPropagation(); }; this.onClick = (e) => { onDisableHighlight(); const noodlNode = this.findNoodlNode(e.target); if (noodlNode) { onInspect([noodlNode.id]); } e.stopPropagation(); e.preventDefault(); //not sure how to stop React input elements from getting focus, so blurring the potential element tha got focus on click if (document.activeElement) { document.activeElement.blur(); } }; this.onContextMenu = (e) => { const nodeIds = document .elementsFromPoint(e.clientX, e.clientY) .map((dom) => this.findNoodlNode(dom)) .filter((node) => !!node) .map((node) => node.id); if (nodeIds.length) { onInspect(nodeIds); } e.stopPropagation(); e.preventDefault(); //not sure how to stop React input elements from getting focus, so blurring the potential element tha got focus on click if (document.activeElement) { document.activeElement.blur(); } }; this.onMouseOut = (e) => { onDisableHighlight(); }; this.blockEvent = (e) => { e.stopPropagation(); }; this.onDisableHighlight = onDisableHighlight; } setComponent(component) { this.component = component; } enable() { //blur active element, if any if (document.activeElement) { document.activeElement.blur(); } //get events from capture phase, before they tunnel down the tree document.addEventListener('mouseenter', this.blockEvent, true); document.addEventListener('mouseover', this.blockEvent, true); document.addEventListener('mousedown', this.blockEvent, true); document.addEventListener('mouseup', this.blockEvent, true); document.addEventListener('mousemove', this.onMouseMove, true); document.addEventListener('mouseout', this.onMouseOut, true); document.addEventListener('click', this.onClick, true); document.addEventListener('contextmenu', this.onContextMenu, true); } disable() { document.body.style.cursor = 'initial'; document.removeEventListener('mouseenter', this.blockEvent, true); document.removeEventListener('mouseover', this.blockEvent, true); document.removeEventListener('mousedown', this.blockEvent, true); document.removeEventListener('mouseup', this.blockEvent, true); document.removeEventListener('mousemove', this.onMouseMove, true); document.removeEventListener('mouseout', this.onMouseOut, true); document.removeEventListener('click', this.onClick, true); document.removeEventListener('contextmenu', this.onContextMenu, true); this.onDisableHighlight(); } findNoodlNode(dom) { //walk the dom tree upwards until a dom element with react state is found let domFiber; while (!domFiber && dom) { // React 18 changed from __reactInternalInstance$ to __reactFiber$ const key = Object.keys(dom).find((key) => key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$') ); domFiber = dom[key]; if (!domFiber) dom = dom.parentElement; } //found none if (!domFiber) { return undefined; } const GetCompFiber = (fiber) => { let parentFiber = fiber.return; while (parentFiber && typeof parentFiber.type == 'string') { parentFiber = parentFiber.return; } return parentFiber; }; //found a react node, now walk the react tree until a noodl node is found //(identified by having a noodlNode prop) let compFiber = GetCompFiber(domFiber); while (compFiber && (!compFiber.stateNode || !compFiber.stateNode.props || !compFiber.stateNode.props.noodlNode)) { compFiber = GetCompFiber(compFiber); } const noodlNode = compFiber ? compFiber.stateNode.props.noodlNode : undefined; if (!noodlNode) return; if (this.component) { let node = noodlNode; while (node) { if (node.parentNodeScope) { if (node.parentNodeScope.componentOwner.name === this.component.name) { return node; } node = node.parentNodeScope.componentOwner; } else { if (node.nodeScope.componentOwner.name === this.component.name) { return node; } node = node.nodeScope.componentOwner; } } return node; } return noodlNode; } }