// @flow import React, { Fragment, useCallback, useContext, useLayoutEffect, useMemo, useRef, useState, } from 'react'; import { ElementTypeClass, ElementTypeFunction } from 'src/devtools/types'; import Store from 'src/devtools/store'; import ButtonIcon from '../ButtonIcon'; import { createRegExp } from '../utils'; import { TreeContext } from './TreeContext'; import { StoreContext } from '../context'; import type { ItemData } from './Tree'; import type { Element } from './types'; import styles from './Element.css'; type Props = { data: ItemData, index: number, style: Object, }; export default function ElementView({ data, index, style }: Props) { const [isHovered, setIsHovered] = useState(false); const { baseDepth, getElementAtIndex, ownerStack, selectOwner, selectedElementID, selectElementByID, } = useContext(TreeContext); const store = useContext(StoreContext); const element = getElementAtIndex(index); const { lastScrolledIDRef, treeFocused, isNavigatingWithKeyboard, onElementMouseEnter, } = data; const id = element === null ? null : element.id; const isSelected = selectedElementID === id; const handleDoubleClick = useCallback(() => { if (id !== null) { selectOwner(id); } }, [id, selectOwner]); const ref = useRef(null); // The tree above has its own autoscrolling, but it only works for rows. // However, even when the row gets into the viewport, the component name // might be too far left or right on the screen. Adjust it in this case. useLayoutEffect(() => { if (isSelected) { // Don't select the same item twice. // A row may appear and disappear just by scrolling: // https://github.com/bvaughn/react-devtools-experimental/issues/67 // It doesn't necessarily indicate a user action. // TODO: we might want to revamp the autoscroll logic // to only happen explicitly for user-initiated events. if (lastScrolledIDRef.current === id) { return; } lastScrolledIDRef.current = id; if (ref.current !== null) { ref.current.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest', }); } } }, [id, isSelected, lastScrolledIDRef]); const handleMouseDown = useCallback( ({ metaKey }) => { if (id !== null) { selectElementByID(metaKey ? null : id); } }, [id, selectElementByID] ); const handleMouseEnter = useCallback(() => { setIsHovered(true); if (id !== null) { onElementMouseEnter(id); } }, [onElementMouseEnter, id]); const handleMouseLeave = useCallback(() => { setIsHovered(false); }, []); // Handle elements that are removed from the tree while an async render is in progress. if (element == null) { console.warn(` Could not find element at index ${index}`); // This return needs to happen after hooks, since hooks can't be conditional. return null; } const { depth, displayName, key, type } = ((element: any): Element); const showDollarR = isSelected && (type === ElementTypeClass || type === ElementTypeFunction); let className = styles.Element; if (isSelected) { className = treeFocused ? styles.SelectedElement : styles.InactiveSelectedElement; } else if (isHovered && !isNavigatingWithKeyboard) { className = styles.HoveredElement; } return (
{ownerStack.length === 0 ? ( ) : null} {key && (  key= "{key}" )} {showDollarR &&  == $r}
); } // Prevent double clicks on toggle from drilling into the owner list. const swallowDoubleClick = event => { event.preventDefault(); event.stopPropagation(); }; type ExpandCollapseToggleProps = {| element: Element, store: Store, |}; function ExpandCollapseToggle({ element, store }: ExpandCollapseToggleProps) { const { children, id, isCollapsed } = element; const toggleCollapsed = useCallback( event => { event.preventDefault(); event.stopPropagation(); store.toggleIsCollapsed(id, !isCollapsed); }, [id, isCollapsed, store] ); const stopPropagation = useCallback(event => { // Prevent the row from selecting event.stopPropagation(); }, []); if (children.length === 0) { return
; } return (
); } type DisplayNameProps = {| displayName: string | null, id: number, |}; function DisplayName({ displayName, id }: DisplayNameProps) { const { searchIndex, searchResults, searchText } = useContext(TreeContext); const isSearchResult = useMemo(() => { return searchResults.includes(id); }, [id, searchResults]); const isCurrentResult = searchIndex !== null && id === searchResults[searchIndex]; if (!isSearchResult || displayName === null) { return displayName; } const match = createRegExp(searchText).exec(displayName); if (match === null) { return displayName; } const startIndex = match.index; const stopIndex = startIndex + match[0].length; const children = []; if (startIndex > 0) { children.push({displayName.slice(0, startIndex)}); } children.push( {displayName.slice(startIndex, stopIndex)} ); if (stopIndex < displayName.length) { children.push({displayName.slice(stopIndex)}); } return children; }