From 8e8190b8006120c72c1355eb952203bc4d176058 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Sat, 19 Sep 2020 22:24:49 +0200 Subject: [PATCH] Add copy-to-clipboard-button to all code blocks (#566) Signed-off-by: Tilman Vatteroth Co-authored-by: mrdrogdrog Co-authored-by: Tilman Vatteroth --- CHANGELOG.md | 1 + package.json | 2 + public/locales/en.json | 8 ++- .../common/copyable-field/copyable-field.tsx | 52 ----------------- .../common/copyable/copy-overlay.tsx | 56 +++++++++++++++++++ .../copy-to-clipboard-button.tsx | 26 +++++++++ .../copyable-field/copyable-field.tsx | 28 ++++++++++ .../buttons/share-link-button.tsx | 4 +- .../landing-layout/footer/version-info.tsx | 4 +- .../highlighted-code/highlighted-code.tsx | 34 ++++++----- .../utils/button-inside.scss | 3 + yarn.lock | 10 ++++ 12 files changed, 156 insertions(+), 72 deletions(-) delete mode 100644 src/components/common/copyable-field/copyable-field.tsx create mode 100644 src/components/common/copyable/copy-overlay.tsx create mode 100644 src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx create mode 100644 src/components/common/copyable/copyable-field/copyable-field.tsx create mode 100644 src/components/markdown-renderer/utils/button-inside.scss diff --git a/CHANGELOG.md b/CHANGELOG.md index 1900c61c6..41bbfa52e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - Code blocks with 'csv' as language render as tables. - Code blocks with 'markmap' are rendered as a mind map (see [the projects website](https://markmap.js.org/repl)). - All images can be clicked to show them in full screen. +- Code blocks have a 'Copy code to clipboard' button. - Code blocks with 'vega-lite' as language are rendered as [vega-lite diagrams](https://vega.github.io/vega-lite/examples/). ### Changed diff --git a/package.json b/package.json index 07ad9e4e8..be004db87 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/react-router": "5.1.8", "@types/react-router-bootstrap": "0.24.5", "@types/react-router-dom": "5.1.5", + "@types/uuid": "^8.3.0", "@typescript-eslint/eslint-plugin": "3.10.1", "@typescript-eslint/parser": "3.10.1", "abcjs": "5.12.0", @@ -98,6 +99,7 @@ "typescript": "4.0.3", "use-media": "1.4.0", "use-resize-observer": "6.1.0", + "uuid": "^8.3.0", "vega": "5.15.0", "vega-embed": "6.12.2", "vega-lite": "4.15.0" diff --git a/public/locales/en.json b/public/locales/en.json index 88b06e1bc..9d2ee8363 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -4,6 +4,9 @@ "title": "Collaborative markdown notes" }, "renderer": { + "highlightCode": { + "copyCode": "Copy code to clipboard" + }, "flowchart": { "invalidSyntax": "Invalid flowchart.js syntax!" }, @@ -126,7 +129,6 @@ "issueTracker": "Found a bug? Fill an issue!", "sourceCode": "Read the source code", "versionInfo": "Version info", - "successfullyCopied": "Copied!", "serverVersion": "Server version", "clientVersion": "Client version", "title": "You are using" @@ -379,7 +381,9 @@ "or": "or", "and": "and", "avatarOf": "avatar of '{{name}}'", - "why": "Why?" + "why": "Why?", + "successfullyCopied": "Copied!", + "copyError": "Error while copying!" }, "login": { "chooseMethod": "Choose method", diff --git a/src/components/common/copyable-field/copyable-field.tsx b/src/components/common/copyable-field/copyable-field.tsx deleted file mode 100644 index 8a3bffd40..000000000 --- a/src/components/common/copyable-field/copyable-field.tsx +++ /dev/null @@ -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 = ({ content }) => { - useTranslation() - const inputField = useRef(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 ( - - - {(props) => ( - - - - )} - - - - - - - - - - ) -} diff --git a/src/components/common/copyable/copy-overlay.tsx b/src/components/common/copyable/copy-overlay.tsx new file mode 100644 index 000000000..a6c25e374 --- /dev/null +++ b/src/components/common/copyable/copy-overlay.tsx @@ -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 +} + +export const CopyOverlay: React.FC = ({ content, clickComponent }) => { + useTranslation() + const [showCopiedTooltip, setShowCopiedTooltip] = useState(false) + const [error, setError] = useState(false) + const [tooltipId] = useState(() => 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 ( + + {(props) => ( + + + + + + + + + )} + + ) +} diff --git a/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx b/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx new file mode 100644 index 000000000..f9359f29d --- /dev/null +++ b/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.tsx @@ -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 = ({ content, size = 'sm', variant = 'dark' }) => { + const { t } = useTranslation() + const button = useRef(null) + + return ( + + + + + ) +} diff --git a/src/components/common/copyable/copyable-field/copyable-field.tsx b/src/components/common/copyable/copyable-field/copyable-field.tsx new file mode 100644 index 000000000..c3f7672f2 --- /dev/null +++ b/src/components/common/copyable/copyable-field/copyable-field.tsx @@ -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 = ({ content }) => { + useTranslation() + const button = useRef(null) + + return ( + + + + + + + + + + ) +} diff --git a/src/components/editor/document-bar/buttons/share-link-button.tsx b/src/components/editor/document-bar/buttons/share-link-button.tsx index 006bc57d4..8d24eefb0 100644 --- a/src/components/editor/document-bar/buttons/share-link-button.tsx +++ b/src/components/editor/document-bar/buttons/share-link-button.tsx @@ -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) diff --git a/src/components/landing-layout/footer/version-info.tsx b/src/components/landing-layout/footer/version-info.tsx index b5a6b5959..113bca104 100644 --- a/src/components/landing-layout/footer/version-info.tsx +++ b/src/components/landing-layout/footer/version-info.tsx @@ -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) diff --git a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx index 6fe09bf8a..42a5427b2 100644 --- a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx +++ b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx @@ -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 = ({ code, language }, [code, language]) return ( - - { - highlightedCode - .map((line, index) => { - return - -
- {line} -
-
- }) - } - -
) + + + { + highlightedCode + .map((line, index) => ( + + +
+ {line} +
+
+ )) + } +
+
+ +
+
) } diff --git a/src/components/markdown-renderer/utils/button-inside.scss b/src/components/markdown-renderer/utils/button-inside.scss new file mode 100644 index 000000000..424947cbc --- /dev/null +++ b/src/components/markdown-renderer/utils/button-inside.scss @@ -0,0 +1,3 @@ +.button-inside { + margin-top: -31px; +} diff --git a/yarn.lock b/yarn.lock index c98dd03c9..58a32333e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2355,6 +2355,11 @@ dependencies: "@types/jest" "*" +"@types/uuid@^8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" + integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== + "@types/warning@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52" @@ -14014,6 +14019,11 @@ uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + v8-compile-cache@^2.0.3: version "2.1.1" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"