Skip to content

Commit

Permalink
feat: add enableClipboard props.
Browse files Browse the repository at this point in the history
  • Loading branch information
jaywcjlove committed Jun 19, 2023
1 parent 9abada3 commit 60a9525
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 57 deletions.
30 changes: 20 additions & 10 deletions core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ react-json-view

[![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)
[![npm version](https://img.shields.io/npm/v/@uiw/react-json-view.svg)](https://www.npmjs.com/package/@uiw/react-json-view)
[![react@^18](https://shields.io/badge/react-^18-green?style=flat&logo=react)](https://github.com/facebook/react/releases)

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

Expand Down Expand Up @@ -106,9 +107,11 @@ const customTheme = {
'--w-rjv-font-family': 'monospace',
'--w-rjv-color': '#9cdcfe',
'--w-rjv-background-color': '#1e1e1e',
'--w-rjv-border-left': '1px solid #323232',
'--w-rjv-line-color': '#323232',
'--w-rjv-arrow-color': 'var(--w-rjv-color)',
'--w-rjv-info-color': '#656565',
'--w-rjv-copied-color': '#9cdcfe',
'--w-rjv-copied-success-color': '#28a745',

'--w-rjv-curlybraces-color': '#d4d4d4',
'--w-rjv-brackets-color': '#d4d4d4',
Expand Down Expand Up @@ -165,9 +168,11 @@ const object = {
const customTheme = {
'--w-rjv-color': '#9cdcfe',
'--w-rjv-background-color': '#1e1e1e',
'--w-rjv-border-left': '1px solid #323232',
'--w-rjv-arrow-color': 'var(--w-rjv-color)',
'--w-rjv-line-color': '#323232',
'--w-rjv-arrow-color': '#9cdcfe',
'--w-rjv-info-color': '#656565',
'--w-rjv-copied-color': '#0184a6',
'--w-rjv-copied-success-color': '#28a745',

'--w-rjv-curlybraces-color': '#d4d4d4',
'--w-rjv-brackets-color': '#d4d4d4',
Expand All @@ -188,9 +193,8 @@ export default function Demo() {
const [hex, setHex] = useState("#1e1e1e");
const [theme, setTheme] = useState(customTheme);
const onChange = ({ hexa }) => {
const value = cssvar === '--w-rjv-border-left' ? `1px solid ${hexa}` : hexa;
setHex(hexa);
setTheme({ ...theme, [cssvar]: value });
setTheme({ ...theme, [cssvar]: hexa });
};
return (
<React.Fragment>
Expand All @@ -200,7 +204,10 @@ export default function Demo() {
<Colorful color={hex} onChange={onChange} />
<div style={{ display: 'flex', gap: '0.4rem', flexWrap: 'wrap' }}>
{Object.keys(customTheme).map((varname, idx) => {
const click = () => setCssvar(varname);
const click = () => {
setCssvar(varname);
setHex(customTheme[varname]);
};
const active = cssvar === varname ? '#a8a8a8' : '';
return <button key={idx} style={{ background: active }} onClick={click}>{varname}</button>
})}
Expand Down Expand Up @@ -375,14 +382,17 @@ import { MetaProps, SemicolonProps, EllipsisProps, ValueViewProps } from '@uiw/r
export interface JsonViewProps<T> extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
/** This property contains your input JSON */
value?: T;
/** Set the indent-width for nested objects @default `15`*/
/** Set the indent-width for nested objects @default 15 */
indentWidth?: number;
/** When set to `true`, data type labels prefix values @default `true` */
/** When set to `true`, data type labels prefix values @default true */
displayDataTypes?: boolean;
/** When set to `true`, `objects` and `arrays` are labeled with size @default `true` */
/** When set to `true`, `objects` and `arrays` are labeled with size @default true */
displayObjectSize?: boolean;
/** Define the root node name. @default `undefined` */
/** Define the root node name. @default undefined */
keyName?: string | number;
/** The user can copy objects and arrays to clipboard by clicking on the clipboard icon. @default true */
enableClipboard?: boolean;
/** Redefine interface elements to re-render. */
components?: {
braces?: MetaProps['render'];
ellipsis?: EllipsisProps['render'];
Expand Down
55 changes: 55 additions & 0 deletions core/src/copied.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useState } from "react";

export interface CopiedProps<T = object> extends React.SVGProps<SVGSVGElement> {
show?: boolean;
text?: T;
}

export function Copied<T>(props: CopiedProps<T>) {
const { children, style, text = '', show, ...reset } = props;
if (!show) return null;
const [copied, setCopied] = useState(false);
const click = (event: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
event.stopPropagation();
let copyText = JSON.stringify(text, (key, value) => {
if (typeof value === 'bigint') {
return value.toString()
}
return value
}, 2);

if (text === Infinity) copyText = Infinity;

navigator.clipboard.writeText(copyText)
.then(() => {
setCopied(true);
const timer = setTimeout(() => {
setCopied(false);
clearTimeout(timer);
}, 3000);
})
.catch((error) => {})
};
const defalutStyle = { ...style, cursor: 'pointer', marginLeft: 5 } as React.CSSProperties;
const svgProps: React.SVGProps<SVGSVGElement> = {
height: "1em",
width: "1em",
fill: "var(--w-rjv-copied-color, currentColor)",
onClick: click,
style: defalutStyle,
className: 'w-rjv-copied',
...reset,
}
if (copied) {
return (
<svg viewBox="0 0 38 38" {...svgProps} fill="var(--w-rjv-copied-success-color, #28a745)">
<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>
</svg>
);
}
return (
<svg viewBox="0 0 38 38" {...svgProps}>
<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>
</svg>
);
}
11 changes: 7 additions & 4 deletions core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ export interface JsonViewProps<T>
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
/** This property contains your input JSON */
value?: T;
/** Set the indent-width for nested objects @default `15`*/
/** Set the indent-width for nested objects @default 15 */
indentWidth?: number;
/** When set to `true`, data type labels prefix values @default `true` */
/** When set to `true`, data type labels prefix values @default true */
displayDataTypes?: boolean;
/** When set to `true`, `objects` and `arrays` are labeled with size @default `true` */
/** When set to `true`, `objects` and `arrays` are labeled with size @default true */
displayObjectSize?: boolean;
/** Define the root node name. @default `undefined` */
/** Define the root node name. @default undefined */
keyName?: string | number;
/** The user can copy objects and arrays to clipboard by clicking on the clipboard icon. @default true */
enableClipboard?: boolean;
/** Redefine interface elements to re-render. */
components?: {
braces?: MetaProps['render'];
ellipsis?: EllipsisProps['render'];
Expand Down
53 changes: 25 additions & 28 deletions core/src/node.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { FC, Fragment, PropsWithChildren, useId, cloneElement } from 'react';
import { ValueView, ValueViewProps, Colon, Label, LabelProps, typeMap } from './value';
import { FC, Fragment, PropsWithChildren, useId, cloneElement, useState } from 'react';
import { ValueView, ValueViewProps, Colon, Label, LabelProps, Line, typeMap } from './value';
import { TriangleArrow } from './arrow/TriangleArrow';
import { useExpandsStatus, store } from './store';
import { JsonViewProps } from './';
import { Copied } from './copied';

export interface MetaProps extends LabelProps {
isArray?: boolean;
Expand Down Expand Up @@ -30,8 +31,6 @@ export function Meta(props: MetaProps) {
);
}

export const Line: FC<PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>> = (props) => <div {...props} />;

export interface EllipsisProps extends React.HTMLAttributes<HTMLSpanElement> {
render?: (props: EllipsisProps) => JSX.Element;
}
Expand Down Expand Up @@ -88,6 +87,7 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
displayDataTypes = true,
components = {},
displayObjectSize = true,
enableClipboard = true,
indentWidth = 15,
keyid = 'root',
...reset
Expand All @@ -110,6 +110,9 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
};
const valueViewProps = {
displayDataTypes,
displayObjectSize,
enableClipboard,
indentWidth,
renderValue: components.value,
} as ValueViewProps<T>;

Expand All @@ -118,8 +121,12 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
) : (
<TriangleArrow style={arrowStyle} />
);
const [showTools, setShowTools] = useState(false);
const tools = enableClipboard ? <Copied show={showTools} text={value} /> : undefined;
const mouseEnter = () => setShowTools(true);
const mouseLeave = () => setShowTools(false);
return (
<div {...reset}>
<div {...reset} onMouseEnter={mouseEnter} onMouseLeave={mouseLeave}>
<Line style={{ display: 'inline-flex', alignItems: 'center' }} onClick={handle}>
{arrowView}
{(typeof keyName === 'string' || typeof keyName === 'number') && (
Expand All @@ -139,20 +146,13 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
{!expand && <Ellipsis render={components.ellipsis} />}
{!expand && <Meta isArray={isArray} render={components.braces} />}
{displayObjectSize && <CountInfo>{nameKeys.length} items</CountInfo>}
{tools}
</Line>
{expand && (
<Line style={{ borderLeft: 'var(--w-rjv-border-left, 1px solid #ebebeb)', marginLeft: 6 }}>
<Line style={{ borderLeft: '1px solid var(--w-rjv-line-color, #ebebeb)', marginLeft: 6 }}>
{nameKeys.length > 0 &&
nameKeys.map((key, idx) => {
const item = value[key];
if (Array.isArray(item)) {
const label = isArray ? idx : key;
return (
<Line key={label + idx}>
<RooNode value={item} keyid={keyid + subkeyid + label} keyName={label} {...subNodeProps} />
</Line>
);
}
const renderKey = (
<Semicolon
value={item}
Expand All @@ -163,17 +163,16 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
{key}
</Semicolon>
);
if (typeof item === 'object' && item && !((item as any) instanceof Date)) {
if (Object.keys(item).length === 0) {
return (
<Line key={key + idx} style={{ paddingLeft: indentWidth }}>
<ValueView {...valueViewProps} renderKey={renderKey} keyName={key} value={item} />
<Meta render={components.braces} start isArray={isArray} />
<Meta render={components.braces} isArray={isArray} />
{displayObjectSize && <CountInfo>{Object.keys(item).length} items</CountInfo>}
</Line>
);
}
const isEmpty = (Array.isArray(item) && (item as []).length === 0) || (typeof item === 'object' && item && !((item as any) instanceof Date) && Object.keys(item).length === 0);
if (Array.isArray(item) && !isEmpty) {
const label = isArray ? idx : key;
return (
<Line key={label + idx}>
<RooNode value={item} keyid={keyid + subkeyid + label} keyName={label} {...subNodeProps} />
</Line>
);
}
if (typeof item === 'object' && item && !((item as any) instanceof Date) && !isEmpty) {
return (
<Line key={key + idx}>
<RooNode keyid={keyid + subkeyid + key} value={item} keyName={key} {...subNodeProps} />
Expand All @@ -184,9 +183,7 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
return;
}
return (
<Line key={idx} style={{ paddingLeft: indentWidth }}>
<ValueView {...valueViewProps} renderKey={renderKey} keyName={key} value={item} />
</Line>
<ValueView key={idx} {...valueViewProps} renderBraces={components.braces} renderKey={renderKey} keyName={key} value={item} />
);
})}
</Line>
Expand Down
4 changes: 3 additions & 1 deletion core/src/theme/dark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ export const darkTheme = {
'--w-rjv-font-family': 'monospace',
'--w-rjv-color': '#0184a6',
'--w-rjv-background-color': '#202020',
'--w-rjv-border-left': '1px solid #323232',
'--w-rjv-line-color': '#323232',
'--w-rjv-arrow-color': 'var(--w-rjv-color)',
'--w-rjv-info-color': '#656565',
'--w-rjv-copied-color': '#0184a6',
'--w-rjv-copied-success-color': '#28a745',

'--w-rjv-curlybraces-color': '#1896b6',
'--w-rjv-brackets-color': '#1896b6',
Expand Down
4 changes: 3 additions & 1 deletion core/src/theme/light.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ export const lightTheme = {
'--w-rjv-font-family': 'monospace',
'--w-rjv-color': '#002b36',
'--w-rjv-background-color': '#ffffff',
'--w-rjv-border-left': '1px solid #ebebeb',
'--w-rjv-line-color': '#ebebeb',
'--w-rjv-arrow-color': 'var(--w-rjv-color)',
'--w-rjv-info-color': '#0000004d',
'--w-rjv-copied-color': '#002b36',
'--w-rjv-copied-success-color': '#28a745',

'--w-rjv-curlybraces-color': '#236a7c',
'--w-rjv-brackets-color': '#236a7c',
Expand Down
54 changes: 41 additions & 13 deletions core/src/value.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { FC, Fragment, PropsWithChildren } from 'react';
import { FC, Fragment, PropsWithChildren, useState } from 'react';
import { Meta, MetaProps, CountInfo } from './node';
import { Copied } from './copied';

export const Line: FC<PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>> = (props) => <div {...props} />;
const isFloat = (n: number) => (Number(n) === n && n % 1 !== 0) || isNaN(n);
export const typeMap = {
string: {
Expand Down Expand Up @@ -53,14 +56,18 @@ export const Colon: FC<PropsWithChildren<React.HTMLAttributes<HTMLSpanElement>>>
export interface ValueViewProps<T>
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> {
keyName?: string;
value: T;
value?: T;
displayDataTypes: boolean;
displayObjectSize: boolean;
enableClipboard: boolean;
indentWidth: number;
renderKey?: JSX.Element;
renderBraces?: MetaProps['render'];
renderValue?: (props: React.HTMLAttributes<HTMLSpanElement> & { type: TypeProps['type'] }) => JSX.Element;
}

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

let type = typeof value as TypeProps['type'];
let content = '';
Expand Down Expand Up @@ -102,6 +109,12 @@ export function ValueView<T>(props: ValueViewProps<T>) {
typeView = <Fragment />;
}
color = typeMap[type]?.color || '';

const [showTools, setShowTools] = useState(false);
const tools = enableClipboard ? <Copied show={showTools} text={value} /> : undefined;
const mouseEnter = () => setShowTools(true);
const mouseLeave = () => setShowTools(false);

if (content && typeof content === 'string') {
const valueView = renderValue ? (
renderValue({
Expand All @@ -116,20 +129,35 @@ export function ValueView<T>(props: ValueViewProps<T>) {
</Label>
);
return (
<Line style={{ paddingLeft: indentWidth }} onMouseEnter={mouseEnter} onMouseLeave={mouseLeave}>
<Label {...reset}>
{renderKey}
<Colon />
{typeView}
{valueView}
{tools}
</Label>
</Line>
);
}
const length = Array.isArray(value) ? value.length : Object.keys(value as object).length;
const empty = (
<Fragment>
<Meta render={renderBraces} start isArray={Array.isArray(value)} />
<Meta render={renderBraces} isArray={Array.isArray(value)} />
{displayObjectSize && <CountInfo>{length} items</CountInfo>}
</Fragment>
)
return (
<Line style={{ paddingLeft: indentWidth }} onMouseEnter={mouseEnter} onMouseLeave={mouseLeave}>
<Label {...reset}>
{renderKey}
<Colon />
{typeView}
{valueView}
{empty}
{tools}
</Label>
);
}
return (
<Label {...reset}>
{renderKey}
<Colon />
{typeView}
</Label>
</Line>
);
}

Expand Down
Loading

0 comments on commit 60a9525

Please sign in to comment.