diff --git a/client/components/CopyableTooltip.jsx b/client/components/CopyableTooltip.jsx new file mode 100644 index 0000000000..ddb9343d12 --- /dev/null +++ b/client/components/CopyableTooltip.jsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +/** + * CopyableTooltip: A reusable tooltip component with the ability + * to copy text to the clipboard when triggered. + */ +const CopyableTooltip = ({ className, label, copyText, children }) => { + const [isCopied, setIsCopied] = useState(false); + + const handleCopyClick = () => { + navigator.clipboard.writeText(copyText); + setIsCopied(true); + }; + + // Add click handler to element with the "copy-trigger" class + const processChildren = (childElements) => + React.Children.map(childElements, (child) => { + if (React.isValidElement(child)) { + const childClassNames = child.props.className || ''; + + if (childClassNames.includes('copy-trigger')) { + return React.cloneElement(child, { onClick: handleCopyClick }); + } + + if (child.props.children) { + const newChildren = processChildren(child.props.children); + return React.cloneElement(child, { children: newChildren }); + } + } + + return child; + }); + + const childrenWithClickHandler = processChildren(children); + + return ( +
setIsCopied(false)} + > + {childrenWithClickHandler} +
+ ); +}; + +CopyableTooltip.propTypes = { + className: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + copyText: PropTypes.string.isRequired, + children: PropTypes.node.isRequired +}; + +export default CopyableTooltip; diff --git a/client/images/copy.svg b/client/images/copy.svg new file mode 100644 index 0000000000..987ea172df --- /dev/null +++ b/client/images/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/modules/IDE/components/CopyableInput.jsx b/client/modules/IDE/components/CopyableInput.jsx index 1fa9f6bec3..6d7ee501aa 100644 --- a/client/modules/IDE/components/CopyableInput.jsx +++ b/client/modules/IDE/components/CopyableInput.jsx @@ -1,95 +1,73 @@ import PropTypes from 'prop-types'; -import React from 'react'; -import Clipboard from 'clipboard'; +import React, { useRef } from 'react'; import classNames from 'classnames'; -import { withTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; -import ShareIcon from '../../../images/share.svg'; - -class CopyableInput extends React.Component { - constructor(props) { - super(props); - this.onMouseLeaveHandler = this.onMouseLeaveHandler.bind(this); - } +import CopyableTooltip from '../../../components/CopyableTooltip'; - componentDidMount() { - this.clipboard = new Clipboard(this.input, { - target: () => this.input - }); +import ShareIcon from '../../../images/share.svg'; - this.clipboard.on('success', (e) => { - this.tooltip.classList.add('tooltipped'); - this.tooltip.classList.add('tooltipped-n'); - }); - } +const CopyableInput = ({ label, value, hasPreviewLink }) => { + const { t } = useTranslation(); + const inputRef = useRef(null); - componentWillUnmount() { - this.clipboard.destroy(); - } + const handleInputFocus = () => { + if (!inputRef?.current) return; + inputRef.current.select(); + }; - onMouseLeaveHandler() { - this.tooltip.classList.remove('tooltipped'); - this.tooltip.classList.remove('tooltipped-n'); - } + return ( +
+ + + - render() { - const { label, value, hasPreviewLink } = this.props; - const copyableInputClass = classNames({ - 'copyable-input': true, - 'copyable-input--with-preview': hasPreviewLink - }); - return ( -
-
{ - this.tooltip = element; - }} - onMouseLeave={this.onMouseLeaveHandler} + {hasPreviewLink && ( + - -
- {hasPreviewLink && ( - - - )} -
- ); - } -} +
+ ); +}; CopyableInput.propTypes = { label: PropTypes.string.isRequired, value: PropTypes.string.isRequired, - hasPreviewLink: PropTypes.bool, - t: PropTypes.func.isRequired + hasPreviewLink: PropTypes.bool }; CopyableInput.defaultProps = { hasPreviewLink: false }; -export default withTranslation()(CopyableInput); +export default CopyableInput; diff --git a/client/modules/IDE/components/Editor/index.jsx b/client/modules/IDE/components/Editor/index.jsx index 35e47248f8..f3a736f865 100644 --- a/client/modules/IDE/components/Editor/index.jsx +++ b/client/modules/IDE/components/Editor/index.jsx @@ -53,6 +53,7 @@ import '../../../../utils/codemirror-search'; import beepUrl from '../../../../sounds/audioAlert.mp3'; import RightArrowIcon from '../../../../images/right-arrow.svg'; import LeftArrowIcon from '../../../../images/left-arrow.svg'; +import CopyIcon from '../../../../images/copy.svg'; import { getHTMLFile } from '../../reducers/files'; import { selectActiveFile } from '../../selectors/files'; @@ -71,6 +72,7 @@ import UnsavedChangesIndicator from '../UnsavedChangesIndicator'; import { EditorContainer, EditorHolder } from './MobileEditor'; import { FolderIcon } from '../../../../common/icons'; import IconButton from '../../../../components/mobile/IconButton'; +import CopyableTooltip from '../../../../components/CopyableTooltip'; emmet(CodeMirror); @@ -507,6 +509,8 @@ class Editor extends React.Component { this.props.file.fileType === 'folder' || this.props.file.url }); + const editorContent = this._cm && this.getContent().content; + return ( {(matches) => @@ -535,9 +539,27 @@ class Editor extends React.Component { {this.props.file.name} + + + + + + +
{ this.codemirrorContainer = element; @@ -609,6 +631,7 @@ Editor.propTypes = { updateLintMessage: PropTypes.func.isRequired, clearLintMessage: PropTypes.func.isRequired, updateFileContent: PropTypes.func.isRequired, + copyFileContent: PropTypes.func.isRequired, fontSize: PropTypes.number.isRequired, file: PropTypes.shape({ name: PropTypes.string.isRequired, diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 8e37441633..f26885a204 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -91,6 +91,11 @@ const IDEView = () => { dispatch(updateFileContent(file.id, file.content)); }; + const copyFileContent = () => { + const file = cmRef.current.getContent(); + navigator.clipboard.writeText(file.content); + }; + useEffect(() => { dispatch(clearPersistedState()); @@ -183,6 +188,7 @@ const IDEView = () => { provideController={(ctl) => { cmRef.current = ctl; }} + copyFileContent={copyFileContent} /> diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss index 59446aeeed..f80e9a4127 100644 --- a/client/styles/components/_editor.scss +++ b/client/styles/components/_editor.scss @@ -394,6 +394,10 @@ pre.CodeMirror-line { font-size: #{12 / $base-font-size}rem; display: flex; justify-content: space-between; + + .editor__copy-btn { + width: 28px; + } } .editor__unsaved-changes { @@ -439,3 +443,14 @@ pre.CodeMirror-line { .emmet-close-tag { text-decoration: underline; } + +.editor__copy { + position: absolute; + top: 10%; + right: 5px; + z-index: 100; + + svg { + width: 16px; + } +}