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 throttle from 'lodash.throttle';
    
  11. import {
    
  12.   useCallback,
    
  13.   useEffect,
    
  14.   useLayoutEffect,
    
  15.   useReducer,
    
  16.   useState,
    
  17.   useContext,
    
  18. } from 'react';
    
  19. import {
    
  20.   localStorageGetItem,
    
  21.   localStorageSetItem,
    
  22. } from 'react-devtools-shared/src/storage';
    
  23. import {StoreContext, BridgeContext} from './context';
    
  24. import {sanitizeForParse, smartParse, smartStringify} from '../utils';
    
  25. 
    
  26. type ACTION_RESET = {
    
  27.   type: 'RESET',
    
  28.   externalValue: any,
    
  29. };
    
  30. type ACTION_UPDATE = {
    
  31.   type: 'UPDATE',
    
  32.   editableValue: any,
    
  33.   externalValue: any,
    
  34. };
    
  35. 
    
  36. type UseEditableValueAction = ACTION_RESET | ACTION_UPDATE;
    
  37. type UseEditableValueDispatch = (action: UseEditableValueAction) => void;
    
  38. type UseEditableValueState = {
    
  39.   editableValue: any,
    
  40.   externalValue: any,
    
  41.   hasPendingChanges: boolean,
    
  42.   isValid: boolean,
    
  43.   parsedValue: any,
    
  44. };
    
  45. 
    
  46. function useEditableValueReducer(
    
  47.   state: UseEditableValueState,
    
  48.   action: UseEditableValueAction,
    
  49. ) {
    
  50.   switch (action.type) {
    
  51.     case 'RESET':
    
  52.       return {
    
  53.         ...state,
    
  54.         editableValue: smartStringify(action.externalValue),
    
  55.         externalValue: action.externalValue,
    
  56.         hasPendingChanges: false,
    
  57.         isValid: true,
    
  58.         parsedValue: action.externalValue,
    
  59.       };
    
  60.     case 'UPDATE':
    
  61.       let isNewValueValid = false;
    
  62.       let newParsedValue;
    
  63.       try {
    
  64.         newParsedValue = smartParse(action.editableValue);
    
  65.         isNewValueValid = true;
    
  66.       } catch (error) {}
    
  67.       return {
    
  68.         ...state,
    
  69.         editableValue: sanitizeForParse(action.editableValue),
    
  70.         externalValue: action.externalValue,
    
  71.         hasPendingChanges:
    
  72.           smartStringify(action.externalValue) !== action.editableValue,
    
  73.         isValid: isNewValueValid,
    
  74.         parsedValue: isNewValueValid ? newParsedValue : state.parsedValue,
    
  75.       };
    
  76.     default:
    
  77.       throw new Error(`Invalid action "${action.type}"`);
    
  78.   }
    
  79. }
    
  80. 
    
  81. // Convenience hook for working with an editable value that is validated via JSON.parse.
    
  82. export function useEditableValue(
    
  83.   externalValue: any,
    
  84. ): [UseEditableValueState, UseEditableValueDispatch] {
    
  85.   const [state, dispatch] = useReducer<
    
  86.     UseEditableValueState,
    
  87.     UseEditableValueState,
    
  88.     UseEditableValueAction,
    
  89.   >(useEditableValueReducer, {
    
  90.     editableValue: smartStringify(externalValue),
    
  91.     externalValue,
    
  92.     hasPendingChanges: false,
    
  93.     isValid: true,
    
  94.     parsedValue: externalValue,
    
  95.   });
    
  96.   if (!Object.is(state.externalValue, externalValue)) {
    
  97.     if (!state.hasPendingChanges) {
    
  98.       dispatch({
    
  99.         type: 'RESET',
    
  100.         externalValue,
    
  101.       });
    
  102.     } else {
    
  103.       dispatch({
    
  104.         type: 'UPDATE',
    
  105.         editableValue: state.editableValue,
    
  106.         externalValue,
    
  107.       });
    
  108.     }
    
  109.   }
    
  110. 
    
  111.   return [state, dispatch];
    
  112. }
    
  113. 
    
  114. export function useIsOverflowing(
    
  115.   containerRef: {current: HTMLDivElement | null, ...},
    
  116.   totalChildWidth: number,
    
  117. ): boolean {
    
  118.   const [isOverflowing, setIsOverflowing] = useState<boolean>(false);
    
  119. 
    
  120.   // It's important to use a layout effect, so that we avoid showing a flash of overflowed content.
    
  121.   useLayoutEffect(() => {
    
  122.     if (containerRef.current === null) {
    
  123.       return () => {};
    
  124.     }
    
  125. 
    
  126.     const container = ((containerRef.current: any): HTMLDivElement);
    
  127. 
    
  128.     const handleResize = throttle(
    
  129.       () => setIsOverflowing(container.clientWidth <= totalChildWidth),
    
  130.       100,
    
  131.     );
    
  132. 
    
  133.     handleResize();
    
  134. 
    
  135.     // It's important to listen to the ownerDocument.defaultView to support the browser extension.
    
  136.     // Here we use portals to render individual tabs (e.g. Profiler),
    
  137.     // and the root document might belong to a different window.
    
  138.     const ownerWindow = container.ownerDocument.defaultView;
    
  139.     ownerWindow.addEventListener('resize', handleResize);
    
  140.     return () => ownerWindow.removeEventListener('resize', handleResize);
    
  141.   }, [containerRef, totalChildWidth]);
    
  142. 
    
  143.   return isOverflowing;
    
  144. }
    
  145. 
    
  146. // Forked from https://usehooks.com/useLocalStorage/
    
  147. export function useLocalStorage<T>(
    
  148.   key: string,
    
  149.   initialValue: T | (() => T),
    
  150.   onValueSet?: (any, string) => void,
    
  151. ): [T, (value: T | (() => T)) => void] {
    
  152.   const getValueFromLocalStorage = useCallback(() => {
    
  153.     try {
    
  154.       const item = localStorageGetItem(key);
    
  155.       if (item != null) {
    
  156.         return JSON.parse(item);
    
  157.       }
    
  158.     } catch (error) {
    
  159.       console.log(error);
    
  160.     }
    
  161.     if (typeof initialValue === 'function') {
    
  162.       return ((initialValue: any): () => T)();
    
  163.     } else {
    
  164.       return initialValue;
    
  165.     }
    
  166.   }, [initialValue, key]);
    
  167. 
    
  168.   const [storedValue, setStoredValue] = useState<any>(getValueFromLocalStorage);
    
  169. 
    
  170.   const setValue = useCallback(
    
  171.     (value: $FlowFixMe) => {
    
  172.       try {
    
  173.         const valueToStore =
    
  174.           value instanceof Function ? (value: any)(storedValue) : value;
    
  175.         setStoredValue(valueToStore);
    
  176.         localStorageSetItem(key, JSON.stringify(valueToStore));
    
  177. 
    
  178.         // Notify listeners that this setting has changed.
    
  179.         window.dispatchEvent(new Event(key));
    
  180. 
    
  181.         if (onValueSet != null) {
    
  182.           onValueSet(valueToStore, key);
    
  183.         }
    
  184.       } catch (error) {
    
  185.         console.log(error);
    
  186.       }
    
  187.     },
    
  188.     [key, storedValue],
    
  189.   );
    
  190. 
    
  191.   // Listen for changes to this local storage value made from other windows.
    
  192.   // This enables the e.g. "⚛️ Elements" tab to update in response to changes from "⚛️ Settings".
    
  193.   useLayoutEffect(() => {
    
  194.     // $FlowFixMe[missing-local-annot]
    
  195.     const onStorage = event => {
    
  196.       const newValue = getValueFromLocalStorage();
    
  197.       if (key === event.key && storedValue !== newValue) {
    
  198.         setValue(newValue);
    
  199.       }
    
  200.     };
    
  201. 
    
  202.     window.addEventListener('storage', onStorage);
    
  203. 
    
  204.     return () => {
    
  205.       window.removeEventListener('storage', onStorage);
    
  206.     };
    
  207.   }, [getValueFromLocalStorage, key, storedValue, setValue]);
    
  208. 
    
  209.   return [storedValue, setValue];
    
  210. }
    
  211. 
    
  212. export function useModalDismissSignal(
    
  213.   modalRef: {current: HTMLDivElement | null, ...},
    
  214.   dismissCallback: () => void,
    
  215.   dismissOnClickOutside?: boolean = true,
    
  216. ): void {
    
  217.   useEffect(() => {
    
  218.     if (modalRef.current === null) {
    
  219.       return () => {};
    
  220.     }
    
  221. 
    
  222.     const handleDocumentKeyDown = (event: any) => {
    
  223.       if (event.key === 'Escape') {
    
  224.         dismissCallback();
    
  225.       }
    
  226.     };
    
  227. 
    
  228.     const handleDocumentClick = (event: any) => {
    
  229.       if (
    
  230.         modalRef.current !== null &&
    
  231.         !modalRef.current.contains(event.target)
    
  232.       ) {
    
  233.         event.stopPropagation();
    
  234.         event.preventDefault();
    
  235. 
    
  236.         dismissCallback();
    
  237.       }
    
  238.     };
    
  239. 
    
  240.     let ownerDocument = null;
    
  241. 
    
  242.     // Delay until after the current call stack is empty,
    
  243.     // in case this effect is being run while an event is currently bubbling.
    
  244.     // In that case, we don't want to listen to the pre-existing event.
    
  245.     let timeoutID: null | TimeoutID = setTimeout(() => {
    
  246.       timeoutID = null;
    
  247. 
    
  248.       // It's important to listen to the ownerDocument to support the browser extension.
    
  249.       // Here we use portals to render individual tabs (e.g. Profiler),
    
  250.       // and the root document might belong to a different window.
    
  251.       const div = modalRef.current;
    
  252.       if (div != null) {
    
  253.         ownerDocument = div.ownerDocument;
    
  254.         ownerDocument.addEventListener('keydown', handleDocumentKeyDown);
    
  255.         if (dismissOnClickOutside) {
    
  256.           ownerDocument.addEventListener('click', handleDocumentClick, true);
    
  257.         }
    
  258.       }
    
  259.     }, 0);
    
  260. 
    
  261.     return () => {
    
  262.       if (timeoutID !== null) {
    
  263.         clearTimeout(timeoutID);
    
  264.       }
    
  265. 
    
  266.       if (ownerDocument !== null) {
    
  267.         ownerDocument.removeEventListener('keydown', handleDocumentKeyDown);
    
  268.         ownerDocument.removeEventListener('click', handleDocumentClick, true);
    
  269.       }
    
  270.     };
    
  271.   }, [modalRef, dismissCallback, dismissOnClickOutside]);
    
  272. }
    
  273. 
    
  274. // Copied from https://github.com/facebook/react/pull/15022
    
  275. export function useSubscription<Value>({
    
  276.   getCurrentValue,
    
  277.   subscribe,
    
  278. }: {
    
  279.   getCurrentValue: () => Value,
    
  280.   subscribe: (callback: Function) => () => void,
    
  281. }): Value {
    
  282.   const [state, setState] = useState(() => ({
    
  283.     getCurrentValue,
    
  284.     subscribe,
    
  285.     value: getCurrentValue(),
    
  286.   }));
    
  287. 
    
  288.   if (
    
  289.     state.getCurrentValue !== getCurrentValue ||
    
  290.     state.subscribe !== subscribe
    
  291.   ) {
    
  292.     setState({
    
  293.       getCurrentValue,
    
  294.       subscribe,
    
  295.       value: getCurrentValue(),
    
  296.     });
    
  297.   }
    
  298. 
    
  299.   useEffect(() => {
    
  300.     let didUnsubscribe = false;
    
  301. 
    
  302.     const checkForUpdates = () => {
    
  303.       if (didUnsubscribe) {
    
  304.         return;
    
  305.       }
    
  306. 
    
  307.       setState(prevState => {
    
  308.         if (
    
  309.           prevState.getCurrentValue !== getCurrentValue ||
    
  310.           prevState.subscribe !== subscribe
    
  311.         ) {
    
  312.           return prevState;
    
  313.         }
    
  314. 
    
  315.         const value = getCurrentValue();
    
  316.         if (prevState.value === value) {
    
  317.           return prevState;
    
  318.         }
    
  319. 
    
  320.         return {...prevState, value};
    
  321.       });
    
  322.     };
    
  323.     const unsubscribe = subscribe(checkForUpdates);
    
  324. 
    
  325.     checkForUpdates();
    
  326. 
    
  327.     return () => {
    
  328.       didUnsubscribe = true;
    
  329.       unsubscribe();
    
  330.     };
    
  331.   }, [getCurrentValue, subscribe]);
    
  332. 
    
  333.   return state.value;
    
  334. }
    
  335. 
    
  336. export function useHighlightNativeElement(): {
    
  337.   clearHighlightNativeElement: () => void,
    
  338.   highlightNativeElement: (id: number) => void,
    
  339. } {
    
  340.   const bridge = useContext(BridgeContext);
    
  341.   const store = useContext(StoreContext);
    
  342. 
    
  343.   const highlightNativeElement = useCallback(
    
  344.     (id: number) => {
    
  345.       const element = store.getElementByID(id);
    
  346.       const rendererID = store.getRendererIDForElement(id);
    
  347.       if (element !== null && rendererID !== null) {
    
  348.         bridge.send('highlightNativeElement', {
    
  349.           displayName: element.displayName,
    
  350.           hideAfterTimeout: false,
    
  351.           id,
    
  352.           openNativeElementsPanel: false,
    
  353.           rendererID,
    
  354.           scrollIntoView: false,
    
  355.         });
    
  356.       }
    
  357.     },
    
  358.     [store, bridge],
    
  359.   );
    
  360. 
    
  361.   const clearHighlightNativeElement = useCallback(() => {
    
  362.     bridge.send('clearNativeElementHighlight');
    
  363.   }, [bridge]);
    
  364. 
    
  365.   return {
    
  366.     highlightNativeElement,
    
  367.     clearHighlightNativeElement,
    
  368.   };
    
  369. }