From 38c8a46d3270c9174f0e5294bfcdfa643e6f2511 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Wed, 2 Aug 2017 22:10:58 -0700 Subject: [PATCH] 'Copy to Clipboard' functionality (#163) * Adds 'Copy to Clipboard' functionality to all code blocks in readOnly * Show on hover, move to SC * :green_heart: * PR feedback --- .../CopyToClipboard/CopyToClipboard.js | 36 ++++++++++++++ frontend/components/CopyToClipboard/index.js | 3 ++ frontend/components/Editor/Editor.scss | 18 ------- frontend/components/Editor/components/Code.js | 39 ++++++++++++--- .../Editor/components/CopyButton.js | 49 +++++++++++++++++++ .../Editor/components/InlineCode.js | 12 +++++ frontend/components/Editor/schema.js | 3 +- package.json | 1 + yarn.lock | 10 ++++ 9 files changed, 146 insertions(+), 25 deletions(-) create mode 100644 frontend/components/CopyToClipboard/CopyToClipboard.js create mode 100644 frontend/components/CopyToClipboard/index.js create mode 100644 frontend/components/Editor/components/CopyButton.js create mode 100644 frontend/components/Editor/components/InlineCode.js diff --git a/frontend/components/CopyToClipboard/CopyToClipboard.js b/frontend/components/CopyToClipboard/CopyToClipboard.js new file mode 100644 index 00000000..2e12e134 --- /dev/null +++ b/frontend/components/CopyToClipboard/CopyToClipboard.js @@ -0,0 +1,36 @@ +// @flow +import React, { PureComponent } from 'react'; +import copy from 'copy-to-clipboard'; + +class CopyToClipboard extends PureComponent { + props: { + text: string, + children?: React.Element, + onClick?: () => void, + onCopy: (string, boolean) => void, + }; + + onClick = (ev: SyntheticEvent) => { + const { text, onCopy, children } = this.props; + const elem = React.Children.only(children); + const result = copy(text, { + debug: __DEV__, + }); + + if (onCopy) { + onCopy(text, result); + } + + if (elem && elem.props && typeof elem.props.onClick === 'function') { + elem.props.onClick(ev); + } + }; + + render() { + const { text: _text, onCopy: _onCopy, children, ...rest } = this.props; + const elem = React.Children.only(children); + return React.cloneElement(elem, { ...rest, onClick: this.onClick }); + } +} + +export default CopyToClipboard; diff --git a/frontend/components/CopyToClipboard/index.js b/frontend/components/CopyToClipboard/index.js new file mode 100644 index 00000000..4d0c0539 --- /dev/null +++ b/frontend/components/CopyToClipboard/index.js @@ -0,0 +1,3 @@ +// @flow +import CopyToClipboard from './CopyToClipboard'; +export default CopyToClipboard; diff --git a/frontend/components/Editor/Editor.scss b/frontend/components/Editor/Editor.scss index fc26f949..a7694603 100644 --- a/frontend/components/Editor/Editor.scss +++ b/frontend/components/Editor/Editor.scss @@ -77,24 +77,6 @@ } } - code, - pre { - background: #efefef; - border-radius: 3px; - border: 1px solid #dedede; - } - - pre { - padding: 0 .5em; - - code { - background: none; - border: 0; - padding: 0; - border-radius: 0; - } - } - blockquote { border-left: 3px solid #efefef; padding-left: 10px; diff --git a/frontend/components/Editor/components/Code.js b/frontend/components/Editor/components/Code.js index 5ee78600..d6e2d538 100644 --- a/frontend/components/Editor/components/Code.js +++ b/frontend/components/Editor/components/Code.js @@ -1,13 +1,40 @@ // @flow import React from 'react'; +import styled from 'styled-components'; +import CopyButton from './CopyButton'; +import { color } from 'styles/constants'; import type { Props } from '../types'; -export default function Code({ children, attributes }: Props) { +export default function Code({ children, node, readOnly, attributes }: Props) { return ( -
-      
-        {children}
-      
-    
+ + {readOnly && } +
+        
+          {children}
+        
+      
+
); } + +const Pre = styled.pre` + padding: .5em 1em; + background: ${color.smoke}; + border-radius: 4px; + border: 1px solid ${color.smokeDark}; + + code { + padding: 0; + } +`; + +const Container = styled.div` + position: relative; + + &:hover { + > span { + opacity: 1; + } + } +`; diff --git a/frontend/components/Editor/components/CopyButton.js b/frontend/components/Editor/components/CopyButton.js new file mode 100644 index 00000000..696e6c61 --- /dev/null +++ b/frontend/components/Editor/components/CopyButton.js @@ -0,0 +1,49 @@ +// @flow +import React, { Component } from 'react'; +import { observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { color } from 'styles/constants'; +import styled from 'styled-components'; +import CopyToClipboard from 'components/CopyToClipboard'; + +@observer class CopyButton extends Component { + @observable copied: boolean = false; + copiedTimeout: ?number; + + componentWillUnmount() { + clearTimeout(this.copiedTimeout); + } + + handleCopy = () => { + this.copied = true; + this.copiedTimeout = setTimeout(() => (this.copied = false), 3000); + }; + + render() { + return ( + + {this.copied ? 'Copied!' : 'Copy to clipboard'} + + ); + } +} + +const StyledCopyToClipboard = styled(CopyToClipboard)` + position: absolute; + top: 0; + right: 0; + + opacity: 0; + transition: opacity 50ms ease-in-out; + z-index: 1; + font-size: 12px; + background: ${color.slateLight}; + border-radius: 2px; + padding: 1px 6px; + + &:hover { + background: ${color.slate}; + } +`; + +export default CopyButton; diff --git a/frontend/components/Editor/components/InlineCode.js b/frontend/components/Editor/components/InlineCode.js new file mode 100644 index 00000000..ad498a7b --- /dev/null +++ b/frontend/components/Editor/components/InlineCode.js @@ -0,0 +1,12 @@ +// @flow +import styled from 'styled-components'; +import { color } from 'styles/constants'; + +const InlineCode = styled.code` + padding: .25em; + background: ${color.smoke}; + border-radius: 4px; + border: 1px solid ${color.smokeDark}; +`; + +export default InlineCode; diff --git a/frontend/components/Editor/schema.js b/frontend/components/Editor/schema.js index 58a19c20..c1c0ced3 100644 --- a/frontend/components/Editor/schema.js +++ b/frontend/components/Editor/schema.js @@ -1,6 +1,7 @@ // @flow import React from 'react'; import Code from './components/Code'; +import InlineCode from './components/InlineCode'; import Image from './components/Image'; import Link from './components/Link'; import ListItem from './components/ListItem'; @@ -13,7 +14,7 @@ const createSchema = () => { return { marks: { bold: (props: Props) => {props.children}, - code: (props: Props) => {props.children}, + code: (props: Props) => {props.children}, italic: (props: Props) => {props.children}, underlined: (props: Props) => {props.children}, deleted: (props: Props) => {props.children}, diff --git a/package.json b/package.json index 1148fb01..dfa1cd5f 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "boundless-popover": "^1.0.4", "bugsnag": "^1.7.0", "classnames": "2.2.3", + "copy-to-clipboard": "^3.0.6", "css-loader": "0.23.1", "debug": "2.2.0", "dotenv": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index 3ca1b2dc..810762be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1874,6 +1874,12 @@ cookies@~0.7.0: depd "~1.1.0" keygrip "~1.0.1" +copy-to-clipboard@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.6.tgz#13c09bfea0408a5dc5bb987fee3b3986518c9d69" + dependencies: + toggle-selection "^1.0.3" + copy-to@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/copy-to/-/copy-to-2.0.1.tgz#2680fbb8068a48d08656b6098092bdafc906f4a5" @@ -8569,6 +8575,10 @@ to-space-case@^1.0.0: dependencies: to-no-case "^1.0.0" +toggle-selection@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.5.tgz#726c703de607193a73c32c7df49cd24950fc574f" + topo@1.x.x: version "1.1.0" resolved "https://registry.yarnpkg.com/topo/-/topo-1.1.0.tgz#e9d751615d1bb87dc865db182fa1ca0a5ef536d5"