/**
* 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 {copy} from 'clipboard-js';
import React, {Fragment, useCallback, useContext} from 'react';
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
import {BridgeContext, ContextMenuContext, StoreContext} from '../context';
import ContextMenu from '../../ContextMenu/ContextMenu';
import ContextMenuItem from '../../ContextMenu/ContextMenuItem';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Icon from '../Icon';
import HooksTree from './HooksTree';
import {ModalDialogContext} from '../ModalDialog';
import HocBadges from './HocBadges';
import InspectedElementTree from './InspectedElementTree';
import {InspectedElementContext} from './InspectedElementContext';
import ViewElementSourceContext from './ViewElementSourceContext';
import NativeStyleEditor from './NativeStyleEditor';
import Toggle from '../Toggle';
import Badge from './Badge';
import {
ComponentFilterElementType,
ElementTypeClass,
ElementTypeForwardRef,
ElementTypeFunction,
ElementTypeMemo,
ElementTypeSuspense,
} from 'react-devtools-shared/src/types';
import styles from './SelectedElement.css';
import type {
CopyInspectedElementPath,
GetInspectedElementPath,
StoreAsGlobal,
} from './InspectedElementContext';
import type {Element, InspectedElement} from './types';
import type {ElementType} from 'react-devtools-shared/src/types';
export type Props = {||};
export default function SelectedElement(_: Props) {
const {inspectedElementID} = useContext(TreeStateContext);
const dispatch = useContext(TreeDispatcherContext);
const {canViewElementSourceFunction, viewElementSourceFunction} = useContext(
ViewElementSourceContext,
);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const {dispatch: modalDialogDispatch} = useContext(ModalDialogContext);
const {
copyInspectedElementPath,
getInspectedElementPath,
getInspectedElement,
storeAsGlobal,
viewInspectedElementPath,
} = useContext(InspectedElementContext);
const element =
inspectedElementID !== null
? store.getElementByID(inspectedElementID)
: null;
const inspectedElement =
inspectedElementID != null ? getInspectedElement(inspectedElementID) : null;
const highlightElement = useCallback(
() => {
if (element !== null && inspectedElementID !== null) {
const rendererID = store.getRendererIDForElement(inspectedElementID);
if (rendererID !== null) {
bridge.send('highlightNativeElement', {
displayName: element.displayName,
hideAfterTimeout: true,
id: inspectedElementID,
openNativeElementsPanel: true,
rendererID,
scrollIntoView: true,
});
}
}
},
[bridge, element, inspectedElementID, store],
);
const logElement = useCallback(
() => {
if (inspectedElementID !== null) {
const rendererID = store.getRendererIDForElement(inspectedElementID);
if (rendererID !== null) {
bridge.send('logElementToConsole', {
id: inspectedElementID,
rendererID,
});
}
}
},
[bridge, inspectedElementID, store],
);
const viewSource = useCallback(
() => {
if (viewElementSourceFunction != null && inspectedElement !== null) {
viewElementSourceFunction(
inspectedElement.id,
((inspectedElement: any): InspectedElement),
);
}
},
[inspectedElement, viewElementSourceFunction],
);
// In some cases (e.g. FB internal usage) the standalone shell might not be able to view the source.
// To detect this case, we defer to an injected helper function (if present).
const canViewSource =
inspectedElement !== null &&
inspectedElement.canViewSource &&
viewElementSourceFunction !== null &&
(canViewElementSourceFunction === null ||
canViewElementSourceFunction(inspectedElement));
const isSuspended =
element !== null &&
element.type === ElementTypeSuspense &&
inspectedElement != null &&
inspectedElement.state != null;
const canToggleSuspense =
inspectedElement != null && inspectedElement.canToggleSuspense;
// TODO (suspense toggle) Would be nice to eventually use a two setState pattern here as well.
const toggleSuspended = useCallback(
() => {
let nearestSuspenseElement = null;
let currentElement = element;
while (currentElement !== null) {
if (currentElement.type === ElementTypeSuspense) {
nearestSuspenseElement = currentElement;
break;
} else if (currentElement.parentID > 0) {
currentElement = store.getElementByID(currentElement.parentID);
} else {
currentElement = null;
}
}
// If we didn't find a Suspense ancestor, we can't suspend.
// Instead we can show a warning to the user.
if (nearestSuspenseElement === null) {
modalDialogDispatch({
type: 'SHOW',
content: ,
});
} else {
const nearestSuspenseElementID = nearestSuspenseElement.id;
// If we're suspending from an arbitary (non-Suspense) component, select the nearest Suspense element in the Tree.
// This way when the fallback UI is shown and the current element is hidden, something meaningful is selected.
if (nearestSuspenseElement !== element) {
dispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: nearestSuspenseElementID,
});
}
const rendererID = store.getRendererIDForElement(
nearestSuspenseElementID,
);
// Toggle suspended
if (rendererID !== null) {
bridge.send('overrideSuspense', {
id: nearestSuspenseElementID,
rendererID,
forceFallback: !isSuspended,
});
}
}
},
[bridge, dispatch, element, isSuspended, modalDialogDispatch, store],
);
if (element === null) {
return (
);
}
return (
{canToggleSuspense && (
)}
{store.supportsNativeInspection && (
)}
{inspectedElement === null && (
Loading...
)}
{inspectedElement !== null && (
)}
);
}
export type CopyPath = (path: Array) => void;
export type InspectPath = (path: Array) => void;
type InspectedElementViewProps = {|
copyInspectedElementPath: CopyInspectedElementPath,
element: Element,
getInspectedElementPath: GetInspectedElementPath,
inspectedElement: InspectedElement,
storeAsGlobal: StoreAsGlobal,
|};
const IS_SUSPENDED = 'Suspended';
function InspectedElementView({
copyInspectedElementPath,
element,
getInspectedElementPath,
inspectedElement,
storeAsGlobal,
viewInspectedElementPath,
}: InspectedElementViewProps) {
const {id, type} = element;
const {
canEditFunctionProps,
canEditHooks,
canToggleSuspense,
hasLegacyContext,
context,
hooks,
owners,
props,
source,
state,
} = inspectedElement;
const {ownerID} = useContext(TreeStateContext);
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const {
isEnabledForInspectedElement,
supportsCopyOperation,
viewAttributeSourceFunction,
} = useContext(ContextMenuContext);
const inspectContextPath = useCallback(
(path: Array) => {
getInspectedElementPath(id, ['context', ...path]);
},
[getInspectedElementPath, id],
);
const inspectPropsPath = useCallback(
(path: Array) => {
getInspectedElementPath(id, ['props', ...path]);
},
[getInspectedElementPath, id],
);
const inspectStatePath = useCallback(
(path: Array) => {
getInspectedElementPath(id, ['state', ...path]);
},
[getInspectedElementPath, id],
);
let overrideContextFn = null;
let overridePropsFn = null;
let overrideStateFn = null;
let overrideSuspenseFn = null;
if (type === ElementTypeClass) {
overrideContextFn = (path: Array, value: any) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
bridge.send('overrideContext', {id, path, rendererID, value});
}
};
overridePropsFn = (path: Array, value: any) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
bridge.send('overrideProps', {id, path, rendererID, value});
}
};
overrideStateFn = (path: Array, value: any) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
bridge.send('overrideState', {id, path, rendererID, value});
}
};
} else if (
(type === ElementTypeFunction ||
type === ElementTypeMemo ||
type === ElementTypeForwardRef) &&
canEditFunctionProps
) {
overridePropsFn = (path: Array, value: any) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
bridge.send('overrideProps', {id, path, rendererID, value});
}
};
} else if (type === ElementTypeSuspense && canToggleSuspense) {
overrideSuspenseFn = (path: Array, value: boolean) => {
if (path.length !== 1 && path !== IS_SUSPENDED) {
throw new Error('Unexpected path.');
}
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
bridge.send('overrideSuspense', {
id,
rendererID,
forceFallback: value,
});
}
};
}
return (
{type === ElementTypeSuspense ? (
) : (
)}
{ownerID === null &&
owners !== null &&
owners.length > 0 && (
rendered by
{owners.map(owner => (
))}
)}
{source !== null && (
)}
{isEnabledForInspectedElement && (
{data => (
{supportsCopyOperation && (
copyInspectedElementPath(id, data.path)}
title="Copy value to clipboard">
Copy
value to clipboard
)}
storeAsGlobal(id, data.path)}
title="Store as global variable">
{' '}
Store as global variable
{viewAttributeSourceFunction !== null &&
data.type === 'function' && (
viewAttributeSourceFunction(id, data.path)}
title="Go to definition">
Go
to definition
)}
)}
)}
);
}
// This function is based on packages/shared/describeComponentFrame.js
function formatSourceForDisplay(fileName: string, lineNumber: string) {
const BEFORE_SLASH_RE = /^(.*)[\\\/]/;
let nameOnly = fileName.replace(BEFORE_SLASH_RE, '');
// In DEV, include code for a common special case:
// prefer "folder/index.js" instead of just "index.js".
if (/^index\./.test(nameOnly)) {
const match = fileName.match(BEFORE_SLASH_RE);
if (match) {
const pathBeforeSlash = match[1];
if (pathBeforeSlash) {
const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');
nameOnly = folderName + '/' + nameOnly;
}
}
}
return `${nameOnly}:${lineNumber}`;
}
type SourceProps = {|
fileName: string,
lineNumber: string,
|};
function Source({fileName, lineNumber}: SourceProps) {
const handleCopy = () => copy(`${fileName}:${lineNumber}`);
return (
{formatSourceForDisplay(fileName, lineNumber)}
);
}
type OwnerViewProps = {|
displayName: string,
hocDisplayNames: Array | null,
id: number,
isInStore: boolean,
type: ElementType,
|};
function OwnerView({
displayName,
hocDisplayNames,
id,
isInStore,
type,
}: OwnerViewProps) {
const dispatch = useContext(TreeDispatcherContext);
const handleClick = useCallback(
() =>
dispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: id,
}),
[dispatch, id],
);
return (
{displayName}
);
}
function CannotSuspendWarningMessage() {
const store = useContext(StoreContext);
const areSuspenseElementsHidden = !!store.componentFilters.find(
filter =>
filter.type === ComponentFilterElementType &&
filter.value === ElementTypeSuspense &&
filter.isEnabled,
);
// Has the user filted out Suspense nodes from the tree?
// If so, the selected element might actually be in a Suspense tree after all.
if (areSuspenseElementsHidden) {
return (
Suspended state cannot be toggled while Suspense components are hidden.
Disable the filter and try agan.
);
} else {
return (
The selected element is not within a Suspense container. Suspending it
would cause an error.
);
}
}