1
1
import * as React from 'react'
2
2
3
3
import { displayValue , styled } from './utils'
4
+ import superjson from 'superjson'
4
5
5
6
export const Entry = styled ( 'div' , {
6
7
fontFamily : 'Menlo, monospace' ,
@@ -29,6 +30,55 @@ export const ExpandButton = styled('button', {
29
30
padding : 0 ,
30
31
} )
31
32
33
+ type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
34
+
35
+ export const CopyButton = ( { value } : { value : unknown } ) => {
36
+ const [ copyState , setCopyState ] = React . useState < CopyState > ( 'NoCopy' )
37
+
38
+ return (
39
+ < button
40
+ onClick = {
41
+ copyState === 'NoCopy'
42
+ ? ( ) => {
43
+ navigator . clipboard . writeText ( superjson . stringify ( value ) ) . then (
44
+ ( ) => {
45
+ setCopyState ( 'SuccessCopy' )
46
+ setTimeout ( ( ) => {
47
+ setCopyState ( 'NoCopy' )
48
+ } , 1500 )
49
+ } ,
50
+ ( err ) => {
51
+ console . error ( 'Failed to copy: ' , err )
52
+ setCopyState ( 'ErrorCopy' )
53
+ setTimeout ( ( ) => {
54
+ setCopyState ( 'NoCopy' )
55
+ } , 1500 )
56
+ } ,
57
+ )
58
+ }
59
+ : undefined
60
+ }
61
+ style = { {
62
+ cursor : 'pointer' ,
63
+ color : 'inherit' ,
64
+ font : 'inherit' ,
65
+ outline : 'inherit' ,
66
+ background : 'transparent' ,
67
+ border : 'none' ,
68
+ padding : 0 ,
69
+ } }
70
+ >
71
+ { copyState === 'NoCopy' ? (
72
+ < Copier />
73
+ ) : copyState === 'SuccessCopy' ? (
74
+ < CopiedCopier />
75
+ ) : (
76
+ < ErrorCopier />
77
+ ) }
78
+ </ button >
79
+ )
80
+ }
81
+
32
82
export const Value = styled ( 'span' , ( _props , theme ) => ( {
33
83
color : theme . danger ,
34
84
} ) )
@@ -62,6 +112,76 @@ export const Expander = ({ expanded, style = {} }: ExpanderProps) => (
62
112
</ span >
63
113
)
64
114
115
+ const Copier = ( ) => (
116
+ < span
117
+ aria-label = "Copy object to clipboard"
118
+ title = "Copy object to clipboard"
119
+ style = { {
120
+ paddingLeft : '1em' ,
121
+ } }
122
+ >
123
+ < svg height = "12" viewBox = "0 0 16 12" width = "10" >
124
+ < path
125
+ fill = "currentColor"
126
+ d = "M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"
127
+ > </ path >
128
+ < path
129
+ fill = "currentColor"
130
+ d = "M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"
131
+ > </ path >
132
+ </ svg >
133
+ </ span >
134
+ )
135
+
136
+ const ErrorCopier = ( ) => (
137
+ < span
138
+ aria-label = "Failed copying to clipboard"
139
+ title = "Failed copying to clipboard"
140
+ style = { {
141
+ paddingLeft : '1em' ,
142
+ display : 'flex' ,
143
+ alignItems : 'center' ,
144
+ } }
145
+ >
146
+ < svg height = "12" viewBox = "0 0 16 12" width = "10" display = "block" >
147
+ < path
148
+ fill = "red"
149
+ d = "M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"
150
+ > </ path >
151
+ </ svg >
152
+ < span
153
+ style = { {
154
+ color : 'red' ,
155
+ fontSize : '12px' ,
156
+ paddingLeft : '4px' ,
157
+ position : 'relative' ,
158
+ top : '2px' ,
159
+ } }
160
+ >
161
+ See console
162
+ </ span >
163
+ </ span >
164
+ )
165
+
166
+ const CopiedCopier = ( ) => (
167
+ < span
168
+ aria-label = "Object copied to clipboard"
169
+ title = "Object copied to clipboard"
170
+ style = { {
171
+ paddingLeft : '1em' ,
172
+ display : 'inline-block' ,
173
+ verticalAlign : 'middle' ,
174
+ } }
175
+ >
176
+ < svg height = "16" viewBox = "0 0 16 16" width = "16" display = "block" >
177
+ < path
178
+ fill = "green"
179
+ d = "M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"
180
+ > </ path >
181
+ </ svg >
182
+ </ span >
183
+ )
184
+
65
185
type Entry = {
66
186
label : string
67
187
}
@@ -74,6 +194,7 @@ type RendererProps = {
74
194
subEntryPages : Entry [ ] [ ]
75
195
type : string
76
196
expanded : boolean
197
+ copyable : boolean
77
198
toggleExpanded : ( ) => void
78
199
pageSize : number
79
200
}
@@ -108,6 +229,7 @@ export const DefaultRenderer: Renderer = ({
108
229
subEntryPages = [ ] ,
109
230
type,
110
231
expanded = false ,
232
+ copyable = false ,
111
233
toggleExpanded,
112
234
pageSize,
113
235
} ) => {
@@ -124,6 +246,7 @@ export const DefaultRenderer: Renderer = ({
124
246
{ subEntries . length } { subEntries . length > 1 ? `items` : `item` }
125
247
</ Info >
126
248
</ ExpandButton >
249
+ { copyable ? < CopyButton value = { value } /> : null }
127
250
{ expanded ? (
128
251
subEntryPages . length === 1 ? (
129
252
< SubEntries > { subEntries . map ( handleEntry ) } </ SubEntries >
@@ -166,6 +289,7 @@ export const DefaultRenderer: Renderer = ({
166
289
type ExplorerProps = Partial < RendererProps > & {
167
290
renderer ?: Renderer
168
291
defaultExpanded ?: true | Record < string , boolean >
292
+ copyable ?: boolean
169
293
}
170
294
171
295
type Property = {
@@ -183,6 +307,7 @@ export default function Explorer({
183
307
defaultExpanded,
184
308
renderer = DefaultRenderer ,
185
309
pageSize = 100 ,
310
+ copyable = false ,
186
311
...rest
187
312
} : ExplorerProps ) {
188
313
const [ expanded , setExpanded ] = React . useState ( Boolean ( defaultExpanded ) )
@@ -241,6 +366,7 @@ export default function Explorer({
241
366
key = { entry . label }
242
367
value = { value }
243
368
renderer = { renderer }
369
+ copyable = { copyable }
244
370
{ ...rest }
245
371
{ ...entry }
246
372
/>
@@ -250,6 +376,7 @@ export default function Explorer({
250
376
subEntryPages,
251
377
value,
252
378
expanded,
379
+ copyable,
253
380
toggleExpanded,
254
381
pageSize,
255
382
...rest ,
0 commit comments