1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import memoize from 'memoize-one';
    
  11. import throttle from 'lodash.throttle';
    
  12. import Agent from 'react-devtools-shared/src/backend/agent';
    
  13. import {hideOverlay, showOverlay} from './Highlighter';
    
  14. 
    
  15. import type {BackendBridge} from 'react-devtools-shared/src/bridge';
    
  16. 
    
  17. // This plug-in provides in-page highlighting of the selected element.
    
  18. // It is used by the browser extension and the standalone DevTools shell (when connected to a browser).
    
  19. // It is not currently the mechanism used to highlight React Native views.
    
  20. // That is done by the React Native Inspector component.
    
  21. 
    
  22. let iframesListeningTo: Set<HTMLIFrameElement> = new Set();
    
  23. 
    
  24. export default function setupHighlighter(
    
  25.   bridge: BackendBridge,
    
  26.   agent: Agent,
    
  27. ): void {
    
  28.   bridge.addListener(
    
  29.     'clearNativeElementHighlight',
    
  30.     clearNativeElementHighlight,
    
  31.   );
    
  32.   bridge.addListener('highlightNativeElement', highlightNativeElement);
    
  33.   bridge.addListener('shutdown', stopInspectingNative);
    
  34.   bridge.addListener('startInspectingNative', startInspectingNative);
    
  35.   bridge.addListener('stopInspectingNative', stopInspectingNative);
    
  36. 
    
  37.   function startInspectingNative() {
    
  38.     registerListenersOnWindow(window);
    
  39.   }
    
  40. 
    
  41.   function registerListenersOnWindow(window: any) {
    
  42.     // This plug-in may run in non-DOM environments (e.g. React Native).
    
  43.     if (window && typeof window.addEventListener === 'function') {
    
  44.       window.addEventListener('click', onClick, true);
    
  45.       window.addEventListener('mousedown', onMouseEvent, true);
    
  46.       window.addEventListener('mouseover', onMouseEvent, true);
    
  47.       window.addEventListener('mouseup', onMouseEvent, true);
    
  48.       window.addEventListener('pointerdown', onPointerDown, true);
    
  49.       window.addEventListener('pointermove', onPointerMove, true);
    
  50.       window.addEventListener('pointerup', onPointerUp, true);
    
  51.     } else {
    
  52.       agent.emit('startInspectingNative');
    
  53.     }
    
  54.   }
    
  55. 
    
  56.   function stopInspectingNative() {
    
  57.     hideOverlay(agent);
    
  58.     removeListenersOnWindow(window);
    
  59.     iframesListeningTo.forEach(function (frame) {
    
  60.       try {
    
  61.         removeListenersOnWindow(frame.contentWindow);
    
  62.       } catch (error) {
    
  63.         // This can error when the iframe is on a cross-origin.
    
  64.       }
    
  65.     });
    
  66.     iframesListeningTo = new Set();
    
  67.   }
    
  68. 
    
  69.   function removeListenersOnWindow(window: any) {
    
  70.     // This plug-in may run in non-DOM environments (e.g. React Native).
    
  71.     if (window && typeof window.removeEventListener === 'function') {
    
  72.       window.removeEventListener('click', onClick, true);
    
  73.       window.removeEventListener('mousedown', onMouseEvent, true);
    
  74.       window.removeEventListener('mouseover', onMouseEvent, true);
    
  75.       window.removeEventListener('mouseup', onMouseEvent, true);
    
  76.       window.removeEventListener('pointerdown', onPointerDown, true);
    
  77.       window.removeEventListener('pointermove', onPointerMove, true);
    
  78.       window.removeEventListener('pointerup', onPointerUp, true);
    
  79.     } else {
    
  80.       agent.emit('stopInspectingNative');
    
  81.     }
    
  82.   }
    
  83. 
    
  84.   function clearNativeElementHighlight() {
    
  85.     hideOverlay(agent);
    
  86.   }
    
  87. 
    
  88.   function highlightNativeElement({
    
  89.     displayName,
    
  90.     hideAfterTimeout,
    
  91.     id,
    
  92.     openNativeElementsPanel,
    
  93.     rendererID,
    
  94.     scrollIntoView,
    
  95.   }: {
    
  96.     displayName: string | null,
    
  97.     hideAfterTimeout: boolean,
    
  98.     id: number,
    
  99.     openNativeElementsPanel: boolean,
    
  100.     rendererID: number,
    
  101.     scrollIntoView: boolean,
    
  102.     ...
    
  103.   }) {
    
  104.     const renderer = agent.rendererInterfaces[rendererID];
    
  105.     if (renderer == null) {
    
  106.       console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
    
  107. 
    
  108.       hideOverlay(agent);
    
  109.       return;
    
  110.     }
    
  111. 
    
  112.     // In some cases fiber may already be unmounted
    
  113.     if (!renderer.hasFiberWithId(id)) {
    
  114.       hideOverlay(agent);
    
  115.       return;
    
  116.     }
    
  117. 
    
  118.     const nodes: ?Array<HTMLElement> = (renderer.findNativeNodesForFiberID(
    
  119.       id,
    
  120.     ): any);
    
  121. 
    
  122.     if (nodes != null && nodes[0] != null) {
    
  123.       const node = nodes[0];
    
  124.       // $FlowFixMe[method-unbinding]
    
  125.       if (scrollIntoView && typeof node.scrollIntoView === 'function') {
    
  126.         // If the node isn't visible show it before highlighting it.
    
  127.         // We may want to reconsider this; it might be a little disruptive.
    
  128.         node.scrollIntoView({block: 'nearest', inline: 'nearest'});
    
  129.       }
    
  130. 
    
  131.       showOverlay(nodes, displayName, agent, hideAfterTimeout);
    
  132. 
    
  133.       if (openNativeElementsPanel) {
    
  134.         window.__REACT_DEVTOOLS_GLOBAL_HOOK__.$0 = node;
    
  135.         bridge.send('syncSelectionToNativeElementsPanel');
    
  136.       }
    
  137.     } else {
    
  138.       hideOverlay(agent);
    
  139.     }
    
  140.   }
    
  141. 
    
  142.   function onClick(event: MouseEvent) {
    
  143.     event.preventDefault();
    
  144.     event.stopPropagation();
    
  145. 
    
  146.     stopInspectingNative();
    
  147. 
    
  148.     bridge.send('stopInspectingNative', true);
    
  149.   }
    
  150. 
    
  151.   function onMouseEvent(event: MouseEvent) {
    
  152.     event.preventDefault();
    
  153.     event.stopPropagation();
    
  154.   }
    
  155. 
    
  156.   function onPointerDown(event: MouseEvent) {
    
  157.     event.preventDefault();
    
  158.     event.stopPropagation();
    
  159. 
    
  160.     selectFiberForNode(getEventTarget(event));
    
  161.   }
    
  162. 
    
  163.   let lastHoveredNode: HTMLElement | null = null;
    
  164.   function onPointerMove(event: MouseEvent) {
    
  165.     event.preventDefault();
    
  166.     event.stopPropagation();
    
  167. 
    
  168.     const target: HTMLElement = getEventTarget(event);
    
  169.     if (lastHoveredNode === target) return;
    
  170.     lastHoveredNode = target;
    
  171. 
    
  172.     if (target.tagName === 'IFRAME') {
    
  173.       const iframe: HTMLIFrameElement = (target: any);
    
  174.       try {
    
  175.         if (!iframesListeningTo.has(iframe)) {
    
  176.           const window = iframe.contentWindow;
    
  177.           registerListenersOnWindow(window);
    
  178.           iframesListeningTo.add(iframe);
    
  179.         }
    
  180.       } catch (error) {
    
  181.         // This can error when the iframe is on a cross-origin.
    
  182.       }
    
  183.     }
    
  184. 
    
  185.     // Don't pass the name explicitly.
    
  186.     // It will be inferred from DOM tag and Fiber owner.
    
  187.     showOverlay([target], null, agent, false);
    
  188. 
    
  189.     selectFiberForNode(target);
    
  190.   }
    
  191. 
    
  192.   function onPointerUp(event: MouseEvent) {
    
  193.     event.preventDefault();
    
  194.     event.stopPropagation();
    
  195.   }
    
  196. 
    
  197.   const selectFiberForNode = throttle(
    
  198.     memoize((node: HTMLElement) => {
    
  199.       const id = agent.getIDForNode(node);
    
  200.       if (id !== null) {
    
  201.         bridge.send('selectFiber', id);
    
  202.       }
    
  203.     }),
    
  204.     200,
    
  205.     // Don't change the selection in the very first 200ms
    
  206.     // because those are usually unintentional as you lift the cursor.
    
  207.     {leading: false},
    
  208.   );
    
  209. 
    
  210.   function getEventTarget(event: MouseEvent): HTMLElement {
    
  211.     if (event.composed) {
    
  212.       return (event.composedPath()[0]: any);
    
  213.     }
    
  214. 
    
  215.     return (event.target: any);
    
  216.   }
    
  217. }