forked from Shiloh/remnantchat
feat(ui): Add copy button for code (#178)
* feat(ui): stand up CopyableBlock component * feat(ui): implement clipboard writing * feat(ui): make code blocks copyable * refactor(imports): use SyntaxHighlighter directly * feat(ui): make copy button slightly transparent * feat(ui): tweak copy alert text
This commit is contained in:
parent
ff988d9da5
commit
c170edb692
53
src/components/CopyableBlock/CopyableBlock.tsx
Normal file
53
src/components/CopyableBlock/CopyableBlock.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import Box, { BoxProps } from '@mui/material/Box'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import Fab from '@mui/material/Fab'
|
||||
import ContentCopy from '@mui/icons-material/ContentCopy'
|
||||
import { useContext, useRef } from 'react'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
|
||||
interface CopyableBlockProps extends BoxProps {}
|
||||
|
||||
export const CopyableBlock = ({ children }: CopyableBlockProps) => {
|
||||
const { showAlert } = useContext(ShellContext)
|
||||
const boxRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleCopyClick = async () => {
|
||||
const div = boxRef?.current
|
||||
|
||||
if (!div) return
|
||||
|
||||
await navigator.clipboard.writeText(div.innerText)
|
||||
|
||||
showAlert('Copied to clipboard', { severity: 'success' })
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={boxRef}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
'&:hover button': {
|
||||
opacity: 0.75,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<Tooltip title="Copy to clipboard">
|
||||
<Fab
|
||||
color="default"
|
||||
size="small"
|
||||
onClick={handleCopyClick}
|
||||
sx={theme => ({
|
||||
position: 'absolute',
|
||||
top: '1em',
|
||||
right: '1em',
|
||||
opacity: 0,
|
||||
transition: theme.transitions.create(['opacity', 'transform']),
|
||||
})}
|
||||
>
|
||||
<ContentCopy />
|
||||
</Fab>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)
|
||||
}
|
1
src/components/CopyableBlock/index.ts
Normal file
1
src/components/CopyableBlock/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './CopyableBlock'
|
@ -1,4 +1,5 @@
|
||||
import { HTMLAttributes } from 'react'
|
||||
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import YouTube from 'react-youtube'
|
||||
import Box from '@mui/material/Box'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
@ -27,10 +28,10 @@ import {
|
||||
isInlineMedia,
|
||||
} from 'models/chat'
|
||||
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
|
||||
import { SyntaxHighlighter } from '../../components/SyntaxHighlighter'
|
||||
import { CopyableBlock } from 'components/CopyableBlock/CopyableBlock'
|
||||
|
||||
import { InlineMedia } from './InlineMedia'
|
||||
|
||||
import './Message.sass'
|
||||
|
||||
export interface MessageProps {
|
||||
@ -65,14 +66,17 @@ const componentMap = {
|
||||
// https://github.com/remarkjs/react-markdown#use-custom-components-syntax-highlight
|
||||
code({ node, inline, className, children, style, ...props }: CodeProps) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
|
||||
return !inline && match ? (
|
||||
<SyntaxHighlighter
|
||||
children={String(children).replace(/\n$/, '')}
|
||||
language={match[1]}
|
||||
style={materialDark}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
/>
|
||||
<CopyableBlock>
|
||||
<SyntaxHighlighter
|
||||
children={String(children).replace(/\n$/, '')}
|
||||
language={match[1]}
|
||||
style={materialDark}
|
||||
PreTag="div"
|
||||
{...props}
|
||||
/>
|
||||
</CopyableBlock>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
|
@ -1,3 +0,0 @@
|
||||
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
|
||||
export { SyntaxHighlighter }
|
@ -1 +0,0 @@
|
||||
export * from './SyntaxHighlighter'
|
@ -1,3 +1,4 @@
|
||||
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import Button from '@mui/material/Button'
|
||||
import Dialog from '@mui/material/Dialog'
|
||||
import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
@ -5,8 +6,7 @@ import DialogActions from '@mui/material/DialogActions'
|
||||
import DialogContent from '@mui/material/DialogContent'
|
||||
import DialogContentText from '@mui/material/DialogContentText'
|
||||
import DialogTitle from '@mui/material/DialogTitle'
|
||||
|
||||
import { SyntaxHighlighter } from '../../components/SyntaxHighlighter'
|
||||
import { CopyableBlock } from 'components/CopyableBlock/CopyableBlock'
|
||||
|
||||
interface EmbedCodeDialogProps {
|
||||
showEmbedCode: boolean
|
||||
@ -29,22 +29,24 @@ export const EmbedCodeDialog = ({
|
||||
<Dialog open={showEmbedCode} onClose={handleEmbedCodeWindowClose}>
|
||||
<DialogTitle>Room embed code</DialogTitle>
|
||||
<DialogContent>
|
||||
<SyntaxHighlighter
|
||||
language="html"
|
||||
style={materialDark}
|
||||
PreTag="div"
|
||||
lineProps={{
|
||||
style: {
|
||||
wordBreak: 'break-all',
|
||||
whiteSpace: 'pre-wrap',
|
||||
},
|
||||
}}
|
||||
wrapLines={true}
|
||||
>
|
||||
{`<iframe src="${embedUrl}" allow="${iframeFeatureAllowList.join(
|
||||
';'
|
||||
)}" width="800" height="800" />`}
|
||||
</SyntaxHighlighter>
|
||||
<CopyableBlock>
|
||||
<SyntaxHighlighter
|
||||
language="html"
|
||||
style={materialDark}
|
||||
PreTag="div"
|
||||
lineProps={{
|
||||
style: {
|
||||
wordBreak: 'break-all',
|
||||
whiteSpace: 'pre-wrap',
|
||||
},
|
||||
}}
|
||||
wrapLines={true}
|
||||
>
|
||||
{`<iframe src="${embedUrl}" allow="${iframeFeatureAllowList.join(
|
||||
';'
|
||||
)}" width="800" height="800" />`}
|
||||
</SyntaxHighlighter>
|
||||
</CopyableBlock>
|
||||
<DialogContentText
|
||||
sx={{
|
||||
mb: 2,
|
||||
|
Loading…
Reference in New Issue
Block a user