From 60a952549022196610696bc37527df20b8831aa0 Mon Sep 17 00:00:00 2001 From: jaywcjlove <398188662@qq.com> Date: Mon, 19 Jun 2023 21:14:26 +0800 Subject: [PATCH] feat: add `enableClipboard` props. --- core/README.md | 30 ++++++++++++++-------- core/src/copied.tsx | 55 +++++++++++++++++++++++++++++++++++++++++ core/src/index.tsx | 11 ++++++--- core/src/node.tsx | 53 +++++++++++++++++++-------------------- core/src/theme/dark.ts | 4 ++- core/src/theme/light.ts | 4 ++- core/src/value.tsx | 54 ++++++++++++++++++++++++++++++---------- www/src/Example.tsx | 7 ++++++ 8 files changed, 161 insertions(+), 57 deletions(-) create mode 100644 core/src/copied.tsx diff --git a/core/README.md b/core/README.md index e8bb30c5..291fbede 100644 --- a/core/README.md +++ b/core/README.md @@ -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. @@ -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', @@ -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', @@ -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 ( @@ -200,7 +204,10 @@ export default function Demo() {
{Object.keys(customTheme).map((varname, idx) => { - const click = () => setCssvar(varname); + const click = () => { + setCssvar(varname); + setHex(customTheme[varname]); + }; const active = cssvar === varname ? '#a8a8a8' : ''; return })} @@ -375,14 +382,17 @@ import { MetaProps, SemicolonProps, EllipsisProps, ValueViewProps } from '@uiw/r export interface JsonViewProps extends React.DetailedHTMLProps, 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']; diff --git a/core/src/copied.tsx b/core/src/copied.tsx new file mode 100644 index 00000000..05a9947e --- /dev/null +++ b/core/src/copied.tsx @@ -0,0 +1,55 @@ +import { useState } from "react"; + +export interface CopiedProps extends React.SVGProps { + show?: boolean; + text?: T; +} + +export function Copied(props: CopiedProps) { + const { children, style, text = '', show, ...reset } = props; + if (!show) return null; + const [copied, setCopied] = useState(false); + const click = (event: React.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 = { + height: "1em", + width: "1em", + fill: "var(--w-rjv-copied-color, currentColor)", + onClick: click, + style: defalutStyle, + className: 'w-rjv-copied', + ...reset, + } + if (copied) { + return ( + + + + ); + } + return ( + + + + ); +} \ No newline at end of file diff --git a/core/src/index.tsx b/core/src/index.tsx index ece9401b..caf11509 100644 --- a/core/src/index.tsx +++ b/core/src/index.tsx @@ -9,14 +9,17 @@ export interface JsonViewProps extends React.DetailedHTMLProps, 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']; diff --git a/core/src/node.tsx b/core/src/node.tsx index 6a00e08f..b5f0d9bf 100644 --- a/core/src/node.tsx +++ b/core/src/node.tsx @@ -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; @@ -30,8 +31,6 @@ export function Meta(props: MetaProps) { ); } -export const Line: FC>> = (props) =>
; - export interface EllipsisProps extends React.HTMLAttributes { render?: (props: EllipsisProps) => JSX.Element; } @@ -88,6 +87,7 @@ export function RooNode(props: RooNodeProps) { displayDataTypes = true, components = {}, displayObjectSize = true, + enableClipboard = true, indentWidth = 15, keyid = 'root', ...reset @@ -110,6 +110,9 @@ export function RooNode(props: RooNodeProps) { }; const valueViewProps = { displayDataTypes, + displayObjectSize, + enableClipboard, + indentWidth, renderValue: components.value, } as ValueViewProps; @@ -118,8 +121,12 @@ export function RooNode(props: RooNodeProps) { ) : ( ); + const [showTools, setShowTools] = useState(false); + const tools = enableClipboard ? : undefined; + const mouseEnter = () => setShowTools(true); + const mouseLeave = () => setShowTools(false); return ( -
+
{arrowView} {(typeof keyName === 'string' || typeof keyName === 'number') && ( @@ -139,20 +146,13 @@ export function RooNode(props: RooNodeProps) { {!expand && } {!expand && } {displayObjectSize && {nameKeys.length} items} + {tools} {expand && ( - + {nameKeys.length > 0 && nameKeys.map((key, idx) => { const item = value[key]; - if (Array.isArray(item)) { - const label = isArray ? idx : key; - return ( - - - - ); - } const renderKey = ( (props: RooNodeProps) { {key} ); - if (typeof item === 'object' && item && !((item as any) instanceof Date)) { - if (Object.keys(item).length === 0) { - return ( - - - - - {displayObjectSize && {Object.keys(item).length} items} - - ); - } + 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 ( + + + + ); + } + if (typeof item === 'object' && item && !((item as any) instanceof Date) && !isEmpty) { return ( @@ -184,9 +183,7 @@ export function RooNode(props: RooNodeProps) { return; } return ( - - - + ); })} diff --git a/core/src/theme/dark.ts b/core/src/theme/dark.ts index 08a266be..c911443b 100644 --- a/core/src/theme/dark.ts +++ b/core/src/theme/dark.ts @@ -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', diff --git a/core/src/theme/light.ts b/core/src/theme/light.ts index a7ea76a1..38c241b8 100644 --- a/core/src/theme/light.ts +++ b/core/src/theme/light.ts @@ -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', diff --git a/core/src/value.tsx b/core/src/value.tsx index 8a7fbf1b..db21f93b 100644 --- a/core/src/value.tsx +++ b/core/src/value.tsx @@ -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>> = (props) =>
; const isFloat = (n: number) => (Number(n) === n && n % 1 !== 0) || isNaN(n); export const typeMap = { string: { @@ -53,14 +56,18 @@ export const Colon: FC>> export interface ValueViewProps extends React.DetailedHTMLProps, 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 & { type: TypeProps['type'] }) => JSX.Element; } -export function ValueView(props: ValueViewProps) { - const { value, keyName, renderKey, renderValue, displayDataTypes, ...reset } = props; +export function ValueView(props: ValueViewProps) { + const { value, keyName, indentWidth, renderKey, renderValue, renderBraces, enableClipboard, displayObjectSize, displayDataTypes, ...reset } = props; let type = typeof value as TypeProps['type']; let content = ''; @@ -102,6 +109,12 @@ export function ValueView(props: ValueViewProps) { typeView = ; } color = typeMap[type]?.color || ''; + + const [showTools, setShowTools] = useState(false); + const tools = enableClipboard ? : undefined; + const mouseEnter = () => setShowTools(true); + const mouseLeave = () => setShowTools(false); + if (content && typeof content === 'string') { const valueView = renderValue ? ( renderValue({ @@ -116,20 +129,35 @@ export function ValueView(props: ValueViewProps) { ); return ( + + + + ); + } + const length = Array.isArray(value) ? value.length : Object.keys(value as object).length; + const empty = ( + + + + {displayObjectSize && {length} items} + + ) + return ( + - ); - } - return ( - + ); } diff --git a/www/src/Example.tsx b/www/src/Example.tsx index 9b786319..faf58d97 100644 --- a/www/src/Example.tsx +++ b/www/src/Example.tsx @@ -27,6 +27,7 @@ const example = { timer: 0, date: new Date('Tue Sep 13 2022 14:07:44 GMT-0500 (Central Daylight Time)'), array: [19, 100.86, 'test', NaN, Infinity], + emptyArray: [], nestedArray: [ [1, 2], [3, 4], @@ -56,6 +57,7 @@ export function Example() { const [theme, setTheme] = useState(lightTheme as React.CSSProperties); const [displayDataTypes, setDisplayDataTypes] = useState(true); const [displayObjectSize, setDisplayObjectSize] = useState(true); + const [clipboard, setClipboard] = useState(true); return ( +