Skip to content

Commit 60a9525

Browse files
committed
feat: add enableClipboard props.
1 parent 9abada3 commit 60a9525

File tree

8 files changed

+161
-57
lines changed

8 files changed

+161
-57
lines changed

core/README.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ react-json-view
33

44
[![CI](https://github.com/uiwjs/react-json-view/actions/workflows/ci.yml/badge.svg)](https://github.com/uiwjs/react-json-view/actions/workflows/ci.yml)
55
[![npm version](https://img.shields.io/npm/v/@uiw/react-json-view.svg)](https://www.npmjs.com/package/@uiw/react-json-view)
6+
[![react@^18](https://shields.io/badge/react-^18-green?style=flat&logo=react)](https://github.com/facebook/react/releases)
67

78
A React component for displaying and editing javascript arrays and JSON objects.
89

@@ -106,9 +107,11 @@ const customTheme = {
106107
'--w-rjv-font-family': 'monospace',
107108
'--w-rjv-color': '#9cdcfe',
108109
'--w-rjv-background-color': '#1e1e1e',
109-
'--w-rjv-border-left': '1px solid #323232',
110+
'--w-rjv-line-color': '#323232',
110111
'--w-rjv-arrow-color': 'var(--w-rjv-color)',
111112
'--w-rjv-info-color': '#656565',
113+
'--w-rjv-copied-color': '#9cdcfe',
114+
'--w-rjv-copied-success-color': '#28a745',
112115

113116
'--w-rjv-curlybraces-color': '#d4d4d4',
114117
'--w-rjv-brackets-color': '#d4d4d4',
@@ -165,9 +168,11 @@ const object = {
165168
const customTheme = {
166169
'--w-rjv-color': '#9cdcfe',
167170
'--w-rjv-background-color': '#1e1e1e',
168-
'--w-rjv-border-left': '1px solid #323232',
169-
'--w-rjv-arrow-color': 'var(--w-rjv-color)',
171+
'--w-rjv-line-color': '#323232',
172+
'--w-rjv-arrow-color': '#9cdcfe',
170173
'--w-rjv-info-color': '#656565',
174+
'--w-rjv-copied-color': '#0184a6',
175+
'--w-rjv-copied-success-color': '#28a745',
171176

172177
'--w-rjv-curlybraces-color': '#d4d4d4',
173178
'--w-rjv-brackets-color': '#d4d4d4',
@@ -188,9 +193,8 @@ export default function Demo() {
188193
const [hex, setHex] = useState("#1e1e1e");
189194
const [theme, setTheme] = useState(customTheme);
190195
const onChange = ({ hexa }) => {
191-
const value = cssvar === '--w-rjv-border-left' ? `1px solid ${hexa}` : hexa;
192196
setHex(hexa);
193-
setTheme({ ...theme, [cssvar]: value });
197+
setTheme({ ...theme, [cssvar]: hexa });
194198
};
195199
return (
196200
<React.Fragment>
@@ -200,7 +204,10 @@ export default function Demo() {
200204
<Colorful color={hex} onChange={onChange} />
201205
<div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap' }}>
202206
{Object.keys(customTheme).map((varname, idx) => {
203-
const click = () => setCssvar(varname);
207+
const click = () => {
208+
setCssvar(varname);
209+
setHex(customTheme[varname]);
210+
};
204211
const active = cssvar === varname ? '#a8a8a8' : '';
205212
return <button key={idx} style={{ background: active }} onClick={click}>{varname}</button>
206213
})}
@@ -375,14 +382,17 @@ import { MetaProps, SemicolonProps, EllipsisProps, ValueViewProps } from '@uiw/r
375382
export interface JsonViewProps<T> extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
376383
/** This property contains your input JSON */
377384
value?: T;
378-
/** Set the indent-width for nested objects @default `15`*/
385+
/** Set the indent-width for nested objects @default 15 */
379386
indentWidth?: number;
380-
/** When set to `true`, data type labels prefix values @default `true` */
387+
/** When set to `true`, data type labels prefix values @default true */
381388
displayDataTypes?: boolean;
382-
/** When set to `true`, `objects` and `arrays` are labeled with size @default `true` */
389+
/** When set to `true`, `objects` and `arrays` are labeled with size @default true */
383390
displayObjectSize?: boolean;
384-
/** Define the root node name. @default `undefined` */
391+
/** Define the root node name. @default undefined */
385392
keyName?: string | number;
393+
/** The user can copy objects and arrays to clipboard by clicking on the clipboard icon. @default true */
394+
enableClipboard?: boolean;
395+
/** Redefine interface elements to re-render. */
386396
components?: {
387397
braces?: MetaProps['render'];
388398
ellipsis?: EllipsisProps['render'];

core/src/copied.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useState } from "react";
2+
3+
export interface CopiedProps<T = object> extends React.SVGProps<SVGSVGElement> {
4+
show?: boolean;
5+
text?: T;
6+
}
7+
8+
export function Copied<T>(props: CopiedProps<T>) {
9+
const { children, style, text = '', show, ...reset } = props;
10+
if (!show) return null;
11+
const [copied, setCopied] = useState(false);
12+
const click = (event: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
13+
event.stopPropagation();
14+
let copyText = JSON.stringify(text, (key, value) => {
15+
if (typeof value === 'bigint') {
16+
return value.toString()
17+
}
18+
return value
19+
}, 2);
20+
21+
if (text === Infinity) copyText = Infinity;
22+
23+
navigator.clipboard.writeText(copyText)
24+
.then(() => {
25+
setCopied(true);
26+
const timer = setTimeout(() => {
27+
setCopied(false);
28+
clearTimeout(timer);
29+
}, 3000);
30+
})
31+
.catch((error) => {})
32+
};
33+
const defalutStyle = { ...style, cursor: 'pointer', marginLeft: 5 } as React.CSSProperties;
34+
const svgProps: React.SVGProps<SVGSVGElement> = {
35+
height: "1em",
36+
width: "1em",
37+
fill: "var(--w-rjv-copied-color, currentColor)",
38+
onClick: click,
39+
style: defalutStyle,
40+
className: 'w-rjv-copied',
41+
...reset,
42+
}
43+
if (copied) {
44+
return (
45+
<svg viewBox="0 0 38 38" {...svgProps} fill="var(--w-rjv-copied-success-color, #28a745)">
46+
<path d="M27.5,35 L2.5,35 L2.5,12.5 L27.5,12.5 L27.5,15.2249049 C29.1403264,13.8627542 29.9736597,13.1778155 30,13.1700887 C30,11.9705278 30,10.0804982 30,7.5 C30,6.1 28.9,5 27.5,5 L20,5 C20,2.2 17.8,0 15,0 C12.2,0 10,2.2 10,5 L2.5,5 C1.1,5 0,6.1 0,7.5 L0,35 C0,36.4 1.1,37.5 2.5,37.5 L27.5,37.5 C28.9,37.5 30,36.4 30,35 L30,30 L27.5,30 L27.5,35 Z M7.5,7.5 L10,7.5 C10,7.5 12.5,6.4 12.5,5 C12.5,3.6 13.6,2.5 15,2.5 C16.4,2.5 17.5,3.6 17.5,5 C17.5,6.4 18.8,7.5 20,7.5 L22.5,7.5 C22.5,7.5 25,8.6 25,10 L5,10 C5,8.5 6.1,7.5 7.5,7.5 Z M5,27.5 L10,27.5 L10,25 L5,25 L5,27.5 Z M31.5589286,15 L35.1589286,18.6 L21.0160714,33 L12.5303571,24.2571429 L16.1303571,20.6571429 L21.0160714,25.5428571 L31.5589286,15 Z M12.5,30 L12.5,32.5 L5,32.5 L5,30 L12.5,30 Z M17.5,15 L5,15 L5,17.5 L17.5,17.5 L17.5,15 Z M10,20 L5,20 L5,22.5 L10,22.5 L10,20 Z"></path>
47+
</svg>
48+
);
49+
}
50+
return (
51+
<svg viewBox="0 0 38 38" {...svgProps}>
52+
<path d="M27.5,35 L2.5,35 L2.5,12.5 L27.5,12.5 L27.5,20 L30,20 L30,7.5 C30,6.1 28.9,5 27.5,5 L20,5 C20,2.2 17.8,0 15,0 C12.2,0 10,2.2 10,5 L2.5,5 C1.1,5 0,6.1 0,7.5 L0,35 C0,36.4 1.1,37.5 2.5,37.5 L27.5,37.5 C28.9,37.5 30,36.4 30,35 L30,30 L27.5,30 L27.5,35 Z M7.5,7.5 L10,7.5 C10,7.5 12.5,6.4 12.5,5 C12.5,3.6 13.6,2.5 15,2.5 C16.4,2.5 17.5,3.6 17.5,5 C17.5,6.4 18.8,7.5 20,7.5 L22.5,7.5 C22.5,7.5 25,8.6 25,10 L5,10 C5,8.5 6.1,7.5 7.5,7.5 Z M5,27.5 L10,27.5 L10,25 L5,25 L5,27.5 Z M22.5,22.5 L22.5,17.5 L12.5,25 L22.5,32.5 L22.5,27.5 L35,27.5 L35,22.5 L22.5,22.5 Z M5,32.5 L12.5,32.5 L12.5,30 L5,30 L5,32.5 Z M17.5,15 L5,15 L5,17.5 L17.5,17.5 L17.5,15 Z M10,20 L5,20 L5,22.5 L10,22.5 L10,20 Z"></path>
53+
</svg>
54+
);
55+
}

core/src/index.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ export interface JsonViewProps<T>
99
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
1010
/** This property contains your input JSON */
1111
value?: T;
12-
/** Set the indent-width for nested objects @default `15`*/
12+
/** Set the indent-width for nested objects @default 15 */
1313
indentWidth?: number;
14-
/** When set to `true`, data type labels prefix values @default `true` */
14+
/** When set to `true`, data type labels prefix values @default true */
1515
displayDataTypes?: boolean;
16-
/** When set to `true`, `objects` and `arrays` are labeled with size @default `true` */
16+
/** When set to `true`, `objects` and `arrays` are labeled with size @default true */
1717
displayObjectSize?: boolean;
18-
/** Define the root node name. @default `undefined` */
18+
/** Define the root node name. @default undefined */
1919
keyName?: string | number;
20+
/** The user can copy objects and arrays to clipboard by clicking on the clipboard icon. @default true */
21+
enableClipboard?: boolean;
22+
/** Redefine interface elements to re-render. */
2023
components?: {
2124
braces?: MetaProps['render'];
2225
ellipsis?: EllipsisProps['render'];

core/src/node.tsx

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { FC, Fragment, PropsWithChildren, useId, cloneElement } from 'react';
2-
import { ValueView, ValueViewProps, Colon, Label, LabelProps, typeMap } from './value';
1+
import { FC, Fragment, PropsWithChildren, useId, cloneElement, useState } from 'react';
2+
import { ValueView, ValueViewProps, Colon, Label, LabelProps, Line, typeMap } from './value';
33
import { TriangleArrow } from './arrow/TriangleArrow';
44
import { useExpandsStatus, store } from './store';
55
import { JsonViewProps } from './';
6+
import { Copied } from './copied';
67

78
export interface MetaProps extends LabelProps {
89
isArray?: boolean;
@@ -30,8 +31,6 @@ export function Meta(props: MetaProps) {
3031
);
3132
}
3233

33-
export const Line: FC<PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>> = (props) => <div {...props} />;
34-
3534
export interface EllipsisProps extends React.HTMLAttributes<HTMLSpanElement> {
3635
render?: (props: EllipsisProps) => JSX.Element;
3736
}
@@ -88,6 +87,7 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
8887
displayDataTypes = true,
8988
components = {},
9089
displayObjectSize = true,
90+
enableClipboard = true,
9191
indentWidth = 15,
9292
keyid = 'root',
9393
...reset
@@ -110,6 +110,9 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
110110
};
111111
const valueViewProps = {
112112
displayDataTypes,
113+
displayObjectSize,
114+
enableClipboard,
115+
indentWidth,
113116
renderValue: components.value,
114117
} as ValueViewProps<T>;
115118

@@ -118,8 +121,12 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
118121
) : (
119122
<TriangleArrow style={arrowStyle} />
120123
);
124+
const [showTools, setShowTools] = useState(false);
125+
const tools = enableClipboard ? <Copied show={showTools} text={value} /> : undefined;
126+
const mouseEnter = () => setShowTools(true);
127+
const mouseLeave = () => setShowTools(false);
121128
return (
122-
<div {...reset}>
129+
<div {...reset} onMouseEnter={mouseEnter} onMouseLeave={mouseLeave}>
123130
<Line style={{ display: 'inline-flex', alignItems: 'center' }} onClick={handle}>
124131
{arrowView}
125132
{(typeof keyName === 'string' || typeof keyName === 'number') && (
@@ -139,20 +146,13 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
139146
{!expand && <Ellipsis render={components.ellipsis} />}
140147
{!expand && <Meta isArray={isArray} render={components.braces} />}
141148
{displayObjectSize && <CountInfo>{nameKeys.length} items</CountInfo>}
149+
{tools}
142150
</Line>
143151
{expand && (
144-
<Line style={{ borderLeft: 'var(--w-rjv-border-left, 1px solid #ebebeb)', marginLeft: 6 }}>
152+
<Line style={{ borderLeft: '1px solid var(--w-rjv-line-color, #ebebeb)', marginLeft: 6 }}>
145153
{nameKeys.length > 0 &&
146154
nameKeys.map((key, idx) => {
147155
const item = value[key];
148-
if (Array.isArray(item)) {
149-
const label = isArray ? idx : key;
150-
return (
151-
<Line key={label + idx}>
152-
<RooNode value={item} keyid={keyid + subkeyid + label} keyName={label} {...subNodeProps} />
153-
</Line>
154-
);
155-
}
156156
const renderKey = (
157157
<Semicolon
158158
value={item}
@@ -163,17 +163,16 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
163163
{key}
164164
</Semicolon>
165165
);
166-
if (typeof item === 'object' && item && !((item as any) instanceof Date)) {
167-
if (Object.keys(item).length === 0) {
168-
return (
169-
<Line key={key + idx} style={{ paddingLeft: indentWidth }}>
170-
<ValueView {...valueViewProps} renderKey={renderKey} keyName={key} value={item} />
171-
<Meta render={components.braces} start isArray={isArray} />
172-
<Meta render={components.braces} isArray={isArray} />
173-
{displayObjectSize && <CountInfo>{Object.keys(item).length} items</CountInfo>}
174-
</Line>
175-
);
176-
}
166+
const isEmpty = (Array.isArray(item) && (item as []).length === 0) || (typeof item === 'object' && item && !((item as any) instanceof Date) && Object.keys(item).length === 0);
167+
if (Array.isArray(item) && !isEmpty) {
168+
const label = isArray ? idx : key;
169+
return (
170+
<Line key={label + idx}>
171+
<RooNode value={item} keyid={keyid + subkeyid + label} keyName={label} {...subNodeProps} />
172+
</Line>
173+
);
174+
}
175+
if (typeof item === 'object' && item && !((item as any) instanceof Date) && !isEmpty) {
177176
return (
178177
<Line key={key + idx}>
179178
<RooNode keyid={keyid + subkeyid + key} value={item} keyName={key} {...subNodeProps} />
@@ -184,9 +183,7 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
184183
return;
185184
}
186185
return (
187-
<Line key={idx} style={{ paddingLeft: indentWidth }}>
188-
<ValueView {...valueViewProps} renderKey={renderKey} keyName={key} value={item} />
189-
</Line>
186+
<ValueView key={idx} {...valueViewProps} renderBraces={components.braces} renderKey={renderKey} keyName={key} value={item} />
190187
);
191188
})}
192189
</Line>

core/src/theme/dark.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ export const darkTheme = {
22
'--w-rjv-font-family': 'monospace',
33
'--w-rjv-color': '#0184a6',
44
'--w-rjv-background-color': '#202020',
5-
'--w-rjv-border-left': '1px solid #323232',
5+
'--w-rjv-line-color': '#323232',
66
'--w-rjv-arrow-color': 'var(--w-rjv-color)',
77
'--w-rjv-info-color': '#656565',
8+
'--w-rjv-copied-color': '#0184a6',
9+
'--w-rjv-copied-success-color': '#28a745',
810

911
'--w-rjv-curlybraces-color': '#1896b6',
1012
'--w-rjv-brackets-color': '#1896b6',

core/src/theme/light.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ export const lightTheme = {
22
'--w-rjv-font-family': 'monospace',
33
'--w-rjv-color': '#002b36',
44
'--w-rjv-background-color': '#ffffff',
5-
'--w-rjv-border-left': '1px solid #ebebeb',
5+
'--w-rjv-line-color': '#ebebeb',
66
'--w-rjv-arrow-color': 'var(--w-rjv-color)',
77
'--w-rjv-info-color': '#0000004d',
8+
'--w-rjv-copied-color': '#002b36',
9+
'--w-rjv-copied-success-color': '#28a745',
810

911
'--w-rjv-curlybraces-color': '#236a7c',
1012
'--w-rjv-brackets-color': '#236a7c',

core/src/value.tsx

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import { FC, Fragment, PropsWithChildren } from 'react';
1+
import { FC, Fragment, PropsWithChildren, useState } from 'react';
2+
import { Meta, MetaProps, CountInfo } from './node';
3+
import { Copied } from './copied';
24

5+
export const Line: FC<PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>> = (props) => <div {...props} />;
36
const isFloat = (n: number) => (Number(n) === n && n % 1 !== 0) || isNaN(n);
47
export const typeMap = {
58
string: {
@@ -53,14 +56,18 @@ export const Colon: FC<PropsWithChildren<React.HTMLAttributes<HTMLSpanElement>>>
5356
export interface ValueViewProps<T>
5457
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> {
5558
keyName?: string;
56-
value: T;
59+
value?: T;
5760
displayDataTypes: boolean;
61+
displayObjectSize: boolean;
62+
enableClipboard: boolean;
63+
indentWidth: number;
5864
renderKey?: JSX.Element;
65+
renderBraces?: MetaProps['render'];
5966
renderValue?: (props: React.HTMLAttributes<HTMLSpanElement> & { type: TypeProps['type'] }) => JSX.Element;
6067
}
6168

62-
export function ValueView<T>(props: ValueViewProps<T>) {
63-
const { value, keyName, renderKey, renderValue, displayDataTypes, ...reset } = props;
69+
export function ValueView<T = object>(props: ValueViewProps<T>) {
70+
const { value, keyName, indentWidth, renderKey, renderValue, renderBraces, enableClipboard, displayObjectSize, displayDataTypes, ...reset } = props;
6471

6572
let type = typeof value as TypeProps['type'];
6673
let content = '';
@@ -102,6 +109,12 @@ export function ValueView<T>(props: ValueViewProps<T>) {
102109
typeView = <Fragment />;
103110
}
104111
color = typeMap[type]?.color || '';
112+
113+
const [showTools, setShowTools] = useState(false);
114+
const tools = enableClipboard ? <Copied show={showTools} text={value} /> : undefined;
115+
const mouseEnter = () => setShowTools(true);
116+
const mouseLeave = () => setShowTools(false);
117+
105118
if (content && typeof content === 'string') {
106119
const valueView = renderValue ? (
107120
renderValue({
@@ -116,20 +129,35 @@ export function ValueView<T>(props: ValueViewProps<T>) {
116129
</Label>
117130
);
118131
return (
132+
<Line style={{ paddingLeft: indentWidth }} onMouseEnter={mouseEnter} onMouseLeave={mouseLeave}>
133+
<Label {...reset}>
134+
{renderKey}
135+
<Colon />
136+
{typeView}
137+
{valueView}
138+
{tools}
139+
</Label>
140+
</Line>
141+
);
142+
}
143+
const length = Array.isArray(value) ? value.length : Object.keys(value as object).length;
144+
const empty = (
145+
<Fragment>
146+
<Meta render={renderBraces} start isArray={Array.isArray(value)} />
147+
<Meta render={renderBraces} isArray={Array.isArray(value)} />
148+
{displayObjectSize && <CountInfo>{length} items</CountInfo>}
149+
</Fragment>
150+
)
151+
return (
152+
<Line style={{ paddingLeft: indentWidth }} onMouseEnter={mouseEnter} onMouseLeave={mouseLeave}>
119153
<Label {...reset}>
120154
{renderKey}
121155
<Colon />
122156
{typeView}
123-
{valueView}
157+
{empty}
158+
{tools}
124159
</Label>
125-
);
126-
}
127-
return (
128-
<Label {...reset}>
129-
{renderKey}
130-
<Colon />
131-
{typeView}
132-
</Label>
160+
</Line>
133161
);
134162
}
135163

0 commit comments

Comments
 (0)