diff --git a/client/components/useAsModal.jsx b/client/components/useAsModal.jsx index 350d1de2ed..51373bb50e 100644 --- a/client/components/useAsModal.jsx +++ b/client/components/useAsModal.jsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'styled-components'; -import { useModalBehavior } from '../utils/custom-hooks'; +import { useModalBehavior } from '../modules/IDE/hooks/custom-hooks'; const BackgroundOverlay = styled.div` position: fixed; diff --git a/client/images/console-command-contrast.svg b/client/images/console-command-contrast.svg new file mode 100644 index 0000000000..a08ee8b0f7 --- /dev/null +++ b/client/images/console-command-contrast.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/console-command-dark.svg b/client/images/console-command-dark.svg new file mode 100644 index 0000000000..a08ee8b0f7 --- /dev/null +++ b/client/images/console-command-dark.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/console-command-light.svg b/client/images/console-command-light.svg new file mode 100644 index 0000000000..3def60e1fb --- /dev/null +++ b/client/images/console-command-light.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/console-result-contrast.svg b/client/images/console-result-contrast.svg new file mode 100644 index 0000000000..36fa2e36d4 --- /dev/null +++ b/client/images/console-result-contrast.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/console-result-dark.svg b/client/images/console-result-dark.svg new file mode 100644 index 0000000000..7c5c5c9b27 --- /dev/null +++ b/client/images/console-result-dark.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/images/console-result-light.svg b/client/images/console-result-light.svg new file mode 100644 index 0000000000..a51506e00a --- /dev/null +++ b/client/images/console-result-light.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/modules/App/App.jsx b/client/modules/App/App.jsx index 089d80f76d..3b74e1fca6 100644 --- a/client/modules/App/App.jsx +++ b/client/modules/App/App.jsx @@ -41,7 +41,7 @@ class App extends React.Component { render() { return (
- {false && this.state.isMounted && !window.devToolsExtension && getConfig('NODE_ENV') === 'development' && } + {this.state.isMounted && !window.devToolsExtension && getConfig('NODE_ENV') === 'development' && } {this.props.children}
); diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index eaee94929a..2e7a2e9ead 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useRef, useEffect } from 'react'; import PropTypes from 'prop-types'; import { withTranslation } from 'react-i18next'; @@ -23,40 +23,58 @@ import debugContrastUrl from '../../../images/console-debug-contrast.svg?byUrl'; import infoLightUrl from '../../../images/console-info-light.svg?byUrl'; import infoDarkUrl from '../../../images/console-info-dark.svg?byUrl'; import infoContrastUrl from '../../../images/console-info-contrast.svg?byUrl'; +import ConsoleInput from './ConsoleInput'; + +import commandLightUrl from '../../../images/console-command-light.svg?byUrl'; +import resultLightUrl from '../../../images/console-result-light.svg?byUrl'; +import commandDarkUrl from '../../../images/console-command-dark.svg?byUrl'; +import resultDarkUrl from '../../../images/console-result-dark.svg?byUrl'; +import commandContrastUrl from '../../../images/console-command-contrast.svg?byUrl'; +import resultContrastUrl from '../../../images/console-result-contrast.svg?byUrl'; import UpArrowIcon from '../../../images/up-arrow.svg'; import DownArrowIcon from '../../../images/down-arrow.svg'; import * as IDEActions from '../../IDE/actions/ide'; import * as ConsoleActions from '../../IDE/actions/console'; -import { useDidUpdate } from '../../../utils/custom-hooks'; +import { useDidUpdate } from '../hooks/custom-hooks'; +import useHandleMessageEvent from '../hooks/useHandleMessageEvent'; +import { listen } from '../../../utils/dispatcher'; const getConsoleFeedStyle = (theme, times, fontSize) => { - const style = {}; + const style = { + BASE_FONT_FAMILY: 'Inconsolata, monospace', + }; const CONSOLE_FEED_LIGHT_ICONS = { LOG_WARN_ICON: `url(${warnLightUrl})`, LOG_ERROR_ICON: `url(${errorLightUrl})`, LOG_DEBUG_ICON: `url(${debugLightUrl})`, - LOG_INFO_ICON: `url(${infoLightUrl})` + LOG_INFO_ICON: `url(${infoLightUrl})`, + LOG_COMMAND_ICON: `url(${commandLightUrl})`, + LOG_RESULT_ICON: `url(${resultLightUrl})` }; const CONSOLE_FEED_DARK_ICONS = { LOG_WARN_ICON: `url(${warnDarkUrl})`, LOG_ERROR_ICON: `url(${errorDarkUrl})`, LOG_DEBUG_ICON: `url(${debugDarkUrl})`, - LOG_INFO_ICON: `url(${infoDarkUrl})` + LOG_INFO_ICON: `url(${infoDarkUrl})`, + LOG_COMMAND_ICON: `url(${commandDarkUrl})`, + LOG_RESULT_ICON: `url(${resultDarkUrl})` }; const CONSOLE_FEED_CONTRAST_ICONS = { LOG_WARN_ICON: `url(${warnContrastUrl})`, LOG_ERROR_ICON: `url(${errorContrastUrl})`, LOG_DEBUG_ICON: `url(${debugContrastUrl})`, - LOG_INFO_ICON: `url(${infoContrastUrl})` + LOG_INFO_ICON: `url(${infoContrastUrl})`, + LOG_COMMAND_ICON: `url(${commandContrastUrl})`, + LOG_RESULT_ICON: `url(${resultContrastUrl})` }; const CONSOLE_FEED_SIZES = { TREENODE_LINE_HEIGHT: 1.2, BASE_FONT_SIZE: fontSize, ARROW_FONT_SIZE: fontSize, LOG_ICON_WIDTH: fontSize, - LOG_ICON_HEIGHT: 1.45 * fontSize, + LOG_ICON_HEIGHT: 1.45 * fontSize }; if (times > 1) { @@ -77,21 +95,25 @@ const getConsoleFeedStyle = (theme, times, fontSize) => { const Console = ({ t }) => { const consoleEvents = useSelector(state => state.console); const isExpanded = useSelector(state => state.ide.consoleIsExpanded); + const isPlaying = useSelector(state => state.ide.isPlaying); const { theme, fontSize } = useSelector(state => state.preferences); const { collapseConsole, expandConsole, clearConsole, dispatchConsoleEvent } = bindActionCreators({ ...IDEActions, ...ConsoleActions }, useDispatch()); - useDidUpdate(() => { - clearConsole(); - dispatchConsoleEvent(consoleEvents); - }, [theme, fontSize]); - const cm = useRef({}); useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; }); + const handleMessageEvent = useHandleMessageEvent(); + useEffect(() => { + const unsubscribe = listen(handleMessageEvent); + return function cleanup() { + unsubscribe(); + }; + }); + const consoleClass = classNames({ 'preview-console': true, 'preview-console--collapsed': !isExpanded @@ -117,26 +139,36 @@ const Console = ({ t }) => { -
- {consoleEvents.map((consoleEvent) => { - const { method, times } = consoleEvent; - return ( -
- { times > 1 && -
- {times} +
+
+ {consoleEvents.map((consoleEvent) => { + const { method, times } = consoleEvent; + return ( +
+ { times > 1 && +
+ {times} +
+ } +
- } - -
- ); - })} + ); + })} +
+ { isExpanded && isPlaying && + + }
); diff --git a/client/modules/IDE/components/ConsoleInput.jsx b/client/modules/IDE/components/ConsoleInput.jsx new file mode 100644 index 0000000000..274845192f --- /dev/null +++ b/client/modules/IDE/components/ConsoleInput.jsx @@ -0,0 +1,132 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import CodeMirror from 'codemirror'; +import { Encode } from 'console-feed'; + +import RightArrowIcon from '../../../images/right-arrow.svg'; +import { dispatch } from '../../../utils/dispatcher'; + +// heavily inspired by +// https://github.com/codesandbox/codesandbox-client/blob/92a1131f4ded6f7d9c16945dc7c18aa97c8ada27/packages/app/src/app/components/Preview/DevTools/Console/Input/index.tsx + +class ConsoleInput extends React.Component { + constructor(props) { + super(props); + this.state = { + commandHistory: [], + commandCursor: -1 + }; + } + + componentDidMount() { + this._cm = CodeMirror(this.codemirrorContainer, { // eslint-disable-line + theme: `p5-${this.props.theme}`, + scrollbarStyle: null, + keymap: 'sublime', + mode: 'javascript', + inputStyle: 'contenteditable' + }); + + this._cm.on('keydown', (cm, e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + e.stopPropagation(); + const value = cm.getValue(); + if (value.trim(' ') === '') { + return false; + } + const messages = [{ log: Encode({ method: 'command', data: [value] }) }]; + const consoleEvent = [{ method: 'command', data: [value] }]; + dispatch({ + source: 'console', + messages + }); + this.props.dispatchConsoleEvent(consoleEvent); + cm.setValue(''); + this.setState(state => ({ + commandCursor: -1, + commandHistory: [value, ...state.commandHistory], + })); + } else if (e.key === 'ArrowUp') { + const lineNumber = this._cm.getDoc().getCursor().line; + if (lineNumber !== 0) { + return false; + } + + this.setState((state) => { + const newCursor = Math.min( + state.commandCursor + 1, + state.commandHistory.length - 1 + ); + this._cm + .getDoc() + .setValue(state.commandHistory[newCursor] || ''); + const cursorPos = this._cm.getDoc().getLine(0).length - 1; + this._cm.getDoc().setCursor({ line: 0, ch: cursorPos }); + return { commandCursor: newCursor }; + }); + } else if (e.key === 'ArrowDown') { + const lineNumber = this._cm.getDoc().getCursor().line; + const lineCount = this._cm.getValue().split('\n').length; + if (lineNumber + 1 !== lineCount) { + return false; + } + + this.setState((state) => { + const newCursor = Math.max(state.commandCursor - 1, -1); + this._cm + .getDoc() + .setValue(state.commandHistory[newCursor] || ''); + const newLineCount = this._cm.getValue().split('\n').length; + const newLine = this._cm.getDoc().getLine(newLineCount); + const cursorPos = newLine ? newLine.length - 1 : 1; + this._cm.getDoc().setCursor({ line: lineCount, ch: cursorPos }); + return { commandCursor: newCursor }; + }); + } + return true; + }); + + this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; + } + + componentDidUpdate(prevProps) { + this._cm.setOption('theme', `p5-${this.props.theme}`); + this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; + this._cm.refresh(); + } + + componentWillUnmount() { + this._cm = null; + } + + render() { + return ( +
+
+
+
{ this.codemirrorContainer = element; }} className="console__editor" /> +
+ ); + } +} + +ConsoleInput.propTypes = { + theme: PropTypes.string.isRequired, + dispatchConsoleEvent: PropTypes.func.isRequired, + fontSize: PropTypes.number.isRequired +}; + + +export default ConsoleInput; diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index 3c838175c0..927dd248af 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -189,6 +189,7 @@ class Editor extends React.Component { const oldDoc = this._cm.swapDoc(this._docs[this.props.file.id]); this._docs[prevProps.file.id] = oldDoc; this._cm.focus(); + if (!prevProps.unsavedChanges) { setTimeout(() => this.props.setUnsavedChanges(false), 400); } diff --git a/client/modules/IDE/components/PreviewFrame.jsx b/client/modules/IDE/components/PreviewFrame.jsx index ef33bb3f64..142cb5ece7 100644 --- a/client/modules/IDE/components/PreviewFrame.jsx +++ b/client/modules/IDE/components/PreviewFrame.jsx @@ -2,13 +2,11 @@ import PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; // import escapeStringRegexp from 'escape-string-regexp'; -import { isEqual } from 'lodash'; import srcDoc from 'srcdoc-polyfill'; import loopProtect from 'loop-protect'; import { JSHINT } from 'jshint'; import decomment from 'decomment'; import classNames from 'classnames'; -import { Decode } from 'console-feed'; import { getBlobUrl } from '../actions/files'; import { resolvePathToFile } from '../../../../server/utils/filePath'; import { @@ -21,6 +19,7 @@ import { } from '../../../../server/utils/fileUtils'; import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets } from '../../../utils/consoleUtils'; +import { registerFrame } from '../../../utils/dispatcher'; const shouldRenderSketch = (props, prevProps = undefined) => { @@ -42,18 +41,18 @@ const shouldRenderSketch = (props, prevProps = undefined) => { class PreviewFrame extends React.Component { constructor(props) { super(props); - this.handleConsoleEvent = this.handleConsoleEvent.bind(this); + + this.iframe = React.createRef(); } componentDidMount() { - window.addEventListener('message', this.handleConsoleEvent); - const props = { ...this.props, previewIsRefreshing: this.props.previewIsRefreshing, isAccessibleOutputPlaying: this.props.isAccessibleOutputPlaying }; if (shouldRenderSketch(props)) this.renderSketch(); + registerFrame(this.iframe.current.contentWindow); } componentDidUpdate(prevProps) { @@ -63,51 +62,10 @@ class PreviewFrame extends React.Component { } componentWillUnmount() { - window.removeEventListener('message', this.handleConsoleEvent); - const iframeBody = this.iframeElement.contentDocument.body; + const iframeBody = this.iframe.current.contentDocument.body; if (iframeBody) { ReactDOM.unmountComponentAtNode(iframeBody); } } - handleConsoleEvent(messageEvent) { - if (Array.isArray(messageEvent.data)) { - const decodedMessages = messageEvent.data.map(message => - Object.assign(Decode(message.log), { - source: message.source - })); - - decodedMessages.every((message, index, arr) => { - const { data: args } = message; - let hasInfiniteLoop = false; - Object.keys(args).forEach((key) => { - if (typeof args[key] === 'string' && args[key].includes('Exiting potential infinite loop')) { - this.props.stopSketch(); - this.props.expandConsole(); - hasInfiniteLoop = true; - } - }); - if (hasInfiniteLoop) { - return false; - } - if (index === arr.length - 1) { - Object.assign(message, { times: 1 }); - return false; - } - const cur = Object.assign(message, { times: 1 }); - const nextIndex = index + 1; - while (isEqual(cur.data, arr[nextIndex].data) && cur.method === arr[nextIndex].method) { - cur.times += 1; - arr.splice(nextIndex, 1); - if (nextIndex === arr.length) { - return false; - } - } - return true; - }); - - this.props.dispatchConsoleEvent(decodedMessages); - } - } - addLoopProtect(sketchDoc) { const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); @@ -326,7 +284,7 @@ class PreviewFrame extends React.Component { } renderSketch() { - const doc = this.iframeElement; + const doc = this.iframe.current; const localFiles = this.injectLocalFiles(); if (this.props.isPlaying) { this.props.clearConsole(); @@ -355,7 +313,7 @@ class PreviewFrame extends React.Component { role="main" frameBorder="0" title="sketch preview" - ref={(element) => { this.iframeElement = element; }} + ref={this.iframe} sandbox={sandboxAttributes} /> ); @@ -377,17 +335,14 @@ PreviewFrame.propTypes = { url: PropTypes.string, id: PropTypes.string.isRequired })).isRequired, - dispatchConsoleEvent: PropTypes.func.isRequired, endSketchRefresh: PropTypes.func.isRequired, previewIsRefreshing: PropTypes.bool.isRequired, fullView: PropTypes.bool, setBlobUrl: PropTypes.func.isRequired, - stopSketch: PropTypes.func.isRequired, - expandConsole: PropTypes.func.isRequired, clearConsole: PropTypes.func.isRequired, cmController: PropTypes.shape({ getContent: PropTypes.func - }), + }) }; PreviewFrame.defaultProps = { diff --git a/client/utils/custom-hooks.js b/client/modules/IDE/hooks/custom-hooks.js similarity index 100% rename from client/utils/custom-hooks.js rename to client/modules/IDE/hooks/custom-hooks.js diff --git a/client/modules/IDE/hooks/useHandleMessageEvent.js b/client/modules/IDE/hooks/useHandleMessageEvent.js new file mode 100644 index 0000000000..2bd22168b7 --- /dev/null +++ b/client/modules/IDE/hooks/useHandleMessageEvent.js @@ -0,0 +1,47 @@ +import { useDispatch } from 'react-redux'; +import { Decode } from 'console-feed'; +import { isEqual } from 'lodash'; +import { dispatchConsoleEvent } from '../actions/console'; +import { stopSketch, expandConsole } from '../actions/console'; + +export default function useHandleMessageEvent() { + const dispatch = useDispatch(); + + const handleMessageEvent = (data) => { + const { source, messages } = data; + if (source === 'sketch' && Array.isArray(messages)) { + const decodedMessages = messages.map(message => Decode(message.log)); + decodedMessages.every((message, index, arr) => { + const { data: args } = message; + let hasInfiniteLoop = false; + Object.keys(args).forEach((key) => { + if (typeof args[key] === 'string' && args[key].includes('Exiting potential infinite loop')) { + dispatch(stopSketch()); + dispatch(expandConsole()); + hasInfiniteLoop = true; + } + }); + if (hasInfiniteLoop) { + return false; + } + if (index === arr.length - 1) { + Object.assign(message, { times: 1 }); + return false; + } + // this should be done in the reducer probs + const cur = Object.assign(message, { times: 1 }); + const nextIndex = index + 1; + while (isEqual(cur.data, arr[nextIndex].data) && cur.method === arr[nextIndex].method) { + cur.times += 1; + arr.splice(nextIndex, 1); + if (nextIndex === arr.length) { + return false; + } + } + return true; + }); + dispatch(dispatchConsoleEvent(decodedMessages)); + } + }; + return handleMessageEvent; +} diff --git a/client/modules/IDE/pages/MobileIDEView.jsx b/client/modules/IDE/pages/MobileIDEView.jsx index 0d58887e04..963b4fbafa 100644 --- a/client/modules/IDE/pages/MobileIDEView.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -37,7 +37,7 @@ import Dropdown from '../../../components/Dropdown'; import { getIsUserOwner } from '../selectors/users'; -import { useEffectWithComparison, useEventListener } from '../../../utils/custom-hooks'; +import { useEffectWithComparison, useEventListener } from '../hooks/custom-hooks'; import * as device from '../../../utils/device'; diff --git a/client/styles/abstracts/_variables.scss b/client/styles/abstracts/_variables.scss index d0f62b27b4..d356c68168 100644 --- a/client/styles/abstracts/_variables.scss +++ b/client/styles/abstracts/_variables.scss @@ -64,7 +64,11 @@ $themes: ( icon-toast-hover-color: $lightest, shadow-color: rgba(0, 0, 0, 0.16), console-background-color: $light, - console-color: $lightest, + console-input-background-color: $lightest, + console-color: $darker, + console-logged-times-color: $lightest, + console-arrow-color: $middle-gray, + console-active-arrow-color: #0071AD, console-header-background-color: $medium-light, console-header-color: $darker, console-info-background-color: #5276B7, @@ -140,7 +144,11 @@ $themes: ( icon-toast-hover-color: $lightest, shadow-color: rgba(0, 0, 0, 0.16), console-background-color: $dark, + console-input-background-color: $darker, console-color: $lightest, + console-logged-times-color: $dark, + console-arrow-color: $medium-light, + console-active-arrow-color: #097BB3, console-header-background-color: $medium-dark, console-header-color: $lightest, console-info-background-color: #5276B7, @@ -214,7 +222,11 @@ $themes: ( icon-toast-hover-color: $yellow, shadow-color: rgba(0, 0, 0, 0.16), console-background-color: $dark, - console-color: $black, + console-input-background-color: $darker, + console-color: $lightest, + console-logged-times-color: $darker, + console-arrow-color: $lightest, + console-active-arrow-color: $dodgerblue, console-header-background-color: $medium-dark, console-header-color: $lightest, console-info-background-color: $lightsteelblue, diff --git a/client/styles/components/_console-input.scss b/client/styles/components/_console-input.scss new file mode 100644 index 0000000000..84db1f3387 --- /dev/null +++ b/client/styles/components/_console-input.scss @@ -0,0 +1,52 @@ +.console__input { + width: 100%; + display: flex; + align-items: start; + @include themify() { + background-color: getThemifyVariable('console-input-background-color'); + } +} + +.console__input .console-active__arrow { + width: auto; + height: 38%; + & path { + @include themify() { + fill: getThemifyVariable('console-active-arrow-color'); + opacity: 1; + } + } +} + +.console-active__arrow-container { + height: 100%; + display: flex; + align-items: center; + justify-content: center; + margin-left: #{10 / $base-font-size}rem; +} + +.console__editor { + margin-left: #{15 / $base-font-size}rem; + flex: 1; + & .CodeMirror { + height: auto; + } + & .CodeMirror-lines { + padding-top: #{2 / $base-font-size}rem; + } +} + +.console__editor .CodeMirror { + border: none; + font-family: Inconsolata,monospace; + @include themify() { + background-color: getThemifyVariable('console-input-background-color'); + } + + .CodeMirror-line { + @include themify() { + color: getThemifyVariable('console-color'); + } + } +} \ No newline at end of file diff --git a/client/styles/components/_console.scss b/client/styles/components/_console.scss index 29e26f62c5..e3854a6764 100644 --- a/client/styles/components/_console.scss +++ b/client/styles/components/_console.scss @@ -3,12 +3,11 @@ background: getThemifyVariable('console-background-color'); border-color: getThemifyVariable('ide-border-color'); } - border-left: 1px solid; - border-right: 1px solid; + border-left: #{1 / $base-font-size}rem solid; + border-right: #{1 / $base-font-size}rem solid; width: 100%; height: 100%; z-index: 1000; - overflow: hidden; display: flex; flex-direction: column; @@ -16,14 +15,6 @@ position:relative; text-align:left; } - - .preview-console__message { - @include themify() { - color: getThemifyVariable('console-color'); - } - flex: 1 0 auto; - position: relative; - } } .preview-console__header { @@ -49,8 +40,9 @@ .preview-console__messages { display: flex; + flex: 1; flex-direction: column; - overflow-y: auto; + overflow-y: scroll; } .preview-console__collapse { @@ -109,6 +101,7 @@ font-weight: bold; margin: #{2 / $base-font-size}rem 0 0 #{8 / $base-font-size}rem; + border-radius: #{10 / $base-font-size}rem; padding: #{1 / $base-font-size}rem #{4 / $base-font-size}rem; z-index: 100; left: 0; @@ -135,3 +128,16 @@ } } } + +.preview-console__body { + display: flex; + flex-direction: column; + height: calc(100% - #{30 / $base-font-size}rem); + + .preview-console__message { + position: relative; + @include themify() { + color: getThemifyVariable('console-logged-times-color'); + } + } +} diff --git a/client/styles/components/_p5-contrast-codemirror-theme.scss b/client/styles/components/_p5-contrast-codemirror-theme.scss index 0ec609b834..b9f837cc15 100644 --- a/client/styles/components/_p5-contrast-codemirror-theme.scss +++ b/client/styles/components/_p5-contrast-codemirror-theme.scss @@ -131,3 +131,7 @@ $p5-contrast-activeline: #999999; background-color: white; color: #333; } + +.cm-s-p5-contrast .CodeMirror-cursor { + border-left: 1px solid $p5-contrast-white; +} diff --git a/client/styles/components/_p5-dark-codemirror-theme.scss b/client/styles/components/_p5-dark-codemirror-theme.scss index 9e16951432..51879fdfd9 100644 --- a/client/styles/components/_p5-dark-codemirror-theme.scss +++ b/client/styles/components/_p5-dark-codemirror-theme.scss @@ -138,3 +138,7 @@ $p5-dark-error: #df3a3d; background-color: white; color: #333; } + +.cm-s-p5-dark .CodeMirror-cursor { + border-left: 1px solid $p5-dark-white; +} diff --git a/client/styles/components/_p5-light-codemirror-theme.scss b/client/styles/components/_p5-light-codemirror-theme.scss index 485aa7beef..28345a34e0 100644 --- a/client/styles/components/_p5-light-codemirror-theme.scss +++ b/client/styles/components/_p5-light-codemirror-theme.scss @@ -131,3 +131,7 @@ $p5-light-activeline: rgb(207, 207, 207); background-color: #333; color: white; } + +.cm-s-p5-light .CodeMirror-cursor { + border-left: 1px solid $p5-light-black; +} diff --git a/client/styles/main.scss b/client/styles/main.scss index 8ba66af71e..b9e4fa53c4 100644 --- a/client/styles/main.scss +++ b/client/styles/main.scss @@ -44,6 +44,7 @@ @import 'components/keyboard-shortcuts'; @import 'components/copyable-input'; @import 'components/feedback'; +@import 'components/console-input'; @import 'components/loader'; @import 'components/uploader'; @import 'components/tabs'; diff --git a/client/utils/dispatcher.js b/client/utils/dispatcher.js new file mode 100644 index 0000000000..6745840999 --- /dev/null +++ b/client/utils/dispatcher.js @@ -0,0 +1,48 @@ +// Inspired by +// https://github.com/codesandbox/codesandbox-client/blob/master/packages/codesandbox-api/src/dispatcher/index.ts + +let frame = null; +let listener = null; +const { origin } = window; + +export function registerFrame(newFrame) { + frame = newFrame; +} + +function notifyListener(message) { + if (listener) listener(message); +} + +function notifyFrame(message) { + const rawMessage = JSON.parse(JSON.stringify(message)); + if (frame && frame.postMessage) { + frame.postMessage(rawMessage, origin); + } +} + +export function dispatch(message) { + if (!message) return; + + notifyListener(message); + notifyFrame(message); +} + +/** + * Call callback to remove listener + */ +export function listen(callback) { + listener = callback; + return () => { + listener = null; + }; +} + +function eventListener(e) { + const { data } = e; + + if (data && e.origin === origin) { + notifyListener(data); + } +} + +window.addEventListener('message', eventListener); diff --git a/client/utils/evaluateExpression.js b/client/utils/evaluateExpression.js new file mode 100644 index 0000000000..570adf6fa2 --- /dev/null +++ b/client/utils/evaluateExpression.js @@ -0,0 +1,28 @@ +function __makeEvaluateExpression(evalInClosure) { + return expr => evalInClosure(` + ${expr}`); +} + +function evaluateExpression() { + return __makeEvaluateExpression((expr) => { + let newExpr = expr; + let result = null; + let error = false; + try { + try { + const wrapped = `(${expr})`; + const validate = new Function(wrapped); // eslint-disable-line + newExpr = wrapped; // eslint-disable-line + } catch (e) { + // We shouldn't wrap the expression + } + result = (0, eval)(newExpr); // eslint-disable-line + } catch (e) { + result = `${e.name}: ${e.message}`; + error = true; + } + return { result, error }; + }); +} + +export default evaluateExpression(); diff --git a/client/utils/previewEntry.js b/client/utils/previewEntry.js index 59858938b5..7db7fb478d 100644 --- a/client/utils/previewEntry.js +++ b/client/utils/previewEntry.js @@ -1,5 +1,6 @@ import loopProtect from 'loop-protect'; -import { Hook } from 'console-feed'; +import { Hook, Decode, Encode } from 'console-feed'; +import evaluateExpression from './evaluateExpression'; window.loopProtect = loopProtect; @@ -7,13 +8,36 @@ const consoleBuffer = []; const LOGWAIT = 500; Hook(window.console, (log) => { consoleBuffer.push({ - log, - source: 'sketch' + log }); }); setInterval(() => { if (consoleBuffer.length > 0) { - window.parent.postMessage(consoleBuffer, '*'); + const message = { + messages: consoleBuffer, + source: 'sketch' + }; + window.parent.postMessage(message, window.origin); consoleBuffer.length = 0; } }, LOGWAIT); + +function handleMessageEvent(e) { + if (window.origin !== e.origin) return; + const { data } = e; + const { source, messages } = data; + if (source === 'console' && Array.isArray(messages)) { + const decodedMessages = messages.map(message => Decode(message.log)); + decodedMessages.forEach((message) => { + const { data: args } = message; + const { result, error } = evaluateExpression(args); + const resultMessages = [{ log: Encode({ method: error ? 'error' : 'result', data: [result] }) }]; + window.parent.postMessage({ + messages: resultMessages, + source: 'sketch' + }, window.origin); + }); + } +} + +window.addEventListener('message', handleMessageEvent);