diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index bee0bc98..c324881d 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -92,7 +92,7 @@ Check the [source code](https://github.com/TexteaInc/json-viewer/blob/main/docs/ | `keyRenderer` | `{when: (props) => boolean}` | - | Customize a key, if `keyRenderer.when` returns `true`. | | `valueTypes` | `ValueTypes` | - | Customize value types. | | `onChange` | `(path, oldVal, newVal) => void` | - | Callback when value changed. | -| `onCopy` | `(path, value) => void` | - | Callback when value copied, you can use it to customize the copy behavior.
\*Note: you will have to write the data to the clipboard by yourself. | +| `onCopy` | `(path, value, copy) => void` | - | Callback when value copied, you can use it to customize the copy behavior.
\*Note: you can use the third argument `copy` to copy string to clipborad. | | `onSelect` | `(path, value) => void` | - | Callback when value selected. | | `enableClipboard` | `boolean` | `true` | Whether enable clipboard feature. | | `editable` | `boolean` \|
`(path, currentValue) => boolean` | `false` | Whether enable edit feature. You can pass a function to customize the result. | diff --git a/src/components/DataKeyPair.tsx b/src/components/DataKeyPair.tsx index 56846256..9a5d96cb 100644 --- a/src/components/DataKeyPair.tsx +++ b/src/components/DataKeyPair.tsx @@ -8,7 +8,7 @@ import { useInspect } from '../hooks/useInspect' import { useJsonViewerStore } from '../stores/JsonViewerStore' import { useTypeComponents } from '../stores/typeRegistry' import type { DataItemProps } from '../type' -import { getValueSize } from '../utils' +import { copyString, getValueSize } from '../utils' import { CheckIcon, ChevronRightIcon, @@ -167,7 +167,7 @@ export const DataKeyPair: FC = (props) => { onClick={event => { event.preventDefault() try { - copy(path, value) + copy(path, value, copyString) } catch (e) { // in some case, this will throw error // fixme: `useAlert` hook diff --git a/src/hooks/useCopyToClipboard.ts b/src/hooks/useCopyToClipboard.ts index 31e4520b..bec6592d 100644 --- a/src/hooks/useCopyToClipboard.ts +++ b/src/hooks/useCopyToClipboard.ts @@ -1,9 +1,8 @@ -import copyToClipboard from 'copy-to-clipboard' import { useCallback, useRef, useState } from 'react' import { useJsonViewerStore } from '../stores/JsonViewerStore' import type { JsonViewerOnCopy } from '../type' -import { safeStringify } from '../utils' +import { copyString, safeStringify } from '../utils' /** * useClipboard hook accepts one argument options in which copied status timeout duration is defined (defaults to 2000). Hook returns object with properties: @@ -25,24 +24,11 @@ export function useClipboard ({ timeout = 2000 } = {}) { }, [timeout]) const onCopy = useJsonViewerStore(store => store.onCopy) - const copy = useCallback((path, value: unknown) => { + const copy = useCallback(async (path, value: unknown) => { if (typeof onCopy === 'function') { try { - const result = onCopy(path, value) - if (result instanceof Promise) { - result.then(() => { - handleCopyResult(true) - }).catch((error) => { - console.error( - `error when copy ${path.length === 0 - ? 'src' - : `src[${path.join( - '.')}` - }]`, error) - }) - } else { - handleCopyResult(true) - } + await onCopy(path, value, copyString) + handleCopyResult(true) } catch (error) { console.error( `error when copy ${path.length === 0 @@ -52,18 +38,20 @@ export function useClipboard ({ timeout = 2000 } = {}) { }]`, error) } } else { - const valueToCopy = safeStringify( - typeof value === 'function' ? value.toString() : value, - ' ' - ) - if ('clipboard' in navigator) { - navigator.clipboard.writeText(valueToCopy) - .then(() => handleCopyResult(true)) - // When navigator.clipboard throws an error, fallback to copy-to-clipboard package - .catch(() => copyToClipboard(valueToCopy)) - } else { - // fallback to copy-to-clipboard when navigator.clipboard is not available - copyToClipboard(valueToCopy) + try { + const valueToCopy = safeStringify( + typeof value === 'function' ? value.toString() : value, + ' ' + ) + await copyString(valueToCopy) + handleCopyResult(true) + } catch (error) { + console.error( + `error when copy ${path.length === 0 + ? 'src' + : `src[${path.join( + '.')}` + }]`, error) } } }, [handleCopyResult, onCopy]) diff --git a/src/index.tsx b/src/index.tsx index 10760d89..a5b2c958 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -21,9 +21,9 @@ import { } from './stores/typeRegistry' import { darkColorspace, lightColorspace } from './theme/base16' import type { JsonViewerProps } from './type' -import { applyValue, createDataType, isCycleReference } from './utils' +import { applyValue, createDataType, isCycleReference, safeStringify } from './utils' -export { applyValue, createDataType, isCycleReference } +export { applyValue, createDataType, isCycleReference, safeStringify } /** * @internal diff --git a/src/type.ts b/src/type.ts index 55e0968c..7137f1c6 100644 --- a/src/type.ts +++ b/src/type.ts @@ -15,12 +15,14 @@ export type JsonViewerOnChange = ( newValue: U /*, type: ChangeType */) => void /** - * @param path path to the target value - * @param value + * @param path path to the target value + * @param value + * @param copy the function to copy the value to clipboard */ export type JsonViewerOnCopy = ( path: Path, - value: U + value: U, + copy: (value: string) => Promise ) => unknown | Promise /** diff --git a/src/utils/index.ts b/src/utils/index.ts index 9080f5a2..c709f216 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,4 @@ +import copyToClipboard from 'copy-to-clipboard' import type { ComponentType } from 'react' import type { DataItemProps, EditorProps, Path } from '../type' @@ -187,6 +188,15 @@ export function segmentArray (arr: T[], size: number): T[][] { return result } +/** + * A safe version of `JSON.stringify` that handles circular references and BigInts. + * + * *This function might be changed in the future to support more types. Use it with caution.* + * + * @param obj A JavaScript value, usually an object or array, to be converted. + * @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read. + * @returns + */ export function safeStringify (obj: any, space?: string | number) { const seenValues: any[] = [] @@ -235,3 +245,16 @@ export function safeStringify (obj: any, space?: string | number) { return JSON.stringify(obj, replacer, space) } + +export async function copyString (value: string) { + if ('clipboard' in navigator) { + try { + await navigator.clipboard.writeText(value) + } catch { + // When navigator.clipboard throws an error, fallback to copy-to-clipboard package + } + } + + // fallback to copy-to-clipboard when navigator.clipboard is not available + copyToClipboard(value) +}