Files
react/packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
Brian Vaughn 933f6a07ca DevTools context menu (#17608)
* Added rudimentary context menu hook and menu UI

* Added backend support for copying a value at a specific path for the inspected element

* Added backend support for storing a value (at a specified path) as a global variable

* Added special casing to enable copying undefined/unserializable values to the clipboard

* Added copy and store-as-global context menu options to selected element props panel

* Store global variables separately, with auto-incremented name (like browsers do)

* Added tests for new copy and store-as-global backend functions

* Fixed some ownerDocument/contentWindow edge cases

* Refactored context menu to support dynamic options

Used this mechanism to add a conditional menu option for inspecting the current value (if it's a function)

* Renamed "safeSerialize" to "serializeToString" and added inline comment
2019-12-18 12:12:34 -08:00

267 lines
7.1 KiB
JavaScript

/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import React, {useEffect, useRef, useState} from 'react';
import EditableValue from './EditableValue';
import ExpandCollapseToggle from './ExpandCollapseToggle';
import {alphaSortEntries, getMetaValueLabel} from '../utils';
import {meta} from '../../../hydration';
import useContextMenu from '../../ContextMenu/useContextMenu';
import styles from './KeyValue.css';
import type {Element} from 'react';
import type {InspectPath} from './SelectedElement';
type OverrideValueFn = (path: Array<string | number>, value: any) => void;
type KeyValueProps = {|
alphaSort: boolean,
depth: number,
hidden?: boolean,
inspectPath?: InspectPath,
isReadOnly?: boolean,
name: string,
overrideValueFn?: ?OverrideValueFn,
path: Array<any>,
pathRoot: string,
value: any,
|};
export default function KeyValue({
alphaSort,
depth,
inspectPath,
isReadOnly,
hidden,
name,
overrideValueFn,
path,
pathRoot,
value,
}: KeyValueProps) {
const [isOpen, setIsOpen] = useState<boolean>(false);
const prevIsOpenRef = useRef(isOpen);
const contextMenuTriggerRef = useRef(null);
const isInspectable =
value !== null &&
typeof value === 'object' &&
value[meta.inspectable] &&
value[meta.size] !== 0;
useEffect(
() => {
if (
isInspectable &&
isOpen &&
!prevIsOpenRef.current &&
typeof inspectPath === 'function'
) {
inspectPath(path);
}
prevIsOpenRef.current = isOpen;
},
[inspectPath, isInspectable, isOpen, path],
);
const toggleIsOpen = () => setIsOpen(prevIsOpen => !prevIsOpen);
useContextMenu({
data: {
path: [pathRoot, ...path],
type:
value !== null &&
typeof value === 'object' &&
value.hasOwnProperty(meta.type)
? value[meta.type]
: typeof value,
},
id: 'SelectedElement',
ref: contextMenuTriggerRef,
});
const dataType = typeof value;
const isSimpleType =
dataType === 'number' ||
dataType === 'string' ||
dataType === 'boolean' ||
value == null;
const style = {
paddingLeft: `${(depth - 1) * 0.75}rem`,
};
let children = null;
if (isSimpleType) {
let displayValue = value;
if (dataType === 'string') {
displayValue = `"${value}"`;
} else if (dataType === 'boolean') {
displayValue = value ? 'true' : 'false';
} else if (value === null) {
displayValue = 'null';
} else if (value === undefined) {
displayValue = 'undefined';
}
const isEditable = typeof overrideValueFn === 'function' && !isReadOnly;
children = (
<div
key="root"
path={path}
className={styles.Item}
hidden={hidden}
ref={contextMenuTriggerRef}
style={style}>
<div className={styles.ExpandCollapseToggleSpacer} />
<span className={isEditable ? styles.EditableName : styles.Name}>
{name}
</span>
{isEditable ? (
<EditableValue
overrideValueFn={((overrideValueFn: any): OverrideValueFn)}
path={path}
value={value}
/>
) : (
<span className={styles.Value}>{displayValue}</span>
)}
</div>
);
} else if (
value.hasOwnProperty(meta.type) &&
!value.hasOwnProperty(meta.unserializable)
) {
children = (
<div
ref={contextMenuTriggerRef}
key="root"
className={styles.Item}
hidden={hidden}
style={style}>
{isInspectable ? (
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
) : (
<div className={styles.ExpandCollapseToggleSpacer} />
)}
<span
className={styles.Name}
onClick={isInspectable ? toggleIsOpen : undefined}>
{name}
</span>
<span
className={styles.Value}
onClick={isInspectable ? toggleIsOpen : undefined}>
{getMetaValueLabel(value)}
</span>
</div>
);
} else {
if (Array.isArray(value)) {
const hasChildren = value.length > 0;
const displayName = getMetaValueLabel(value);
children = value.map((innerValue, index) => (
<KeyValue
key={index}
alphaSort={alphaSort}
depth={depth + 1}
inspectPath={inspectPath}
isReadOnly={isReadOnly}
hidden={hidden || !isOpen}
name={index}
overrideValueFn={overrideValueFn}
path={path.concat(index)}
pathRoot={pathRoot}
value={value[index]}
/>
));
children.unshift(
<div
ref={contextMenuTriggerRef}
key={`${depth}-root`}
className={styles.Item}
hidden={hidden}
style={style}>
{hasChildren ? (
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
) : (
<div className={styles.ExpandCollapseToggleSpacer} />
)}
<span
className={styles.Name}
onClick={hasChildren ? toggleIsOpen : undefined}>
{name}
</span>
<span
className={styles.Value}
onClick={hasChildren ? toggleIsOpen : undefined}>
{displayName}
</span>
</div>,
);
} else {
// TRICKY
// It's important to use Object.entries() rather than Object.keys()
// because of the hidden meta Symbols used for hydration and unserializable values.
const entries = Object.entries(value);
if (alphaSort) {
entries.sort(alphaSortEntries);
}
const hasChildren = entries.length > 0;
const displayName = getMetaValueLabel(value);
let areChildrenReadOnly = isReadOnly || !!value[meta.readonly];
children = entries.map<Element<any>>(([key, keyValue]) => (
<KeyValue
key={key}
alphaSort={alphaSort}
depth={depth + 1}
inspectPath={inspectPath}
isReadOnly={areChildrenReadOnly}
hidden={hidden || !isOpen}
name={key}
overrideValueFn={overrideValueFn}
path={path.concat(key)}
pathRoot={pathRoot}
value={keyValue}
/>
));
children.unshift(
<div
ref={contextMenuTriggerRef}
key={`${depth}-root`}
className={styles.Item}
hidden={hidden}
style={style}>
{hasChildren ? (
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
) : (
<div className={styles.ExpandCollapseToggleSpacer} />
)}
<span
className={styles.Name}
onClick={hasChildren ? toggleIsOpen : undefined}>
{name}
</span>
<span
className={styles.Value}
onClick={hasChildren ? toggleIsOpen : undefined}>
{displayName}
</span>
</div>,
);
}
}
return children;
}