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'
)}
>
-
+
+
{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