diff --git a/.changeset/modern-masks-fetch.md b/.changeset/modern-masks-fetch.md
new file mode 100644
index 0000000000..7b52ed8d01
--- /dev/null
+++ b/.changeset/modern-masks-fetch.md
@@ -0,0 +1,5 @@
+---
+'react-json-tree': minor
+---
+
+Add expand/collapse all feature
diff --git a/packages/react-json-tree/README.md b/packages/react-json-tree/README.md
index 7a55b3e110..7577d62f8b 100644
--- a/packages/react-json-tree/README.md
+++ b/packages/react-json-tree/README.md
@@ -137,6 +137,21 @@ Their full signatures are:
- `labelRenderer: function(keyPath, nodeType, expanded, expandable)`
- `valueRenderer: function(valueAsString, value, ...keyPath)`
+#### Customize "Expand All/Collapse All" Buttons
+
+Passing the `expandCollapseAll` props will activate in the top right corner of the JSONTree component the `expand all/collapse all` buttons. You can pass a JSON to customize the expand all/collapse all icons. The default icons are from [FontAwesome](https://fontawesome.com/).
+
+```jsx
+
+```
+
#### More Options
- `shouldExpandNodeInitially: function(keyPath, data, level)` - determines if node should be expanded when it first renders (root is expanded by default)
diff --git a/packages/react-json-tree/examples/package.json b/packages/react-json-tree/examples/package.json
index af9b7394d1..883e2616b7 100644
--- a/packages/react-json-tree/examples/package.json
+++ b/packages/react-json-tree/examples/package.json
@@ -23,7 +23,7 @@
"react": "^18.2.0",
"react-base16-styling": "^0.9.1",
"react-dom": "^18.2.0",
- "react-json-tree": "^0.18.0"
+ "react-json-tree": "link:.."
},
"devDependencies": {
"@babel/core": "^7.21.4",
diff --git a/packages/react-json-tree/examples/src/App.tsx b/packages/react-json-tree/examples/src/App.tsx
index a972ce5888..8d8f6e9f40 100644
--- a/packages/react-json-tree/examples/src/App.tsx
+++ b/packages/react-json-tree/examples/src/App.tsx
@@ -190,7 +190,12 @@ const App = () => (
Sort object keys with sortObjectKeys
prop.
-
+
Collapsed root node
diff --git a/packages/react-json-tree/package.json b/packages/react-json-tree/package.json
index 5190e75517..8bee4142fc 100644
--- a/packages/react-json-tree/package.json
+++ b/packages/react-json-tree/package.json
@@ -46,6 +46,9 @@
},
"dependencies": {
"@babel/runtime": "^7.21.0",
+ "@fortawesome/fontawesome-svg-core": "^6.4.0",
+ "@fortawesome/free-solid-svg-icons": "^6.4.0",
+ "@fortawesome/react-fontawesome": "^0.2.0",
"@types/lodash": "^4.14.194",
"react-base16-styling": "^0.9.1"
},
diff --git a/packages/react-json-tree/src/JSONNestedNode.tsx b/packages/react-json-tree/src/JSONNestedNode.tsx
index 0e5b5be52c..30f6c464d0 100644
--- a/packages/react-json-tree/src/JSONNestedNode.tsx
+++ b/packages/react-json-tree/src/JSONNestedNode.tsx
@@ -1,8 +1,9 @@
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useRef, useState } from 'react';
+import ItemRange from './ItemRange';
import JSONArrow from './JSONArrow';
-import getCollectionEntries from './getCollectionEntries';
import JSONNode from './JSONNode';
-import ItemRange from './ItemRange';
+import { useExpandCollapseAllContext } from './expandCollapseContext';
+import getCollectionEntries from './getCollectionEntries';
import type { CircularCache, CommonInternalProps } from './types';
/**
@@ -112,23 +113,61 @@ export default function JSONNestedNode(props: Props) {
shouldExpandNodeInitially,
styling,
} = props;
+ const { expandAllState, setExpandAllState, setEnableDefaultButton } =
+ useExpandCollapseAllContext();
- const [expanded, setExpanded] = useState
(
+ const [defaultExpanded] = useState(
// calculate individual node expansion if necessary
- isCircular ? false : shouldExpandNodeInitially(keyPath, data, level)
+ isCircular
+ ? false
+ : (function getDefault() {
+ switch (expandAllState) {
+ case 'expand':
+ return true;
+ case 'collapse':
+ return false;
+ default:
+ return shouldExpandNodeInitially(keyPath, data, level);
+ }
+ })()
);
+ const [, setTriggerReRender] = useState(defaultExpanded);
+
+ /**
+ * Used the useRef to handle expanded because calling a setState in a recursive implementation
+ * could lead to a "Maximum update depth exceeded" error */
+ const expandedRef = useRef(defaultExpanded);
+
+ switch (expandAllState) {
+ case 'expand':
+ expandedRef.current = isCircular ? false : true;
+ break;
+ case 'collapse':
+ expandedRef.current = false;
+ break;
+ case 'default':
+ expandedRef.current = shouldExpandNodeInitially(keyPath, data, level);
+ break;
+ default: //Do nothing;
+ }
+
const handleClick = useCallback(() => {
- if (expandable) setExpanded(!expanded);
- }, [expandable, expanded]);
+ if (expandable) {
+ expandedRef.current = !expandedRef.current;
+ setTriggerReRender((e) => !e);
+ setEnableDefaultButton(true);
+ setExpandAllState(undefined);
+ }
+ }, [expandable, setEnableDefaultButton, setExpandAllState]);
const renderedChildren =
- expanded || (hideRoot && level === 0)
+ expandedRef.current || (hideRoot && level === 0)
? renderChildNodes({ ...props, circularCache, level: level + 1 })
: null;
const itemType = (
-
+
{nodeTypeIndicator}
);
@@ -137,9 +176,15 @@ export default function JSONNestedNode(props: Props) {
data,
itemType,
createItemString(data, collectionLimit),
- keyPath
+ keyPath,
+ expandedRef.current
);
- const stylingArgs = [keyPath, nodeType, expanded, expandable] as const;
+ const stylingArgs = [
+ keyPath,
+ nodeType,
+ expandedRef.current,
+ expandable,
+ ] as const;
return hideRoot ? (
@@ -153,7 +198,7 @@ export default function JSONNestedNode(props: Props) {
)}
diff --git a/packages/react-json-tree/src/createStylingFromTheme.ts b/packages/react-json-tree/src/createStylingFromTheme.ts
index 5c776c7e85..2be9fe2f59 100644
--- a/packages/react-json-tree/src/createStylingFromTheme.ts
+++ b/packages/react-json-tree/src/createStylingFromTheme.ts
@@ -46,6 +46,7 @@ const getDefaultThemeStyling = (theme: Base16Theme): StylingConfig => {
return {
tree: {
+ position: 'relative',
border: 0,
padding: 0,
marginTop: '0.5em',
@@ -58,6 +59,19 @@ const getDefaultThemeStyling = (theme: Base16Theme): StylingConfig => {
backgroundColor: colors.BACKGROUND_COLOR,
},
+ expandCollapseAll: {
+ color: colors.TEXT_COLOR,
+ backgroundColor: colors.BACKGROUND_COLOR,
+ position: 'absolute',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: '1rem',
+ top: '1rem',
+ right: '1rem',
+ cursor: 'pointer',
+ },
+
value: ({ style }, nodeType, keyPath) => ({
style: {
...style,
diff --git a/packages/react-json-tree/src/expandCollapseButtons.tsx b/packages/react-json-tree/src/expandCollapseButtons.tsx
new file mode 100644
index 0000000000..e60b9c271f
--- /dev/null
+++ b/packages/react-json-tree/src/expandCollapseButtons.tsx
@@ -0,0 +1,175 @@
+import {
+ faArrowDown,
+ faArrowRight,
+ faUndo,
+ faCopy,
+ faCheck,
+} from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import React, { ReactNode, useEffect, useState } from 'react';
+import { ExpandCollapseAll } from '.';
+import { useExpandCollapseAllContext } from './expandCollapseContext';
+import { StylingFunction } from 'react-base16-styling';
+
+interface Props {
+ expandCollapseAll: ExpandCollapseAll;
+ styling: StylingFunction;
+ value: unknown;
+}
+
+interface ExpandButtonProps {
+ expandableDefaultValue?: 'expand' | 'collapse';
+ expandIcon?: ReactNode;
+}
+
+interface CollapseButtonProps {
+ expandableDefaultValue?: 'expand' | 'collapse';
+ collapseIcon?: ReactNode;
+}
+
+interface CopyToClipboardButtonProps {
+ copyToClipboardIcon?: ReactNode;
+ copiedToClipboardIcon?: ReactNode;
+ value: unknown;
+}
+
+interface DefaultButtonProps {
+ defaultIcon?: ReactNode;
+}
+
+function ExpandCollapseButtons({ expandCollapseAll, styling, value }: Props) {
+ const { enableDefaultButton } = useExpandCollapseAllContext();
+
+ const expandableDefaultValue = expandCollapseAll?.defaultValue || 'expand';
+
+ return (
+
+ {enableDefaultButton && (
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+function ExpandButton({
+ expandableDefaultValue,
+ expandIcon,
+}: ExpandButtonProps) {
+ const { expandAllState, setExpandAllState, setEnableDefaultButton } =
+ useExpandCollapseAllContext();
+
+ const onExpand = () => {
+ setExpandAllState('expand');
+ setEnableDefaultButton(true);
+ };
+
+ const isDefault = !expandAllState || expandAllState === 'default';
+
+ if (
+ expandAllState === 'collapse' ||
+ (isDefault && expandableDefaultValue === 'expand')
+ ) {
+ return (
+
+ {expandIcon || }
+
+ );
+ }
+
+ return <>>;
+}
+
+function CollapseButton({
+ expandableDefaultValue,
+ collapseIcon,
+}: CollapseButtonProps) {
+ const { expandAllState, setExpandAllState, setEnableDefaultButton } =
+ useExpandCollapseAllContext();
+
+ const onCollapse = () => {
+ setExpandAllState('collapse');
+ setEnableDefaultButton(true);
+ };
+
+ const isDefault = !expandAllState || expandAllState === 'default';
+
+ if (
+ expandAllState === 'expand' ||
+ (isDefault && expandableDefaultValue === 'collapse')
+ ) {
+ return (
+
+ {collapseIcon || }
+
+ );
+ }
+
+ return <>>;
+}
+
+function CopyToClipboardButton({
+ copyToClipboardIcon,
+ copiedToClipboardIcon,
+ value,
+}: CopyToClipboardButtonProps) {
+ const [isCopied, setIsCopied] = useState(false);
+
+ const handleOnCopyToClipboard = async () => {
+ await navigator.clipboard.writeText(JSON.stringify(value, null, 2));
+ setIsCopied(true);
+ };
+
+ useEffect(() => {
+ if (isCopied) {
+ setTimeout(() => setIsCopied(false), 6000);
+ }
+ }, [isCopied]);
+
+ if (isCopied) {
+ return (
+
+ {copiedToClipboardIcon || }
+
+ );
+ }
+
+ return (
+
+ {copyToClipboardIcon || }
+
+ );
+}
+
+function DefaultButton({ defaultIcon }: DefaultButtonProps) {
+ const { setExpandAllState, setEnableDefaultButton } =
+ useExpandCollapseAllContext();
+
+ const onDefaultCollapse = () => {
+ setExpandAllState('default');
+ setEnableDefaultButton(false);
+ };
+
+ return (
+
+ {defaultIcon || }
+
+ );
+}
+
+export default ExpandCollapseButtons;
diff --git a/packages/react-json-tree/src/expandCollapseContext.tsx b/packages/react-json-tree/src/expandCollapseContext.tsx
new file mode 100644
index 0000000000..0fbc8bf99f
--- /dev/null
+++ b/packages/react-json-tree/src/expandCollapseContext.tsx
@@ -0,0 +1,64 @@
+import React, {
+ ReactNode,
+ createContext,
+ useContext,
+ useMemo,
+ useState,
+} from 'react';
+import { ExpandCollapseAll } from '.';
+import ExpandCollapseButtons from './expandCollapseButtons';
+import { StylingFunction } from 'react-base16-styling';
+
+interface Context {
+ enableDefaultButton: boolean;
+ setEnableDefaultButton: any;
+ expandAllState?: 'expand' | 'collapse' | 'default';
+ setExpandAllState: any;
+}
+
+interface Props {
+ children: ReactNode;
+ expandCollapseAll?: ExpandCollapseAll;
+ styling: StylingFunction;
+ value: unknown;
+}
+
+const ExpandCollapseAllContext = createContext({} as Context);
+
+function ExpandCollapseAllContextProvider({
+ expandCollapseAll,
+ children,
+ styling,
+ value,
+}: Props) {
+ const [enableDefaultButton, setEnableDefaultButton] = useState(false);
+ const [expandAllState, setExpandAllState] = useState();
+
+ const contextValue = useMemo(
+ () => ({
+ enableDefaultButton,
+ setEnableDefaultButton,
+ expandAllState,
+ setExpandAllState,
+ }),
+ [enableDefaultButton, expandAllState]
+ );
+
+ return (
+
+ {children}
+ {expandCollapseAll && (
+
+ )}
+
+ );
+}
+
+export const useExpandCollapseAllContext = () =>
+ useContext(ExpandCollapseAllContext);
+
+export default ExpandCollapseAllContextProvider;
diff --git a/packages/react-json-tree/src/index.tsx b/packages/react-json-tree/src/index.tsx
index 26fab0fc4b..eb0df18d70 100644
--- a/packages/react-json-tree/src/index.tsx
+++ b/packages/react-json-tree/src/index.tsx
@@ -3,11 +3,13 @@
// Dave Vedder http://www.eskimospy.com/
// port by Daniele Zannotti http://www.github.com/dzannotti
-import React, { useMemo } from 'react';
+import React, { ReactNode, useMemo } from 'react';
import JSONNode from './JSONNode';
import createStylingFromTheme from './createStylingFromTheme';
import { invertTheme } from 'react-base16-styling';
import type { StylingValue, Theme } from 'react-base16-styling';
+import ExpandCollapseAllButtonsContext from './expandCollapseContext';
+
import type {
CommonExternalProps,
GetItemString,
@@ -20,6 +22,16 @@ interface Props extends Partial {
data: unknown;
theme?: Theme;
invertTheme?: boolean;
+ expandCollapseAll?: ExpandCollapseAll;
+}
+
+interface ExpandCollapseAll {
+ defaultValue?: 'expand' | 'collapse';
+ expandIcon?: ReactNode;
+ collapseIcon?: ReactNode;
+ copyToClipboardIcon?: ReactNode;
+ copiedToClipboardIcon?: ReactNode;
+ defaultIcon?: ReactNode;
}
const identity = (value: any) => value;
@@ -41,6 +53,7 @@ export function JSONTree({
labelRenderer = defaultLabelRenderer,
valueRenderer = identity,
shouldExpandNodeInitially = expandRootNode,
+ expandCollapseAll,
hideRoot = false,
getItemString = defaultItemString,
postprocessValue = identity,
@@ -56,20 +69,26 @@ export function JSONTree({
return (
-
+ value={value}
+ >
+
+
);
}
@@ -87,4 +106,4 @@ export type {
Styling,
CommonExternalProps,
} from './types';
-export type { StylingValue };
+export type { ExpandCollapseAll, StylingValue };
diff --git a/packages/react-json-tree/src/types.ts b/packages/react-json-tree/src/types.ts
index 6a67f376c9..26894388e2 100644
--- a/packages/react-json-tree/src/types.ts
+++ b/packages/react-json-tree/src/types.ts
@@ -10,7 +10,8 @@ export type GetItemString = (
data: unknown,
itemType: React.ReactNode,
itemString: string,
- keyPath: KeyPath
+ keyPath: KeyPath,
+ isExpanded: boolean
) => React.ReactNode;
export type LabelRenderer = (
diff --git a/packages/react-json-tree/test/index.spec.tsx b/packages/react-json-tree/test/index.spec.tsx
index 996612913e..136b64a974 100644
--- a/packages/react-json-tree/test/index.spec.tsx
+++ b/packages/react-json-tree/test/index.spec.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { createRenderer } from 'react-test-renderer/shallow';
import { JSONTree } from '../src/index';
-import JSONNode from '../src/JSONNode';
+import ExpandCollapseAllContext from '../src/expandCollapseContext';
const BASIC_DATA = { a: 1, b: 'c' };
@@ -17,6 +17,6 @@ describe('JSONTree', () => {
const result = render();
expect(result.type).toBe('ul');
- expect(result.props.children.type.name).toBe(JSONNode.name);
+ expect(result.props.children.type.name).toBe(ExpandCollapseAllContext.name);
});
});