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 * as React from 'react';
    
  11. import {
    
  12.   Fragment,
    
  13.   Suspense,
    
  14.   useEffect,
    
  15.   useLayoutEffect,
    
  16.   useReducer,
    
  17.   useRef,
    
  18. } from 'react';
    
  19. import Tree from './Tree';
    
  20. import {OwnersListContextController} from './OwnersListContext';
    
  21. import portaledContent from '../portaledContent';
    
  22. import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
    
  23. import {
    
  24.   localStorageGetItem,
    
  25.   localStorageSetItem,
    
  26. } from 'react-devtools-shared/src/storage';
    
  27. import InspectedElementErrorBoundary from './InspectedElementErrorBoundary';
    
  28. import InspectedElement from './InspectedElement';
    
  29. import {InspectedElementContextController} from './InspectedElementContext';
    
  30. import {ModalDialog} from '../ModalDialog';
    
  31. import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
    
  32. import {NativeStyleContextController} from './NativeStyleEditor/context';
    
  33. 
    
  34. import styles from './Components.css';
    
  35. 
    
  36. type Orientation = 'horizontal' | 'vertical';
    
  37. 
    
  38. type ResizeActionType =
    
  39.   | 'ACTION_SET_DID_MOUNT'
    
  40.   | 'ACTION_SET_IS_RESIZING'
    
  41.   | 'ACTION_SET_HORIZONTAL_PERCENTAGE'
    
  42.   | 'ACTION_SET_VERTICAL_PERCENTAGE';
    
  43. 
    
  44. type ResizeAction = {
    
  45.   type: ResizeActionType,
    
  46.   payload: any,
    
  47. };
    
  48. 
    
  49. type ResizeState = {
    
  50.   horizontalPercentage: number,
    
  51.   isResizing: boolean,
    
  52.   verticalPercentage: number,
    
  53. };
    
  54. 
    
  55. function Components(_: {}) {
    
  56.   const wrapperElementRef = useRef<null | HTMLElement>(null);
    
  57.   const resizeElementRef = useRef<null | HTMLElement>(null);
    
  58. 
    
  59.   const [state, dispatch] = useReducer<ResizeState, any, ResizeAction>(
    
  60.     resizeReducer,
    
  61.     null,
    
  62.     initResizeState,
    
  63.   );
    
  64. 
    
  65.   const {horizontalPercentage, verticalPercentage} = state;
    
  66. 
    
  67.   useLayoutEffect(() => {
    
  68.     const resizeElement = resizeElementRef.current;
    
  69. 
    
  70.     setResizeCSSVariable(
    
  71.       resizeElement,
    
  72.       'horizontal',
    
  73.       horizontalPercentage * 100,
    
  74.     );
    
  75.     setResizeCSSVariable(resizeElement, 'vertical', verticalPercentage * 100);
    
  76.   }, []);
    
  77. 
    
  78.   useEffect(() => {
    
  79.     const timeoutID = setTimeout(() => {
    
  80.       localStorageSetItem(
    
  81.         LOCAL_STORAGE_KEY,
    
  82.         JSON.stringify({
    
  83.           horizontalPercentage,
    
  84.           verticalPercentage,
    
  85.         }),
    
  86.       );
    
  87.     }, 500);
    
  88. 
    
  89.     return () => clearTimeout(timeoutID);
    
  90.   }, [horizontalPercentage, verticalPercentage]);
    
  91. 
    
  92.   const {isResizing} = state;
    
  93. 
    
  94.   const onResizeStart = () =>
    
  95.     dispatch({type: 'ACTION_SET_IS_RESIZING', payload: true});
    
  96. 
    
  97.   let onResize;
    
  98.   let onResizeEnd;
    
  99.   if (isResizing) {
    
  100.     onResizeEnd = () =>
    
  101.       dispatch({type: 'ACTION_SET_IS_RESIZING', payload: false});
    
  102. 
    
  103.     // $FlowFixMe[missing-local-annot]
    
  104.     onResize = event => {
    
  105.       const resizeElement = resizeElementRef.current;
    
  106.       const wrapperElement = wrapperElementRef.current;
    
  107. 
    
  108.       if (!isResizing || wrapperElement === null || resizeElement === null) {
    
  109.         return;
    
  110.       }
    
  111. 
    
  112.       event.preventDefault();
    
  113. 
    
  114.       const orientation = getOrientation(wrapperElement);
    
  115. 
    
  116.       const {height, width, left, top} = wrapperElement.getBoundingClientRect();
    
  117. 
    
  118.       const currentMousePosition =
    
  119.         orientation === 'horizontal'
    
  120.           ? event.clientX - left
    
  121.           : event.clientY - top;
    
  122. 
    
  123.       const boundaryMin = MINIMUM_SIZE;
    
  124.       const boundaryMax =
    
  125.         orientation === 'horizontal'
    
  126.           ? width - MINIMUM_SIZE
    
  127.           : height - MINIMUM_SIZE;
    
  128. 
    
  129.       const isMousePositionInBounds =
    
  130.         currentMousePosition > boundaryMin &&
    
  131.         currentMousePosition < boundaryMax;
    
  132. 
    
  133.       if (isMousePositionInBounds) {
    
  134.         const resizedElementDimension =
    
  135.           orientation === 'horizontal' ? width : height;
    
  136.         const actionType =
    
  137.           orientation === 'horizontal'
    
  138.             ? 'ACTION_SET_HORIZONTAL_PERCENTAGE'
    
  139.             : 'ACTION_SET_VERTICAL_PERCENTAGE';
    
  140.         const percentage =
    
  141.           (currentMousePosition / resizedElementDimension) * 100;
    
  142. 
    
  143.         setResizeCSSVariable(resizeElement, orientation, percentage);
    
  144. 
    
  145.         dispatch({
    
  146.           type: actionType,
    
  147.           payload: currentMousePosition / resizedElementDimension,
    
  148.         });
    
  149.       }
    
  150.     };
    
  151.   }
    
  152. 
    
  153.   return (
    
  154.     <SettingsModalContextController>
    
  155.       <OwnersListContextController>
    
  156.         <div
    
  157.           ref={wrapperElementRef}
    
  158.           className={styles.Components}
    
  159.           onMouseMove={onResize}
    
  160.           onMouseLeave={onResizeEnd}
    
  161.           onMouseUp={onResizeEnd}>
    
  162.           <Fragment>
    
  163.             <div ref={resizeElementRef} className={styles.TreeWrapper}>
    
  164.               <Tree />
    
  165.             </div>
    
  166.             <div className={styles.ResizeBarWrapper}>
    
  167.               <div onMouseDown={onResizeStart} className={styles.ResizeBar} />
    
  168.             </div>
    
  169.             <div className={styles.InspectedElementWrapper}>
    
  170.               <NativeStyleContextController>
    
  171.                 <InspectedElementErrorBoundary>
    
  172.                   <Suspense fallback={<Loading />}>
    
  173.                     <InspectedElementContextController>
    
  174.                       <InspectedElement />
    
  175.                     </InspectedElementContextController>
    
  176.                   </Suspense>
    
  177.                 </InspectedElementErrorBoundary>
    
  178.               </NativeStyleContextController>
    
  179.             </div>
    
  180.             <ModalDialog />
    
  181.             <SettingsModal />
    
  182.           </Fragment>
    
  183.         </div>
    
  184.       </OwnersListContextController>
    
  185.     </SettingsModalContextController>
    
  186.   );
    
  187. }
    
  188. 
    
  189. function Loading() {
    
  190.   return <div className={styles.Loading}>Loading...</div>;
    
  191. }
    
  192. 
    
  193. const LOCAL_STORAGE_KEY = 'React::DevTools::createResizeReducer';
    
  194. const VERTICAL_MODE_MAX_WIDTH = 600;
    
  195. const MINIMUM_SIZE = 50;
    
  196. 
    
  197. function initResizeState(): ResizeState {
    
  198.   let horizontalPercentage = 0.65;
    
  199.   let verticalPercentage = 0.5;
    
  200. 
    
  201.   try {
    
  202.     let data = localStorageGetItem(LOCAL_STORAGE_KEY);
    
  203.     if (data != null) {
    
  204.       data = JSON.parse(data);
    
  205.       horizontalPercentage = data.horizontalPercentage;
    
  206.       verticalPercentage = data.verticalPercentage;
    
  207.     }
    
  208.   } catch (error) {}
    
  209. 
    
  210.   return {
    
  211.     horizontalPercentage,
    
  212.     isResizing: false,
    
  213.     verticalPercentage,
    
  214.   };
    
  215. }
    
  216. 
    
  217. function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState {
    
  218.   switch (action.type) {
    
  219.     case 'ACTION_SET_IS_RESIZING':
    
  220.       return {
    
  221.         ...state,
    
  222.         isResizing: action.payload,
    
  223.       };
    
  224.     case 'ACTION_SET_HORIZONTAL_PERCENTAGE':
    
  225.       return {
    
  226.         ...state,
    
  227.         horizontalPercentage: action.payload,
    
  228.       };
    
  229.     case 'ACTION_SET_VERTICAL_PERCENTAGE':
    
  230.       return {
    
  231.         ...state,
    
  232.         verticalPercentage: action.payload,
    
  233.       };
    
  234.     default:
    
  235.       return state;
    
  236.   }
    
  237. }
    
  238. 
    
  239. function getOrientation(
    
  240.   wrapperElement: null | HTMLElement,
    
  241. ): null | Orientation {
    
  242.   if (wrapperElement != null) {
    
  243.     const {width} = wrapperElement.getBoundingClientRect();
    
  244.     return width > VERTICAL_MODE_MAX_WIDTH ? 'horizontal' : 'vertical';
    
  245.   }
    
  246.   return null;
    
  247. }
    
  248. 
    
  249. function setResizeCSSVariable(
    
  250.   resizeElement: null | HTMLElement,
    
  251.   orientation: null | Orientation,
    
  252.   percentage: number,
    
  253. ): void {
    
  254.   if (resizeElement !== null && orientation !== null) {
    
  255.     resizeElement.style.setProperty(
    
  256.       `--${orientation}-resize-percentage`,
    
  257.       `${percentage}%`,
    
  258.     );
    
  259.   }
    
  260. }
    
  261. 
    
  262. export default (portaledContent(
    
  263.   Components,
    
  264. ): React$StatelessFunctionalComponent<{}>);