mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-31 07:08:40 -04:00
Add copy-to-clipboard-button to all code blocks (#566)
Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de> Co-authored-by: mrdrogdrog <mr.drogdrog@gmail.com> Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
005c80ff55
commit
8e8190b800
12 changed files with 156 additions and 72 deletions
|
@ -1,52 +0,0 @@
|
|||
import React, { Fragment, useCallback, useRef, useState } from 'react'
|
||||
import { Button, FormControl, InputGroup, Overlay, Tooltip } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
|
||||
|
||||
export interface CopyableFieldProps {
|
||||
content: string
|
||||
}
|
||||
|
||||
export const CopyableField: React.FC<CopyableFieldProps> = ({ content }) => {
|
||||
useTranslation()
|
||||
const inputField = useRef<HTMLInputElement>(null)
|
||||
const [showCopiedTooltip, setShowCopiedTooltip] = useState(false)
|
||||
|
||||
const copyToClipboard = useCallback((content: string) => {
|
||||
navigator.clipboard.writeText(content).then(() => {
|
||||
setShowCopiedTooltip(true)
|
||||
setTimeout(() => { setShowCopiedTooltip(false) }, 2000)
|
||||
}).catch(() => {
|
||||
console.error("couldn't copy")
|
||||
})
|
||||
}, [])
|
||||
|
||||
const selectContent = useCallback(() => {
|
||||
if (!inputField.current) {
|
||||
return
|
||||
}
|
||||
inputField.current.focus()
|
||||
inputField.current.setSelectionRange(0, inputField.current.value.length)
|
||||
}, [inputField])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Overlay target={inputField} show={showCopiedTooltip} placement="top">
|
||||
{(props) => (
|
||||
<Tooltip id={'copied_' + content} {...props}>
|
||||
<Trans i18nKey={'landing.versionInfo.successfullyCopied'}/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Overlay>
|
||||
|
||||
<InputGroup className="my-3">
|
||||
<FormControl readOnly={true} ref={inputField} className={'text-center'} value={content} onMouseEnter={selectContent} />
|
||||
<InputGroup.Append>
|
||||
<Button variant="outline-secondary" onClick={() => copyToClipboard(content)} title={'Copy'}>
|
||||
<ForkAwesomeIcon icon='files-o'/>
|
||||
</Button>
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
56
src/components/common/copyable/copy-overlay.tsx
Normal file
56
src/components/common/copyable/copy-overlay.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import React, { RefObject, useCallback, useEffect, useState } from 'react'
|
||||
import { Overlay, Tooltip } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { ShowIf } from '../show-if/show-if'
|
||||
|
||||
export interface CopyOverlayProps {
|
||||
content: string
|
||||
clickComponent: RefObject<HTMLElement>
|
||||
}
|
||||
|
||||
export const CopyOverlay: React.FC<CopyOverlayProps> = ({ content, clickComponent }) => {
|
||||
useTranslation()
|
||||
const [showCopiedTooltip, setShowCopiedTooltip] = useState(false)
|
||||
const [error, setError] = useState(false)
|
||||
const [tooltipId] = useState<string>(() => uuid())
|
||||
|
||||
const copyToClipboard = useCallback((content: string) => {
|
||||
navigator.clipboard.writeText(content).then(() => {
|
||||
setError(false)
|
||||
}).catch(() => {
|
||||
setError(true)
|
||||
console.error("couldn't copy")
|
||||
}).finally(() => {
|
||||
setShowCopiedTooltip(true)
|
||||
setTimeout(() => { setShowCopiedTooltip(false) }, 2000)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (clickComponent && clickComponent.current) {
|
||||
clickComponent.current.addEventListener('click', () => copyToClipboard(content))
|
||||
const clickComponentSaved = clickComponent.current
|
||||
return () => {
|
||||
if (clickComponentSaved) {
|
||||
clickComponentSaved.removeEventListener('click', () => copyToClipboard(content))
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [clickComponent, copyToClipboard, content])
|
||||
|
||||
return (
|
||||
<Overlay target={clickComponent} show={showCopiedTooltip} placement="top">
|
||||
{(props) => (
|
||||
<Tooltip id={`copied_${tooltipId}`} {...props}>
|
||||
<ShowIf condition={error}>
|
||||
<Trans i18nKey={'common.copyError'}/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={!error}>
|
||||
<Trans i18nKey={'common.successfullyCopied'}/>
|
||||
</ShowIf>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Overlay>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import React, { Fragment, useRef } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { Variant } from 'react-bootstrap/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../fork-awesome/fork-awesome-icon'
|
||||
import { CopyOverlay } from '../copy-overlay'
|
||||
|
||||
export interface CopyToClipboardButtonProps {
|
||||
content: string
|
||||
size?: 'sm' | 'lg'
|
||||
variant?: Variant
|
||||
}
|
||||
|
||||
export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({ content, size = 'sm', variant = 'dark' }) => {
|
||||
const { t } = useTranslation()
|
||||
const button = useRef<HTMLButtonElement>(null)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Button ref={button} size={size} variant={variant} title={t('renderer.highlightCode.copyCode')}>
|
||||
<ForkAwesomeIcon icon='files-o'/>
|
||||
</Button>
|
||||
<CopyOverlay content={content} clickComponent={button}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import React, { Fragment, useRef } from 'react'
|
||||
import { Button, FormControl, InputGroup } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../fork-awesome/fork-awesome-icon'
|
||||
import { CopyOverlay } from '../copy-overlay'
|
||||
|
||||
export interface CopyableFieldProps {
|
||||
content: string
|
||||
}
|
||||
|
||||
export const CopyableField: React.FC<CopyableFieldProps> = ({ content }) => {
|
||||
useTranslation()
|
||||
const button = useRef<HTMLButtonElement>(null)
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<InputGroup className="my-3">
|
||||
<FormControl readOnly={true} className={'text-center'} value={content} />
|
||||
<InputGroup.Append>
|
||||
<Button variant="outline-secondary" ref={button} title={'Copy'}>
|
||||
<ForkAwesomeIcon icon='files-o'/>
|
||||
</Button>
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
<CopyOverlay content={content} clickComponent={button}/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import React, { Fragment, useState } from 'react'
|
||||
import { Modal } from 'react-bootstrap'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { CopyableField } from '../../../common/copyable-field/copyable-field'
|
||||
import { CommonModal } from '../../../common/modals/common-modal'
|
||||
import { CopyableField } from '../../../common/copyable/copyable-field/copyable-field'
|
||||
import { TranslatedIconButton } from '../../../common/icon-button/translated-icon-button'
|
||||
import { CommonModal } from '../../../common/modals/common-modal'
|
||||
|
||||
export const ShareLinkButton: React.FC = () => {
|
||||
const [showReadOnly, setShowReadOnly] = useState(false)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import equal from 'fast-deep-equal'
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Button, Col, Modal, Row } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
@ -5,10 +6,9 @@ import { useSelector } from 'react-redux'
|
|||
import { Link } from 'react-router-dom'
|
||||
import { ApplicationState } from '../../../redux'
|
||||
import frontendVersion from '../../../version.json'
|
||||
import { CopyableField } from '../../common/copyable/copyable-field/copyable-field'
|
||||
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
|
||||
import { ShowIf } from '../../common/show-if/show-if'
|
||||
import { CopyableField } from '../../common/copyable-field/copyable-field'
|
||||
import equal from 'fast-deep-equal'
|
||||
|
||||
export const VersionInfo: React.FC = () => {
|
||||
const [show, setShow] = useState(false)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import hljs from 'highlight.js'
|
||||
import React, { Fragment, useMemo } from 'react'
|
||||
import ReactHtmlParser from 'react-html-parser'
|
||||
import { CopyToClipboardButton } from '../../../../common/copyable/copy-to-clipboard-button/copy-to-clipboard-button'
|
||||
import '../../../utils/button-inside.scss'
|
||||
import './highlighted-code.scss'
|
||||
|
||||
export interface HighlightedCodeProps {
|
||||
|
@ -42,18 +44,22 @@ export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language
|
|||
}, [code, language])
|
||||
|
||||
return (
|
||||
<code className={`hljs ${startLineNumber !== undefined ? 'showGutter' : ''} ${wrapLines ? 'wrapLines' : ''}`}>
|
||||
{
|
||||
highlightedCode
|
||||
.map((line, index) => {
|
||||
return <Fragment key={index}>
|
||||
<span className={'linenumber'} data-line-number={(startLineNumber || 1) + index}/>
|
||||
<div className={'codeline'}>
|
||||
{line}
|
||||
</div>
|
||||
</Fragment>
|
||||
})
|
||||
}
|
||||
|
||||
</code>)
|
||||
<Fragment>
|
||||
<code className={`hljs ${startLineNumber !== undefined ? 'showGutter' : ''} ${wrapLines ? 'wrapLines' : ''}`}>
|
||||
{
|
||||
highlightedCode
|
||||
.map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
<span className={'linenumber'} data-line-number={(startLineNumber || 1) + index}/>
|
||||
<div className={'codeline'}>
|
||||
{line}
|
||||
</div>
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
</code>
|
||||
<div className={'text-right button-inside'}>
|
||||
<CopyToClipboardButton content={code}/>
|
||||
</div>
|
||||
</Fragment>)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.button-inside {
|
||||
margin-top: -31px;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue