Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 81 additions & 7 deletions examples/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import React from "react";
import { Map } from "immutable";
import { JSONTree, KeyPath, areKeyPathsEqual } from "react-json-tree";
import { ScrollToPath } from "../../src/types";
import {
JSONTree,
KeyPath,
areKeyPathsEqual,
doesNodeContainNode,
ScrollToPath,
} from "react-json-tree";

const getItemString = (type: string) => (
<span>
Expand Down Expand Up @@ -31,6 +36,26 @@ const data: Record<string, any> = {
bool: true,
date: new Date(),
error: new Error(longString),
arrayOfObjects: [
Array.from({ length: 1000 }).map((e) => ({
foo: {
bar: "baz",
nested: {
bar: "baz2",
moreNested: {
bar: "baz3",
evenMoreNested: {
veryNested: {
insanelyNested: {
ridiculouslyDeepValue: "Hello",
},
},
},
},
},
},
})),
],
object: {
foo: {
bar: "baz",
Expand Down Expand Up @@ -108,11 +133,11 @@ const App = () => (
<JSONTree
data={data}
shouldExpandNodeInitially={(keyPath: KeyPath) => {
// Caller needs to ensure that parent node of scrollToPath is expanded for scrollTo to work on initial render, otherwise it will scroll to when the parent node/collection is expanded
return !!areKeyPathsEqual(
keyPath,
hugeArrayKeyPath.slice(keyPath.length * -1),
);
// Caller needs to ensure that parent node of scrollToPath is expanded for scrollTo to work on initial render, otherwise it will scroll to when the parent node/collection is expanded
return !!areKeyPathsEqual(
keyPath,
hugeArrayKeyPath.slice(keyPath.length * -1),
);
}}
scrollToPath={hugeArrayKeyPath}
/>
Expand Down Expand Up @@ -241,7 +266,56 @@ const App = () => (
}))}
/>
</div>

<h3>Shift + click to toggle visualizing all child nodes</h3>
<div style={{ background: "#222" }}>
<OnExpandExample />
</div>
<br />
</div>
);

function OnExpandExample() {
const [keyPathsToExpand, setKeyPathsToExpand] = React.useState<KeyPath[]>([]);
return (
<div>
<div style={{ color: "white" }}>
KeyPaths that will be expanded: {JSON.stringify(keyPathsToExpand)}
</div>

<JSONTree
shouldExpandNodeInitially={(keyPath, data, level) => {
for (let i = 0; i < keyPathsToExpand.length; i++) {
if (doesNodeContainNode(keyPathsToExpand[i], keyPath)) {
return true;
}
}

return level < 1;
}}
data={data}
onExpand={(e, keyPath, expanded) => {
if (e.ctrlKey || e.shiftKey || e.altKey) {
if (expanded) {
setKeyPathsToExpand([
...keyPathsToExpand.filter(
(existingKeyPath) =>
!areKeyPathsEqual(existingKeyPath, keyPath),
),
keyPath,
]);
} else {
if (keyPathsToExpand.length) {
setKeyPathsToExpand(
keyPathsToExpand.filter((k) => !areKeyPathsEqual(k, keyPath)),
);
}
}
}
}}
/>
</div>
);
}

export default App;
26 changes: 21 additions & 5 deletions src/ItemRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CircularCache,
CommonInternalProps,
KeyPath,
OnExpandEvent,
ScrollToPath,
} from "./types.js";
import styles from "./styles/itemRange.module.scss";
Expand All @@ -21,7 +22,15 @@ interface Props extends CommonInternalProps {
}

