Skip to content

Commit 5e4c7f3

Browse files
committed
feat: expose copy function to onCopy callback
expose internal `safeStringify` util
1 parent 21805a3 commit 5e4c7f3

File tree

6 files changed

+51
-38
lines changed

6 files changed

+51
-38
lines changed

docs/pages/index.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Check the [source code](https://github.com./TexteaInc/json-viewer/blob/main/docs/
9292
| `keyRenderer` | `{when: (props) => boolean}` | - | Customize a key, if `keyRenderer.when` returns `true`. |
9393
| `valueTypes` | `ValueTypes` | - | Customize value types. |
9494
| `onChange` | `(path, oldVal, newVal) => void` | - | Callback when value changed. |
95-
| `onCopy` | `(path, value) => void` | - | Callback when value copied, you can use it to customize the copy behavior.<br />\*Note: you will have to write the data to the clipboard by yourself. |
95+
| `onCopy` | `(path, value, copy) => void` | - | Callback when value copied, you can use it to customize the copy behavior.<br />\*Note: you can use the third argument `copy` to copy string to clipborad. |
9696
| `onSelect` | `(path, value) => void` | - | Callback when value selected. |
9797
| `enableClipboard` | `boolean` | `true` | Whether enable clipboard feature. |
9898
| `editable` | `boolean` \|<br />`(path, currentValue) => boolean` | `false` | Whether enable edit feature. You can pass a function to customize the result. |

src/components/DataKeyPair.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useInspect } from '../hooks/useInspect'
88
import { useJsonViewerStore } from '../stores/JsonViewerStore'
99
import { useTypeComponents } from '../stores/typeRegistry'
1010
import type { DataItemProps } from '../type'
11-
import { getValueSize } from '../utils'
11+
import { copyString, getValueSize } from '../utils'
1212
import {
1313
CheckIcon,
1414
ChevronRightIcon,
@@ -167,7 +167,7 @@ export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
167167
onClick={event => {
168168
event.preventDefault()
169169
try {
170-
copy(path, value)
170+
copy(path, value, copyString)
171171
} catch (e) {
172172
// in some case, this will throw error
173173
// fixme: `useAlert` hook

src/hooks/useCopyToClipboard.ts

+18-30
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import copyToClipboard from 'copy-to-clipboard'
21
import { useCallback, useRef, useState } from 'react'
32

43
import { useJsonViewerStore } from '../stores/JsonViewerStore'
54
import type { JsonViewerOnCopy } from '../type'
6-
import { safeStringify } from '../utils'
5+
import { copyString, safeStringify } from '../utils'
76

87
/**
98
* 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 } = {}) {
2524
}, [timeout])
2625
const onCopy = useJsonViewerStore(store => store.onCopy)
2726

28-
const copy = useCallback<JsonViewerOnCopy>((path, value: unknown) => {
27+
const copy = useCallback<JsonViewerOnCopy>(async (path, value: unknown) => {
2928
if (typeof onCopy === 'function') {
3029
try {
31-
const result = onCopy(path, value)
32-
if (result instanceof Promise) {
33-
result.then(() => {
34-
handleCopyResult(true)
35-
}).catch((error) => {
36-
console.error(
37-
`error when copy ${path.length === 0
38-
? 'src'
39-
: `src[${path.join(
40-
'.')}`
41-
}]`, error)
42-
})
43-
} else {
44-
handleCopyResult(true)
45-
}
30+
await onCopy(path, value, copyString)
31+
handleCopyResult(true)
4632
} catch (error) {
4733
console.error(
4834
`error when copy ${path.length === 0
@@ -52,18 +38,20 @@ export function useClipboard ({ timeout = 2000 } = {}) {
5238
}]`, error)
5339
}
5440
} else {
55-
const valueToCopy = safeStringify(
56-
typeof value === 'function' ? value.toString() : value,
57-
' '
58-
)
59-
if ('clipboard' in navigator) {
60-
navigator.clipboard.writeText(valueToCopy)
61-
.then(() => handleCopyResult(true))
62-
// When navigator.clipboard throws an error, fallback to copy-to-clipboard package
63-
.catch(() => copyToClipboard(valueToCopy))
64-
} else {
65-
// fallback to copy-to-clipboard when navigator.clipboard is not available
66-
copyToClipboard(valueToCopy)
41+
try {
42+
const valueToCopy = safeStringify(
43+
typeof value === 'function' ? value.toString() : value,
44+
' '
45+
)
46+
await copyString(valueToCopy)
47+
handleCopyResult(true)
48+
} catch (error) {
49+
console.error(
50+
`error when copy ${path.length === 0
51+
? 'src'
52+
: `src[${path.join(
53+
'.')}`
54+
}]`, error)
6755
}
6856
}
6957
}, [handleCopyResult, onCopy])

src/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import {
2121
} from './stores/typeRegistry'
2222
import { darkColorspace, lightColorspace } from './theme/base16'
2323
import type { JsonViewerProps } from './type'
24-
import { applyValue, createDataType, isCycleReference } from './utils'
24+
import { applyValue, createDataType, isCycleReference, safeStringify } from './utils'
2525

26-
export { applyValue, createDataType, isCycleReference }
26+
export { applyValue, createDataType, isCycleReference, safeStringify }
2727

2828
/**
2929
* @internal

src/type.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ export type JsonViewerOnChange = <U = unknown>(
1515
newValue: U /*, type: ChangeType */) => void
1616

1717
/**
18-
* @param path path to the target value
19-
* @param value
18+
* @param path path to the target value
19+
* @param value
20+
* @param copy the function to copy the value to clipboard
2021
*/
2122
export type JsonViewerOnCopy = <U = unknown>(
2223
path: Path,
23-
value: U
24+
value: U,
25+
copy: (value: string) => Promise<void>
2426
) => unknown | Promise<unknown>
2527

2628
/**

src/utils/index.ts

+23
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copyToClipboard from 'copy-to-clipboard'
12
import type { ComponentType } from 'react'
23

34
import type { DataItemProps, EditorProps, Path } from '../type'
@@ -187,6 +188,15 @@ export function segmentArray<T> (arr: T[], size: number): T[][] {
187188
return result
188189
}
189190

191+
/**
192+
* A safe version of `JSON.stringify` that handles circular references and BigInts.
193+
*
194+
* *This function might be changed in the future to support more types. Use it with caution.*
195+
*
196+
* @param obj A JavaScript value, usually an object or array, to be converted.
197+
* @param space Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.
198+
* @returns
199+
*/
190200
export function safeStringify (obj: any, space?: string | number) {
191201
const seenValues: any[] = []
192202

@@ -235,3 +245,16 @@ export function safeStringify (obj: any, space?: string | number) {
235245

236246
return JSON.stringify(obj, replacer, space)
237247
}
248+
249+
export async function copyString (value: string) {
250+
if ('clipboard' in navigator) {
251+
try {
252+
await navigator.clipboard.writeText(value)
253+
} catch {
254+
// When navigator.clipboard throws an error, fallback to copy-to-clipboard package
255+
}
256+
}
257+
258+
// fallback to copy-to-clipboard when navigator.clipboard is not available
259+
copyToClipboard(value)
260+
}

0 commit comments

Comments
 (0)