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