redux-devtools/packages/react-json-tree/src/JSONNestedNode.tsx

242 lines
5.9 KiB
TypeScript
Raw Normal View History

import React from 'react';
import PropTypes from 'prop-types';
import JSONArrow from './JSONArrow';
import getCollectionEntries from './getCollectionEntries';
import JSONNode from './JSONNode';
import ItemRange from './ItemRange';
import {
CircularPropsPassedThroughJSONNestedNode,
CircularPropsPassedThroughRenderChildNodes,
} from './types';
/**
* Renders nested values (eg. objects, arrays, lists, etc.)
*/
export interface RenderChildNodesProps
extends CircularPropsPassedThroughRenderChildNodes {
data: any;
nodeType: string;
}
interface Range {
from: number;
to: number;
}
interface Entry {
key: string | number;
value: any;
}
function isRange(rangeOrEntry: Range | Entry): rangeOrEntry is Range {
return (rangeOrEntry as Range).to !== undefined;
}
function renderChildNodes(
props: RenderChildNodesProps,
from?: number,
to?: number
) {
const {
nodeType,
data,
collectionLimit,
circularCache,
keyPath,
postprocessValue,
sortObjectKeys,
} = props;
const childNodes: React.ReactNode[] = [];
getCollectionEntries(
nodeType,
data,
sortObjectKeys,
collectionLimit,
from,
to
).forEach((entry) => {
if (isRange(entry)) {
childNodes.push(
<ItemRange
{...props}
key={`ItemRange--${entry.from}-${entry.to}`}
from={entry.from}
to={entry.to}
renderChildNodes={renderChildNodes}
/>
);
} else {
const { key, value } = entry;
const isCircular = circularCache.indexOf(value) !== -1;
childNodes.push(
<JSONNode
{...props}
{...{ postprocessValue, collectionLimit }}
key={`Node--${key}`}
keyPath={[key, ...keyPath]}
value={postprocessValue(value)}
circularCache={[...circularCache, value]}
isCircular={isCircular}
hideRoot={false}
/>
);
}
});
return childNodes;
}
interface Props extends CircularPropsPassedThroughJSONNestedNode {
data: any;
nodeType: string;
nodeTypeIndicator: string;
createItemString: (data: any, collectionLimit: number) => string;
expandable: boolean;
}
interface State {
expanded: boolean;
}
function getStateFromProps(props: Props) {
// calculate individual node expansion if necessary
const expanded =
props.shouldExpandNode && !props.isCircular
? props.shouldExpandNode(props.keyPath, props.data, props.level)
: false;
return {
expanded,
};
}
export default class JSONNestedNode extends React.Component<Props, State> {
static propTypes = {
getItemString: PropTypes.func.isRequired,
nodeTypeIndicator: PropTypes.any,
nodeType: PropTypes.string.isRequired,
data: PropTypes.any,
hideRoot: PropTypes.bool.isRequired,
createItemString: PropTypes.func.isRequired,
styling: PropTypes.func.isRequired,
collectionLimit: PropTypes.number,
keyPath: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number])
).isRequired,
labelRenderer: PropTypes.func.isRequired,
shouldExpandNode: PropTypes.func,
level: PropTypes.number.isRequired,
sortObjectKeys: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
isCircular: PropTypes.bool,
expandable: PropTypes.bool,
};
static defaultProps = {
data: [],
circularCache: [],
level: 0,
expandable: true,
};
constructor(props: Props) {
super(props);
this.state = getStateFromProps(props);
}
UNSAFE_componentWillReceiveProps(nextProps: Props) {
const nextState = getStateFromProps(nextProps);
if (getStateFromProps(this.props).expanded !== nextState.expanded) {
this.setState(nextState);
}
}
shouldComponentUpdate(nextProps: Props, nextState: State) {
return (
!!Object.keys(nextProps).find(
(key) =>
key !== 'circularCache' &&
(key === 'keyPath'
? nextProps[key].join('/') !== this.props[key].join('/')
: nextProps[key as keyof Props] !== this.props[key as keyof Props])
) || nextState.expanded !== this.state.expanded
);
}
render() {
const {
getItemString,
nodeTypeIndicator,
nodeType,
data,
hideRoot,
createItemString,
styling,
collectionLimit,
keyPath,
labelRenderer,
expandable,
} = this.props;
const { expanded } = this.state;
const renderedChildren =
expanded || (hideRoot && this.props.level === 0)
? renderChildNodes({ ...this.props, level: this.props.level + 1 })
: null;
const itemType = (
<span {...styling('nestedNodeItemType', expanded)}>
{nodeTypeIndicator}
</span>
);
const renderedItemString = getItemString(
nodeType,
data,
itemType,
createItemString(data, collectionLimit),
keyPath
);
const stylingArgs = [keyPath, nodeType, expanded, expandable] as const;
return hideRoot ? (
<li {...styling('rootNode', ...stylingArgs)}>
<ul {...styling('rootNodeChildren', ...stylingArgs)}>
{renderedChildren}
</ul>
</li>
) : (
<li {...styling('nestedNode', ...stylingArgs)}>
{expandable && (
<JSONArrow
styling={styling}
nodeType={nodeType}
expanded={expanded}
onClick={this.handleClick}
/>
)}
<label
{...styling(['label', 'nestedNodeLabel'], ...stylingArgs)}
onClick={this.handleClick}
>
{labelRenderer(...stylingArgs)}
</label>
<span
{...styling('nestedNodeItemString', ...stylingArgs)}
onClick={this.handleClick}
>
{renderedItemString}
</span>
<ul {...styling('nestedNodeChildren', ...stylingArgs)}>
{renderedChildren}
</ul>
</li>
);
}
handleClick = () => {
if (this.props.expandable) {
this.setState({ expanded: !this.state.expanded });
}
};
}