added copy clipboard
parent
1f5d1b2641
commit
ca0093dfa4
@ -0,0 +1,64 @@
|
|||||||
|
import React, { useRef, useState, useEffect } from 'react';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faCopy, faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
const CodeBlock = ({ children }) => {
|
||||||
|
const textareaRef = useRef(null);
|
||||||
|
const [codeString, setCodeString] = useState('');
|
||||||
|
const [copied, setCopied] = useState(false); // New state variable
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef.current) {
|
||||||
|
setCodeString(textareaRef.current.textContent || '');
|
||||||
|
}
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
|
const handleCopyClick = () => {
|
||||||
|
if (codeString) {
|
||||||
|
navigator.clipboard.writeText(codeString).then(() => {
|
||||||
|
setCopied(true); // Set copied state to true
|
||||||
|
setTimeout(() => setCopied(false), 3000); // Reset after 3 seconds
|
||||||
|
|
||||||
|
//alert('Code copied to clipboard!');
|
||||||
|
}, () => {
|
||||||
|
alert('Failed to copy code!');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative', borderRadius: '5px', top: '20px' }}>
|
||||||
|
<pre style={{ margin: 0, padding: '0px', fontSize: '1.1em' }}>
|
||||||
|
<code ref={textareaRef} style={{fontSize: '0.9em' }}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
<button
|
||||||
|
onClick={handleCopyClick}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '10px',
|
||||||
|
right: '10px',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '5px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '0.5em',
|
||||||
|
transition: 'color 0.3s',
|
||||||
|
}}
|
||||||
|
//onMouseOver={(e: React.MouseEvent<HTMLButtonElement>) => e.currentTarget.style.color = '#007bff'}
|
||||||
|
//onMouseOut={(e: React.MouseEvent<HTMLButtonElement>) => e.currentTarget.style.color = 'black'}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={copied ? faCheck : faCopy}
|
||||||
|
size="2x"
|
||||||
|
style={{ opacity: 0.5 }}
|
||||||
|
onMouseOver={(e: React.MouseEvent<SVGSVGElement>) => e.currentTarget.style.opacity = '1'}
|
||||||
|
onMouseOut={(e: React.MouseEvent<SVGSVGElement>) => e.currentTarget.style.opacity = '0.5'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeBlock;
|
@ -0,0 +1,22 @@
|
|||||||
|
import cn from 'clsx'
|
||||||
|
import type { ComponentProps, ReactElement } from 'react'
|
||||||
|
|
||||||
|
export const Button = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<'button'>): ReactElement => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
'nextra-button nx-transition-all active:nx-opacity-50',
|
||||||
|
'nx-bg-primary-700/5 nx-border nx-border-black/5 nx-text-gray-600 hover:nx-text-gray-900 nx-rounded-md nx-p-1.5',
|
||||||
|
'dark:nx-bg-primary-300/10 dark:nx-border-white/10 dark:nx-text-gray-400 dark:hover:nx-text-gray-50',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import type { ComponentProps, ReactElement } from 'react'
|
||||||
|
|
||||||
|
export function CheckIcon(props: ComponentProps<'svg'>): ReactElement {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import type { ComponentProps, ReactElement } from 'react'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { CheckIcon } from './check'
|
||||||
|
import { CopyIcon } from './copy'
|
||||||
|
import { Button } from './button'
|
||||||
|
|
||||||
|
export const CopyToClipboard = ({
|
||||||
|
getValue,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
getValue: () => string
|
||||||
|
} & ComponentProps<'button'>): ReactElement => {
|
||||||
|
const [isCopied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCopied) return
|
||||||
|
const timerId = setTimeout(() => {
|
||||||
|
setCopied(false)
|
||||||
|
}, 2000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timerId)
|
||||||
|
}
|
||||||
|
}, [isCopied])
|
||||||
|
|
||||||
|
const handleClick = useCallback<
|
||||||
|
NonNullable<ComponentProps<'button'>['onClick']>
|
||||||
|
>(async () => {
|
||||||
|
setCopied(true)
|
||||||
|
if (!navigator?.clipboard) {
|
||||||
|
console.error('Access to clipboard rejected!')
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(getValue())
|
||||||
|
} catch {
|
||||||
|
console.error('Failed to copy!')
|
||||||
|
}
|
||||||
|
}, [getValue])
|
||||||
|
|
||||||
|
const IconToUse = isCopied ? CheckIcon : CopyIcon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={handleClick} title="Copy code" tabIndex={0} {...props}>
|
||||||
|
<IconToUse className="nextra-copy-icon nx-pointer-events-none nx-h-4 nx-w-4" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
import type { ComponentProps, ReactElement } from 'react'
|
||||||
|
|
||||||
|
export function CopyIcon(props: ComponentProps<'svg'>): ReactElement {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke="currentColor"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x="9"
|
||||||
|
y="9"
|
||||||
|
width="13"
|
||||||
|
height="13"
|
||||||
|
rx="2"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5 15H4C2.89543 15 2 14.1046 2 13V4C2 2.89543 2.89543 2 4 2H13C14.1046 2 15 2.89543 15 4V5"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
import cn from 'clsx'
|
||||||
|
import type { ComponentProps, ReactElement } from 'react'
|
||||||
|
import { useCallback, useRef } from 'react'
|
||||||
|
import { WordWrapIcon } from './word-wrap'
|
||||||
|
import { Button } from './button'
|
||||||
|
import { CopyToClipboard } from './copy-to-clipboard'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
|
||||||
|
export const Pre = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
hasCopyCode = true,
|
||||||
|
filename,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<'pre'> & {
|
||||||
|
filename?: string
|
||||||
|
hasCopyCode?: boolean
|
||||||
|
}): ReactElement => {
|
||||||
|
const preRef = useRef<HTMLPreElement | null>(null);
|
||||||
|
|
||||||
|
const toggleWordWrap = useCallback(() => {
|
||||||
|
const htmlDataset = document.documentElement.dataset;
|
||||||
|
const hasWordWrap = 'nextraWordWrap' in htmlDataset;
|
||||||
|
if (hasWordWrap) {
|
||||||
|
delete htmlDataset.nextraWordWrap;
|
||||||
|
} else {
|
||||||
|
htmlDataset.nextraWordWrap = '';
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderChildren = () => {
|
||||||
|
if (React.isValidElement(children) && children.type === 'code') {
|
||||||
|
return children.props.children;
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="nextra-code-block nx-relative nx-mt-6 first:nx-mt-0">
|
||||||
|
{filename && (
|
||||||
|
<div className="nx-absolute nx-top-0 nx-z-[1] nx-w-full nx-truncate nx-rounded-t-xl nx-bg-primary-700/5 nx-py-2 nx-px-4 nx-text-xs nx-text-gray-700 dark:nx-bg-primary-300/10 dark:nx-text-gray-200">
|
||||||
|
{filename}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<pre
|
||||||
|
className={cn(
|
||||||
|
'nx-bg-primary-700/5 nx-mb-4 nx-overflow-x-auto nx-rounded-xl nx-subpixel-antialiased dark:nx-bg-primary-300/10 nx-text-[.9em]',
|
||||||
|
'contrast-more:nx-border contrast-more:nx-border-primary-900/20 contrast-more:nx-contrast-150 contrast-more:dark:nx-border-primary-100/40',
|
||||||
|
filename ? 'nx-pt-12 nx-pb-4' : 'nx-py-4',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={preRef}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{renderChildren()}
|
||||||
|
</pre>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'nx-opacity-0 nx-transition [div:hover>&]:nx-opacity-100 focus-within:nx-opacity-100',
|
||||||
|
'nx-flex nx-gap-1 nx-absolute nx-m-[11px] nx-right-0',
|
||||||
|
filename ? 'nx-top-8' : 'nx-top-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={toggleWordWrap}
|
||||||
|
className="md:nx-hidden"
|
||||||
|
title="Toggle word wrap elvis"
|
||||||
|
>
|
||||||
|
<WordWrapIcon className="nx-pointer-events-none nx-h-4 nx-w-4" />
|
||||||
|
</Button>
|
||||||
|
{hasCopyCode && (
|
||||||
|
<CopyToClipboard
|
||||||
|
getValue={() =>
|
||||||
|
preRef.current?.querySelector('code')?.textContent || ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import type { ComponentProps, ReactElement } from 'react'
|
||||||
|
|
||||||
|
export function WordWrapIcon(props: ComponentProps<'svg'>): ReactElement {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" width="24" height="24" {...props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 19h6v-2H4v2zM20 5H4v2h16V5zm-3 6H4v2h13.25c1.1 0 2 .9 2 2s-.9 2-2 2H15v-2l-3 3l3 3v-2h2c2.21 0 4-1.79 4-4s-1.79-4-4-4z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue