mirror of
https://github.com/facebook/react.git
synced 2026-02-26 02:44:59 +00:00
* 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
267 lines
7.1 KiB
JavaScript
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;
|
|
}
|