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 (
-
-
- {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;
+ }
+}