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 { HTMLAttributes } from 'react'
|
||||||
|
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
import YouTube from 'react-youtube'
|
import YouTube from 'react-youtube'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Tooltip from '@mui/material/Tooltip'
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
@ -27,10 +28,10 @@ import {
|
|||||||
isInlineMedia,
|
isInlineMedia,
|
||||||
} from 'models/chat'
|
} from 'models/chat'
|
||||||
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
||||||
|
import { CopyableBlock } from 'components/CopyableBlock/CopyableBlock'
|
||||||
import { SyntaxHighlighter } from '../../components/SyntaxHighlighter'
|
|
||||||
|
|
||||||
import { InlineMedia } from './InlineMedia'
|
import { InlineMedia } from './InlineMedia'
|
||||||
|
|
||||||
import './Message.sass'
|
import './Message.sass'
|
||||||
|
|
||||||
export interface MessageProps {
|
export interface MessageProps {
|
||||||
@ -65,14 +66,17 @@ const componentMap = {
|
|||||||
// https://github.com/remarkjs/react-markdown#use-custom-components-syntax-highlight
|
// https://github.com/remarkjs/react-markdown#use-custom-components-syntax-highlight
|
||||||
code({ node, inline, className, children, style, ...props }: CodeProps) {
|
code({ node, inline, className, children, style, ...props }: CodeProps) {
|
||||||
const match = /language-(\w+)/.exec(className || '')
|
const match = /language-(\w+)/.exec(className || '')
|
||||||
|
|
||||||
return !inline && match ? (
|
return !inline && match ? (
|
||||||
<SyntaxHighlighter
|
<CopyableBlock>
|
||||||
children={String(children).replace(/\n$/, '')}
|
<SyntaxHighlighter
|
||||||
language={match[1]}
|
children={String(children).replace(/\n$/, '')}
|
||||||
style={materialDark}
|
language={match[1]}
|
||||||
PreTag="div"
|
style={materialDark}
|
||||||
{...props}
|
PreTag="div"
|
||||||
/>
|
{...props}
|
||||||
|
/>
|
||||||
|
</CopyableBlock>
|
||||||
) : (
|
) : (
|
||||||
<code className={className} {...props}>
|
<code className={className} {...props}>
|
||||||
{children}
|
{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 Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from '@mui/material/Dialog'
|
||||||
import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
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 DialogContent from '@mui/material/DialogContent'
|
||||||
import DialogContentText from '@mui/material/DialogContentText'
|
import DialogContentText from '@mui/material/DialogContentText'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
|
import { CopyableBlock } from 'components/CopyableBlock/CopyableBlock'
|
||||||
import { SyntaxHighlighter } from '../../components/SyntaxHighlighter'
|
|
||||||
|
|
||||||
interface EmbedCodeDialogProps {
|
interface EmbedCodeDialogProps {
|
||||||
showEmbedCode: boolean
|
showEmbedCode: boolean
|
||||||
@ -29,22 +29,24 @@ export const EmbedCodeDialog = ({
|
|||||||
<Dialog open={showEmbedCode} onClose={handleEmbedCodeWindowClose}>
|
<Dialog open={showEmbedCode} onClose={handleEmbedCodeWindowClose}>
|
||||||
<DialogTitle>Room embed code</DialogTitle>
|
<DialogTitle>Room embed code</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<SyntaxHighlighter
|
<CopyableBlock>
|
||||||
language="html"
|
<SyntaxHighlighter
|
||||||
style={materialDark}
|
language="html"
|
||||||
PreTag="div"
|
style={materialDark}
|
||||||
lineProps={{
|
PreTag="div"
|
||||||
style: {
|
lineProps={{
|
||||||
wordBreak: 'break-all',
|
style: {
|
||||||
whiteSpace: 'pre-wrap',
|
wordBreak: 'break-all',
|
||||||
},
|
whiteSpace: 'pre-wrap',
|
||||||
}}
|
},
|
||||||
wrapLines={true}
|
}}
|
||||||
>
|
wrapLines={true}
|
||||||
{`<iframe src="${embedUrl}" allow="${iframeFeatureAllowList.join(
|
>
|
||||||
';'
|
{`<iframe src="${embedUrl}" allow="${iframeFeatureAllowList.join(
|
||||||
)}" width="800" height="800" />`}
|
';'
|
||||||
</SyntaxHighlighter>
|
)}" width="800" height="800" />`}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</CopyableBlock>
|
||||||
<DialogContentText
|
<DialogContentText
|
||||||
sx={{
|
sx={{
|
||||||
mb: 2,
|
mb: 2,
|
||||||
|
Loading…
Reference in New Issue
Block a user