export default function ItemRange(props: Props) {
const { from, to, renderChildNodes, nodeType, scrollToPath, keyPath } = props;
const {
from,
to,
renderChildNodes,
nodeType,
scrollToPath,
keyPath,
onExpand,
} = props;
let initialExpanded = false;
if (scrollToPath) {
const [index] = scrollToPath;
Expand All @@ -35,9 +44,16 @@ export default function ItemRange(props: Props) {
}

const [expanded, setExpanded] = useState<boolean>(initialExpanded);
const handleClick = useCallback(() => {
setExpanded(!expanded);
}, [expanded]);
const handleClick = useCallback(
(e: OnExpandEvent) => {
setExpanded(!expanded);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unify implementations


if (onExpand !== undefined) {
onExpand(e, keyPath, expanded);
}
},
[expanded],
);

return expanded ? (
<div className={`${styles.itemRange}`}>
Expand All @@ -48,7 +64,7 @@ export default function ItemRange(props: Props) {
<JSONArrow
nodeType={nodeType}
expanded={false}
onClick={handleClick}
onNodeExpand={handleClick}
arrowStyle="double"
/>
{`${from} ... ${to}`}
Expand Down
11 changes: 6 additions & 5 deletions src/JSONArrow.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
import React, { EventHandler } from "react";
import styles from "./styles/JSONArrow.module.scss";
import { OnExpandEvent } from "./types.ts";

interface Props {
arrowStyle?: "single" | "double";
expanded: boolean;
nodeType: string;
onClick: () => void;
onNodeExpand: (e: OnExpandEvent) => void;
}

export default function JSONArrow({
arrowStyle = "single",
expanded,
onClick,
onNodeExpand,
}: Props) {
return (
<div
role={"button"}
aria-expanded={expanded}
tabIndex={0}
onClick={onClick}
onClick={onNodeExpand}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onClick();
onNodeExpand(event);
}
}}
className={`${styles.arrow} ${expanded ? styles.arrowExpanded : ""} ${arrowStyle === "single" ? styles.arrowArrowStyleSingle : styles.arrowArrowStyleDouble}`}
className={`${styles.arrow} ${expanded ? styles.arrowExpanded : ""}`}
>
{/* @todo let implementer define custom arrow object */}
{"\u25B6"}
Expand Down
31 changes: 23 additions & 8 deletions src/JSONNestedNode.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import React, { useCallback, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import JSONArrow from "./JSONArrow.js";
import getCollectionEntries from "./getCollectionEntries.js";
import JSONNode from "./JSONNode.js";
import ItemRange from "./ItemRange.js";
import type { CircularCache, CommonInternalProps } from "./types.js";
import {
CircularCache,
CommonInternalProps,
KeyPath,
OnExpandEvent,
ShouldExpandNode,
} from "./types.js";
import styles from "./styles/JSONNestedNode.module.scss";
import { NodeListItem } from "./components/NodeListItem.tsx";

Expand Down Expand Up @@ -116,6 +122,7 @@ export default function JSONNestedNode(props: Props) {
nodeTypeIndicator,
shouldExpandNodeInitially,
scrollToPath,
onExpand,
} = props;

const isRoot = keyPath[0] === "root";
Expand All @@ -129,9 +136,17 @@ export default function JSONNestedNode(props: Props) {
isCircular ? false : shouldExpandNodeInitially(keyPath, data, level),
);

const handleClick = useCallback(() => {
if (isNodeExpandable) setExpanded(!expanded);
}, [isNodeExpandable, expanded]);
const onNodeExpand = useCallback(
(e: OnExpandEvent) => {
if (isNodeExpandable) {
if (onExpand) {
onExpand(e, keyPath, !expanded);
}
setExpanded(!expanded);
}
},
[isNodeExpandable, expanded],
);

const renderedChildren =
expanded || (hideRoot && level === 0)
Expand Down Expand Up @@ -179,18 +194,18 @@ export default function JSONNestedNode(props: Props) {
<JSONArrow
nodeType={nodeType}
expanded={expanded}
onClick={handleClick}
onNodeExpand={onNodeExpand}
/>
)}
<span
data-nodetype={nodeType}
data-keypath={keyPath[0]}
className={`${styles.nestedNodeLabel} ${expanded ? styles.nestedNodeLabelExpanded : ""} ${isNodeExpandable ? styles.nestedNodeLabelExpandable : ""}`}
onClick={handleClick}
onClick={onNodeExpand}
>
{labelRenderer(...stylingArgs)}
</span>
<span className={styles.nestedNodeItemString} onClick={handleClick}>
<span className={styles.nestedNodeItemString} onClick={onNodeExpand}>
{renderedItemString}
</span>
</span>
Expand Down
1 change: 1 addition & 0 deletions src/JSONNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default function JSONNode({
valueRenderer,
isCustomNode,
valueWrap,

scrollToPath,
...rest
}: Props) {
Expand Down
19 changes: 19 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function JSONTree({
sortObjectKeys = false,
scrollToPath,
valueWrap = '"',
onExpand = undefined,
}: JSONTreeProps) {
return (
<ul
Expand All @@ -80,6 +81,7 @@ export function JSONTree({
collectionLimit={collectionLimit}
sortObjectKeys={sortObjectKeys}
valueWrap={valueWrap}
onExpand={onExpand}
/>
</ul>
);
Expand All @@ -97,6 +99,22 @@ export const areKeyPathsEqual = (a: KeyPath, b: KeyPath) => {
return true;
};

/**
* Returns true if source is a child node of target
* Remember child nodes have a longer key path
*/
export const doesNodeContainNode = (
parent: KeyPath,
child: KeyPath,
): boolean => {
for (let i = parent.length - 1; i >= 0; i--) {
if (parent[parent.length - i] !== child[child.length - i]) {
return false;
}
}
return true;
};

export type {
Key,
KeyPath,
Expand All @@ -108,4 +126,5 @@ export type {
IsCustomNode,
SortObjectKeys,
CommonExternalProps,
ScrollToPath,
} from "./types.js";
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export type ShouldExpandNodeInitially = (
level: number,
) => boolean;

export type ShouldExpandNode = (
keyPath: KeyPath,
data: unknown,
level: number,
) => boolean | undefined;

export type PostprocessValue = (value: unknown) => unknown;

export type IsCustomNode = (value: unknown) => boolean;
Expand All @@ -48,10 +54,16 @@ export type SortObjectKeys = ((a: unknown, b: unknown) => number) | boolean;

export type CircularCache = unknown[];

export type OnExpandEvent =
| React.MouseEvent<HTMLDivElement>
| React.KeyboardEvent<HTMLDivElement>
| React.MouseEvent<HTMLSpanElement>;

export interface CommonExternalProps {
keyPath: KeyPath;
labelRenderer: LabelRenderer;
valueRenderer: ValueRenderer;
// Sets expanded state on initial render, will not update expanded state on subsequent renders to prevent changing nodes user has interacted with already
shouldExpandNodeInitially: ShouldExpandNodeInitially;
hideRoot: boolean;
hideRootExpand: boolean;
Expand All @@ -62,6 +74,7 @@ export interface CommonExternalProps {
sortObjectKeys: SortObjectKeys;
valueWrap: string;
scrollToPath?: ScrollToPath;
onExpand?: (event: OnExpandEvent, keyPath: KeyPath, expand: boolean) => void;
}

export interface CommonInternalProps extends CommonExternalProps {
Expand Down