Line data Source code
1 : import * as React from 'react';
2 : import * as styles from './json_view.module.css';
3 :
4 : import icons from '@ankoh/dashql-svg-symbols';
5 :
6 : import { useJsonViewerState } from './json_view_state.js';
7 : import { useToolVisibility } from './json_tool_state.js';
8 : import { bigIntToString } from './json_literal.js';
9 : import { classNames } from '../../utils/classnames.js';
10 :
11 : export interface SectionElementResult<T extends object, K = string | number> {
12 : value?: T;
13 : parentValue?: T;
14 : keyName?: K;
15 : keyPath?: K[];
16 : }
17 :
18 : export interface JsonCopyButtonProps<T extends object> extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'value'>, SectionElementResult<T> {
19 : expandKey: string;
20 : beforeCopy?: (
21 : copyText: string,
22 : keyName?: string | number,
23 : value?: T,
24 : parentValue?: T,
25 : expandKey?: string,
26 : keyPath?: (number | string)[],
27 : ) => string;
28 : }
29 :
30 1 : export function JsonCopyButton<T extends object>(props: JsonCopyButtonProps<T>) {
31 1 : const { keyName, value, parentValue, expandKey, keyPath, beforeCopy, ...other } = props;
32 1 : const { onCopied, enableClipboard, beforeCopy: globalBeforeCopy } = useJsonViewerState();
33 1 : const isShowTools = useToolVisibility(expandKey);
34 1 : const [copied, setCopied] = React.useState(false);
35 :
36 1 : if (enableClipboard === false || !isShowTools) return null;
37 :
38 1 : const click = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
39 0 : event.stopPropagation();
40 0 : let copyText = '';
41 0 : if (typeof value === 'number' && value === Infinity) {
42 0 : copyText = 'Infinity';
43 0 : } else if (typeof value === 'number' && isNaN(value)) {
44 0 : copyText = 'NaN';
45 0 : } else if (typeof value === 'bigint') {
46 0 : copyText = bigIntToString(value);
47 0 : } else if (value instanceof Date) {
48 0 : copyText = value.toLocaleString();
49 : } else {
50 0 : copyText = JSON.stringify(value, (_, v) => (typeof v === 'bigint' ? bigIntToString(v) : v), 2);
51 : }
52 :
53 : // Apply beforeCopy transformation if provided
54 : // Priority: component prop > global prop
55 0 : const finalBeforeCopy = beforeCopy || globalBeforeCopy;
56 0 : if (finalBeforeCopy && typeof finalBeforeCopy === 'function') {
57 0 : copyText = finalBeforeCopy(copyText, keyName, value, parentValue, expandKey, keyPath);
58 : }
59 :
60 0 : onCopied && onCopied(copyText, value);
61 0 : setCopied(true);
62 :
63 0 : const _clipboard = navigator.clipboard || {
64 0 : writeText(text: string) {
65 0 : return new Promise((reslove, reject) => {
66 0 : const textarea = document.createElement('textarea');
67 0 : textarea.style.position = 'absolute';
68 0 : textarea.style.opacity = '0';
69 0 : textarea.style.left = '-99999999px';
70 0 : textarea.value = text;
71 0 : document.body.appendChild(textarea);
72 0 : textarea.select();
73 0 : if (!document.execCommand('copy')) {
74 0 : reject();
75 : } else {
76 0 : reslove();
77 : }
78 0 : textarea.remove();
79 0 : });
80 0 : },
81 0 : };
82 :
83 0 : _clipboard
84 0 : .writeText(copyText)
85 0 : .then(() => {
86 0 : const timer = setTimeout(() => {
87 0 : setCopied(false);
88 0 : clearTimeout(timer);
89 0 : }, 3000);
90 0 : })
91 0 : .catch((_error) => { });
92 0 : };
93 :
94 1 : const sym = copied ? "clipboard_copied" : "clipboard_copy";
95 1 : return (
96 1 : <button
97 1 : {...other}
98 1 : type="button"
99 1 : aria-label={copied ? 'JSON copied' : 'Copy JSON value'}
100 1 : data-testid="json-copy-value"
101 1 : className={classNames(styles.copy_button, {
102 1 : [styles.copy_button_copied]: copied
103 1 : })}
104 1 : onClick={click}
105 : >
106 1 : <svg viewBox="0 0 32 36" aria-hidden="true">
107 1 : <use xlinkHref={`${icons}#${sym}`} />
108 1 : </svg>
109 1 : </button>
110 : );
111 1 : };
|