diff --git a/client/components/CopyableTool.jsx b/client/components/CopyableTool.jsx new file mode 100644 index 0000000000..21ac367dbd --- /dev/null +++ b/client/components/CopyableTool.jsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +/** + * CopyableTool: A reusable tooltip component with the ability + * to copy text to the clipboard when triggered. + */ +const CopyableTool = ({ 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} +
+ ); +}; + +CopyableTool.propTypes = { + className: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + copyText: PropTypes.string.isRequired, + children: PropTypes.node.isRequired +}; + +export default CopyableTool; 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 2d7b5036f2..b7ac70b7ec 100644 --- a/client/modules/IDE/components/CopyableInput.jsx +++ b/client/modules/IDE/components/CopyableInput.jsx @@ -1,36 +1,20 @@ import PropTypes from 'prop-types'; -import React, { useEffect, useRef, useState } from 'react'; -import Clipboard from 'clipboard'; +import React, { useRef } from 'react'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; +import CopyableTool from '../../../components/CopyableTool'; + import ShareIcon from '../../../images/share.svg'; const CopyableInput = ({ label, value, hasPreviewLink }) => { const { t } = useTranslation(); - - const [isCopied, setIsCopied] = useState(false); - const inputRef = useRef(null); - useEffect(() => { - const input = inputRef.current; - - if (!input) return; // should never happen - - const clipboard = new Clipboard(input, { - target: () => input - }); - - clipboard.on('success', () => { - setIsCopied(true); - }); - - // eslint-disable-next-line consistent-return - return () => { - clipboard.destroy(); - }; - }, [inputRef, setIsCopied]); + const handleInputFocus = () => { + if (!inputRef?.current) return; + inputRef.current.select(); + }; return (
{ hasPreviewLink && 'copyable-input--with-preview' )} > -
setIsCopied(false)} + -
+ + {hasPreviewLink && ( { @@ -137,7 +134,7 @@ class Editor extends React.Component { asi: true, eqeqeq: false, '-W041': false, - esversion: 11 + esversion: 7 } }, colorpicker: { @@ -168,7 +165,6 @@ class Editor extends React.Component { }, Enter: 'emmetInsertLineBreak', Esc: 'emmetResetAbbreviation', - [`Shift-Tab`]: false, [`${metaKey}-Enter`]: () => null, [`Shift-${metaKey}-Enter`]: () => null, [`${metaKey}-F`]: 'findPersistent', @@ -200,9 +196,12 @@ class Editor extends React.Component { }, 1000) ); - if (this._cm) { - this._cm.on('keyup', this.handleKeyUp); - } + this._cm.on('keyup', () => { + const temp = this.props.t('Editor.KeyUpLineNumber', { + lineNumber: parseInt(this._cm.getCursor().line + 1, 10) + }); + document.getElementById('current-line').innerHTML = temp; + }); this._cm.on('keydown', (_cm, e) => { // Show hint @@ -237,16 +236,6 @@ class Editor extends React.Component { componentDidUpdate(prevProps) { if (this.props.file.id !== prevProps.file.id) { - const fileMode = this.getFileMode(this.props.file.name); - if (fileMode === 'javascript') { - // Define the new Emmet configuration based on the file mode - const emmetConfig = { - preview: ['html'], - markTagPairs: false, - autoRenameTags: true - }; - this._cm.setOption('emmet', emmetConfig); - } const oldDoc = this._cm.swapDoc(this._docs[this.props.file.id]); this._docs[prevProps.file.id] = oldDoc; this._cm.focus(); @@ -334,9 +323,7 @@ class Editor extends React.Component { } componentWillUnmount() { - if (this._cm) { - this._cm.off('keyup', this.handleKeyUp); - } + this._cm = null; this.props.provideController(null); } @@ -352,7 +339,7 @@ class Editor extends React.Component { mode = 'application/json'; } else if (fileName.match(/.+\.(frag|glsl)$/i)) { mode = 'x-shader/x-fragment'; - } else if (fileName.match(/.+\.(vert|stl|mtl)$/i)) { + } else if (fileName.match(/.+\.(vert|stl)$/i)) { mode = 'x-shader/x-vertex'; } else { mode = 'text/plain'; @@ -366,11 +353,6 @@ class Editor extends React.Component { return updatedFile; } - handleKeyUp = () => { - const lineNumber = parseInt(this._cm.getCursor().line + 1, 10); - this.setState({ currentLine: lineNumber }); - }; - showFind() { this._cm.execCommand('findPersistent'); } @@ -527,14 +509,14 @@ class Editor extends React.Component { this.props.file.fileType === 'folder' || this.props.file.url }); - const { currentLine } = this.state; + const editorContent = this._cm && this.getContent().content; return ( {(matches) => matches ? (
-
+
-
+ + + + + +
{ this.codemirrorContainer = element; @@ -572,14 +572,11 @@ class Editor extends React.Component { name={this.props.file.name} /> ) : null} - + ) : ( -
+
-
+
{ @@ -601,10 +598,7 @@ class Editor extends React.Component { name={this.props.file.name} /> ) : null} - +
) @@ -622,8 +616,7 @@ Editor.propTypes = { linewrap: PropTypes.bool.isRequired, lintMessages: PropTypes.arrayOf( PropTypes.shape({ - severity: PropTypes.oneOf(['error', 'hint', 'info', 'warning']) - .isRequired, + severity: PropTypes.string.isRequired, line: PropTypes.number.isRequired, message: PropTypes.string.isRequired, id: PropTypes.number.isRequired @@ -638,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 4084facb77..f26885a204 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -4,13 +4,14 @@ import { useDispatch, useSelector } from 'react-redux'; import { useTranslation } from 'react-i18next'; import { Helmet } from 'react-helmet'; import SplitPane from 'react-split-pane'; +import MediaQuery from 'react-responsive'; import IDEKeyHandlers from '../components/IDEKeyHandlers'; import Sidebar from '../components/Sidebar'; import PreviewFrame from '../components/PreviewFrame'; import Console from '../components/Console'; import Toast from '../components/Toast'; import { updateFileContent } from '../actions/files'; - +import { stopSketch } from '../actions/ide'; import { autosaveProject, clearPersistedState, @@ -26,7 +27,6 @@ import { PreviewWrapper } from '../components/Editor/MobileEditor'; import IDEOverlays from '../components/IDEOverlays'; -import useIsMobile from '../hooks/useIsMobile'; function getTitle(project) { const { id } = project; @@ -38,7 +38,7 @@ function isAuth(pathname) { } function isOverlay(pathname) { - return pathname === '/feedback'; + return pathname === '/about' || pathname === '/feedback'; } function WarnIfUnsavedChanges() { @@ -48,26 +48,6 @@ function WarnIfUnsavedChanges() { const currentLocation = useLocation(); - // beforeunload handles closing or refreshing the window. - useEffect(() => { - const handleUnload = (e) => { - // See: https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#browser_compatibility - e.preventDefault(); - e.returnValue = t('Nav.WarningUnsavedChanges'); - }; - - if (hasUnsavedChanges) { - window.addEventListener('beforeunload', handleUnload); - } else { - window.removeEventListener('beforeunload', handleUnload); - } - - return () => { - window.removeEventListener('beforeunload', handleUnload); - }; - }, [t, hasUnsavedChanges]); - - // Prompt handles internal navigation between pages. return ( { - const isMobile = useIsMobile(); const ide = useSelector((state) => state.ide); const preferences = useSelector((state) => state.preferences); const project = useSelector((state) => state.project); @@ -102,8 +80,7 @@ const IDEView = () => { const [consoleSize, setConsoleSize] = useState(150); const [sidebarSize, setSidebarSize] = useState(160); - const [isOverlayVisible, setIsOverlayVisible] = useState(false); - const [MaxSize, setMaxSize] = useState(window.innerWidth); + const [isOverlayVisible, setIsOverlayVisible] = useState(true); const cmRef = useRef({}); @@ -114,8 +91,15 @@ const IDEView = () => { dispatch(updateFileContent(file.id, file.content)); }; + const copyFileContent = () => { + const file = cmRef.current.getContent(); + navigator.clipboard.writeText(file.content); + }; + useEffect(() => { dispatch(clearPersistedState()); + + dispatch(stopSketch()); }, [dispatch]); useEffect(() => { @@ -148,22 +132,6 @@ const IDEView = () => { } }; }, [shouldAutosave, dispatch]); - useEffect(() => { - const updateInnerWidth = (e) => { - setMaxSize(e.target.innerWidth); - }; - - window.addEventListener('resize', updateInnerWidth); - - return () => { - window.removeEventListener('resize', updateInnerWidth); - }; - }, [setMaxSize]); - - const consoleCollapsedSize = 29; - const currentConsoleSize = ide.consoleIsExpanded - ? consoleSize - : consoleCollapsedSize; return ( @@ -176,111 +144,106 @@ const IDEView = () => {
- {isMobile ? ( - <> - - - { - setConsoleSize(size); - setIsOverlayVisible(true); - }} - onDragFinished={() => { - setIsOverlayVisible(false); - }} - allowResize={ide.consoleIsExpanded} - className="editor-preview-subpanel" - > - - - - - - - { - cmRef.current = ctl; - }} - /> - - - ) : ( -
- { - setSidebarSize(size); - }} - allowResize={ide.sidebarIsExpanded} - minSize={150} - > - - { - setIsOverlayVisible(true); - }} - onDragFinished={() => { - setIsOverlayVisible(false); - }} - resizerStyle={{ - borderLeftWidth: '2px', - borderRightWidth: '2px', - width: '2px', - margin: '0px 0px' - }} - > + + {(matches) => + matches ? ( +
{ - setConsoleSize(size); + setSidebarSize(size); }} - allowResize={ide.consoleIsExpanded} - className="editor-preview-subpanel" + allowResize={ide.sidebarIsExpanded} + minSize={125} > - { - cmRef.current = ctl; + + { + setIsOverlayVisible(true); }} - /> - + onDragFinished={() => { + // overlayRef.current.style.display = 'none'; + setIsOverlayVisible(false); + }} + resizerStyle={{ + borderLeftWidth: '2px', + borderRightWidth: '2px', + width: '2px', + margin: '0px 0px' + }} + > + setConsoleSize(size)} + allowResize={ide.consoleIsExpanded} + className="editor-preview-subpanel" + > + { + cmRef.current = ctl; + }} + copyFileContent={copyFileContent} + /> + + +
+
+

+ {t('Toolbar.Preview')} +

+
+
+
+
+ {((preferences.textOutput || preferences.gridOutput) && + ide.isPlaying) || + ide.isAccessibleOutputPlaying} +
+ +
+
+
-
-
-

- {t('Toolbar.Preview')} -

-
-
+
+ ) : ( + <> + + + - - - -
-
- )} + + + + + + { + cmRef.current = ctl; + }} + /> + + + ) + } + ); diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss index 0b92dec37b..e516c93caf 100644 --- a/client/styles/components/_editor.scss +++ b/client/styles/components/_editor.scss @@ -1,28 +1,26 @@ -@use "sass:math"; - .CodeMirror { font-family: Inconsolata, monospace; height: 100%; } .CodeMirror-linenumbers { - padding-right: #{math.div(10, $base-font-size)}rem; + padding-right: #{10 / $base-font-size}rem; } .CodeMirror-linenumber { - width: #{math.div(32, $base-font-size)}rem; - left: #{math.div(-3, $base-font-size)}rem !important; + width: #{32 / $base-font-size}rem; + left: #{-3 / $base-font-size}rem !important; @include themify() { color: getThemifyVariable("inactive-text-color"); } } .CodeMirror-lines { - padding-top: #{math.div(25, $base-font-size)}rem; + padding-top: #{25 / $base-font-size}rem; } pre.CodeMirror-line { - padding-left: #{math.div(5, $base-font-size)}rem; + padding-left: #{5 / $base-font-size}rem; } .CodeMirror-gutter-wrapper { @@ -35,7 +33,7 @@ pre.CodeMirror-line { .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-multiple { background-image: none; - width: #{math.div(49, $base-font-size)}rem; + width: #{49 / $base-font-size}rem; position: absolute; height: 100%; right: 100%; @@ -57,7 +55,7 @@ pre.CodeMirror-line { .CodeMirror-gutter-elt:not(.CodeMirror-linenumber) { opacity: 0.2; - width: #{math.div(49, $base-font-size)}rem !important; + width: #{49 / $base-font-size}rem !important; height: 100%; left: 49px !important; // background-color: rgb(255, 95, 82); @@ -80,7 +78,7 @@ pre.CodeMirror-line { border-color: getThemifyVariable("ide-border-color"); } // left: 0 !important; - width: #{math.div(48, $base-font-size)}rem; + width: #{48 / $base-font-size}rem; } /* @@ -91,8 +89,8 @@ pre.CodeMirror-line { position: fixed; top: 0; left: 50%; - margin-left: #{math.div(-552 * 0.5, $base-font-size)}rem; - + margin-left: -#{552/2 / $base-font-size}rem; + @media (max-width: 770px) { left: 0; right: 0; @@ -100,12 +98,12 @@ pre.CodeMirror-line { margin-left: 0; } - z-index: 10; + // z-index: 20; width: 580px; font-family: Montserrat, sans-serif; - padding: #{math.div(8, $base-font-size)}rem #{math.div(10, $base-font-size)}rem #{math.div(5, $base-font-size)}rem #{math.div(9, $base-font-size)}rem; + padding: #{8 / $base-font-size}rem #{10 / $base-font-size}rem #{5 / $base-font-size}rem #{9 / $base-font-size}rem; border-radius: 2px; @@ -123,7 +121,7 @@ pre.CodeMirror-line { } .Toggle-replace-btn-div { - height: #{math.div(40, $base-font-size)}rem; + height: #{40 / $base-font-size}rem; padding: 0; } @@ -133,17 +131,14 @@ pre.CodeMirror-line { } .CodeMirror-search-results { - margin: 0 #{math.div(20, $base-font-size)}rem; - width: #{math.div(75, $base-font-size)}rem; - font-size: #{math.div(12, $base-font-size)}rem; + margin: 0 #{20 / $base-font-size}rem; + width: #{75 / $base-font-size}rem; + font-size: #{12 / $base-font-size}rem; } .CodeMirror-find-controls { - width: 100%; display: flex; align-items: center; - justify-content: space-between; - height: #{math.div(35, $base-font-size)}rem; } .CodeMirror-search-inputs { width: 30%; @@ -155,41 +150,39 @@ pre.CodeMirror-line { align-items: center; } .CodeMirror-search-controls { - width: 60%; display: flex; - flex-wrap: wrap-reverse; - justify-content: flex-start; - align-items: flex-end; + align-items: center; + justify-content: end; } .CodeMirror-replace-controls { display: flex; - margin-left: #{math.div(10, $base-font-size)}rem; + margin-left: #{10 / $base-font-size}rem; } .CodeMirror-replace-options { - width: #{math.div(552, $base-font-size)}rem; - height: #{math.div(65, $base-font-size)}rem; + width: #{552 / $base-font-size}rem; + height: #{65 / $base-font-size}rem; display: flex; justify-content: center; align-items: center; } .CodeMirror-replace-options button { - width: #{math.div(200, $base-font-size)}rem; + width: #{200 / $base-font-size}rem; } .CodeMirror-search-title { display: block; - margin-bottom: #{math.div(12, $base-font-size)}rem; + margin-bottom: #{12 / $base-font-size}rem; - font-size: #{math.div(21, $base-font-size)}rem; + font-size: #{21 / $base-font-size}rem; font-weight: bold; } .CodeMirror-search-field { display: block; width: 100%; - max-width: #{math.div(166, $base-font-size)}rem; - margin-bottom: #{math.div(4, $base-font-size)}rem; + max-width: #{166 / $base-font-size}rem; + margin-bottom: #{4 / $base-font-size}rem; @include themify() { color: getThemifyVariable("input-text-color"); background-color: getThemifyVariable("input-secondary-background-color"); @@ -207,7 +200,7 @@ pre.CodeMirror-line { .CodeMirror-search-count { display: block; - height: #{math.div(20, $base-font-size)}rem; + height: #{20 / $base-font-size}rem; text-align: right; } @@ -220,7 +213,7 @@ pre.CodeMirror-line { display: flex; justify-content: flex-end; align-items: center; - margin-left: #{math.div(10, $base-font-size)}rem; + margin-left: #{10 / $base-font-size}rem; @media (max-width: 579px) { display: none; @@ -232,17 +225,17 @@ pre.CodeMirror-line { .CodeMirror-word-button { @include themify() { // @extend %button; - padding: #{math.div(2, $base-font-size)}rem #{math.div(7, $base-font-size)}rem; + padding: #{2 / $base-font-size}rem #{7 / $base-font-size}rem; border: 2px solid transparent; &:hover { border-color: getThemifyVariable("button-border-color"); } } - width: #{math.div(35, $base-font-size)}rem; - height: #{math.div(35, $base-font-size)}rem; + width: #{35 / $base-font-size}rem; + height: #{35 / $base-font-size}rem; & + & { - margin-left: #{math.div(3, $base-font-size)}rem; + margin-left: #{3 / $base-font-size}rem; } word-break: keep-all; @@ -273,13 +266,13 @@ pre.CodeMirror-line { } .CodeMirror-search-button { - margin-right: #{math.div(10, $base-font-size)}rem; + margin-right: #{10 / $base-font-size}rem; } .CodeMirror-search-match { background: gold; - border-top: #{math.div(1, $base-font-size)}rem solid orange; - border-bottom: #{math.div(1, $base-font-size)}rem solid orange; + border-top: #{1 / $base-font-size}rem solid orange; + border-bottom: #{1 / $base-font-size}rem solid orange; box-sizing: border-box; opacity: 0.5; } @@ -361,7 +354,7 @@ pre.CodeMirror-line { vertical-align: middle; height: 0.85em; line-height: 0.7; - padding: 0 #{math.div(5, $base-font-size)}rem; + padding: 0 #{5 / $base-font-size}rem; font-family: serif; } @@ -376,7 +369,7 @@ pre.CodeMirror-line { } .editor-holder { - height: calc(100% - #{math.div(29, $base-font-size)}rem); + height: calc(100% - #{29 / $base-font-size}rem); width: 100%; position: absolute; @include themify() { @@ -388,23 +381,24 @@ pre.CodeMirror-line { } .editor__header { - height: #{math.div(29, $base-font-size)}rem; + height: #{29 / $base-font-size}rem; } .editor__file-name { @include themify() { color: getThemifyVariable("primary-text-color"); } - height: #{math.div(29, $base-font-size)}rem; - padding-top: #{math.div(7, $base-font-size)}rem; - padding-left: #{math.div(56, $base-font-size)}rem; - font-size: #{math.div(12, $base-font-size)}rem; + height: #{29 / $base-font-size}rem; + padding-top: #{7 / $base-font-size}rem; + padding-left: #{56 / $base-font-size}rem; + font-size: #{12 / $base-font-size}rem; display: flex; justify-content: space-between; + } .editor__unsaved-changes { - margin-left: #{math.div(2, $base-font-size)}rem; + margin-left: #{2 / $base-font-size}rem; } /** Inline abbreviation preview */ @@ -420,8 +414,8 @@ pre.CodeMirror-line { } & .CodeMirror { height: auto; - max-width: #{math.div(400, $base-font-size)}rem; - max-height: #{math.div(300, $base-font-size)}rem; + max-width: #{400 / $base-font-size}rem; + max-height: #{300 / $base-font-size}rem; border: none; } } @@ -446,3 +440,14 @@ pre.CodeMirror-line { .emmet-close-tag { text-decoration: underline; } + +.editor__copy { + position: absolute; + top: 10%; + right: 5px; + z-index: 100; + + svg { + width: 16px; + } +} \ No newline at end of file