From 43052cb675fa0bc500467f5c7c6643f3636493ae Mon Sep 17 00:00:00 2001 From: Yining Shi Date: Mon, 12 Sep 2016 01:31:30 -0400 Subject: [PATCH 01/13] detect infinite loop detect infinite loop detect infinite loop --- client/constants.js | 2 + client/modules/IDE/actions/ide.js | 11 +++ client/modules/IDE/components/Editor.js | 96 ++++++++++++++++--- client/modules/IDE/components/PreviewFrame.js | 12 ++- client/modules/IDE/pages/IDEView.js | 13 ++- client/modules/IDE/reducers/ide.js | 7 +- client/styles/components/_editor.scss | 10 +- package.json | 1 + 8 files changed, 136 insertions(+), 16 deletions(-) diff --git a/client/constants.js b/client/constants.js index c2e6c5be..2c41a15f 100644 --- a/client/constants.js +++ b/client/constants.js @@ -82,5 +82,7 @@ export const SHOW_TOAST = 'SHOW_TOAST'; export const HIDE_TOAST = 'HIDE_TOAST'; export const SET_TOAST_TEXT = 'SET_TOAST_TEXT'; +export const DETECT_INFINITE_LOOPS = 'DETECT_INFINITE_LOOPS'; +export const RESET_INFINITE_LOOPS = 'RESET_INFINITE_LOOPS'; // eventually, handle errors more specifically and better export const ERROR = 'ERROR'; diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index d56ae836..8b1d6034 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -163,3 +163,14 @@ export function closeKeyboardShortcutModal() { }; } +export function detectInfiniteLoops() { + return { + type: ActionTypes.DETECT_INFINITE_LOOPS + }; +} + +export function resetInfiniteLoops() { + return { + type: ActionTypes.RESET_INFINITE_LOOPS + }; +} diff --git a/client/modules/IDE/components/Editor.js b/client/modules/IDE/components/Editor.js index 43379a4d..666a0b60 100644 --- a/client/modules/IDE/components/Editor.js +++ b/client/modules/IDE/components/Editor.js @@ -15,6 +15,7 @@ import 'codemirror/addon/lint/html-lint'; import 'codemirror/addon/comment/comment'; import 'codemirror/keymap/sublime'; import 'codemirror/addon/search/jump-to-line'; + import { JSHINT } from 'jshint'; window.JSHINT = JSHINT; import { CSSLint } from 'csslint'; @@ -27,15 +28,16 @@ const downArrowUrl = require('../../../images/down-arrow.svg'); import classNames from 'classnames'; import { debounce } from 'throttle-debounce'; +import loopProtect from 'loop-protect'; class Editor extends React.Component { constructor(props) { super(props); this.tidyCode = this.tidyCode.bind(this); } - componentDidMount() { this.beep = new Audio(beepUrl); + this.widgets = []; this._cm = CodeMirror(this.refs.container, { // eslint-disable-line theme: 'p5-widget', value: this.props.file.content, @@ -47,21 +49,28 @@ class Editor extends React.Component { gutters: ['CodeMirror-lint-markers'], keyMap: 'sublime', lint: { - onUpdateLinting: debounce(2000, (annotations) => { - this.props.clearLintMessage(); - annotations.forEach((x) => { - if (x.from.line > -1) { - this.props.updateLintMessage(x.severity, (x.from.line + 1), x.message); + onUpdateLinting: () => { + debounce(2000, (annotations) => { + this.props.clearLintMessage(); + annotations.forEach((x) => { + if (x.from.line > -1) { + this.props.updateLintMessage(x.severity, (x.from.line + 1), x.message); + } + }); + if (this.props.lintMessages.length > 0 && this.props.lintWarning) { + this.beep.play(); } }); - if (this.props.lintMessages.length > 0 && this.props.lintWarning) { - this.beep.play(); - } - }) + } } }); this._cm.on('change', debounce(200, () => { this.props.updateFileContent(this.props.file.name, this._cm.getValue()); + this.checkForInfiniteLoop(debounce(200, (infiniteLoop, prevs) => { + if (!infiniteLoop && prevs) { + this.props.startSketch(); + } + })); })); this._cm.on('keyup', () => { const temp = `line ${parseInt((this._cm.getCursor().line) + 1, 10)}`; @@ -128,6 +137,65 @@ class Editor extends React.Component { } } + checkForInfiniteLoop(callback) { + const prevIsplaying = this.props.isPlaying; + let infiniteLoop = false; + this.props.stopSketch(); + this.props.resetInfiniteLoops(); + + for (let i = 0; i < this.widgets.length; ++i) { + this._cm.removeLineWidget(this.widgets[i]); + } + this.widgets.length = 0; + const OriginalIframe = document.getElementById('OriginalIframe'); + if (OriginalIframe !== null) { + document.body.removeChild(OriginalIframe); + } + + loopProtect.alias = 'protect'; + + loopProtect.hit = (line) => { + this.props.detectInfiniteLoops(); + infiniteLoop = true; + callback(infiniteLoop, prevIsplaying); + const msg = document.createElement('div'); + const loopError = `line ${line}: This loop is taking too long to run.`; + msg.appendChild(document.createTextNode(loopError)); + msg.className = 'lint-error'; + this.widgets.push(this._cm.addLineWidget(line - 1, msg, { coverGutter: false, noHScroll: true })); + }; + + const processed = loopProtect(this.props.file.content); + + const iframe = document.createElement('iframe'); + iframe.id = 'OriginalIframe'; + iframe.style.display = 'none'; + + document.body.appendChild(iframe); + + const win = iframe.contentWindow; + const doc = win.document; + doc.open(); + + win.protect = loopProtect; + + doc.write(` + + + + + + + + + + `); + doc.close(); + callback(infiniteLoop, prevIsplaying); + } + _cm: CodeMirror.Editor render() { @@ -190,7 +258,13 @@ Editor.propTypes = { editorOptionsVisible: PropTypes.bool.isRequired, showEditorOptions: PropTypes.func.isRequired, closeEditorOptions: PropTypes.func.isRequired, - showKeyboardShortcutModal: PropTypes.func.isRequired + showKeyboardShortcutModal: PropTypes.func.isRequired, + infiniteLoop: PropTypes.bool.isRequired, + detectInfiniteLoops: PropTypes.func.isRequired, + resetInfiniteLoops: PropTypes.func.isRequired, + stopSketch: PropTypes.func.isRequired, + startSketch: PropTypes.func.isRequired, + isPlaying: PropTypes.bool.isRequired }; export default Editor; diff --git a/client/modules/IDE/components/PreviewFrame.js b/client/modules/IDE/components/PreviewFrame.js index 16545376..9fe1f6f1 100644 --- a/client/modules/IDE/components/PreviewFrame.js +++ b/client/modules/IDE/components/PreviewFrame.js @@ -205,7 +205,13 @@ class PreviewFrame extends React.Component { renderSketch() { const doc = ReactDOM.findDOMNode(this); - if (this.props.isPlaying) { + if (this.props.infiniteLoop) { + window.alert('There is an infinite loop in the code, please remove it before running the sketch.'); + this.props.resetInfiniteLoops(); + doc.srcdoc = ''; + srcDoc.set(doc, ' '); + } + if (this.props.isPlaying && !this.props.infiniteLoop) { srcDoc.set(doc, this.injectLocalFiles()); } else { doc.srcdoc = ''; @@ -250,7 +256,9 @@ PreviewFrame.propTypes = { cssFiles: PropTypes.array.isRequired, files: PropTypes.array.isRequired, dispatchConsoleEvent: PropTypes.func, - children: PropTypes.element + children: PropTypes.element, + infiniteLoop: PropTypes.bool.isRequired, + resetInfiniteLoops: PropTypes.func.isRequired }; export default PreviewFrame; diff --git a/client/modules/IDE/pages/IDEView.js b/client/modules/IDE/pages/IDEView.js index 5c1ffc1d..cfccecca 100644 --- a/client/modules/IDE/pages/IDEView.js +++ b/client/modules/IDE/pages/IDEView.js @@ -243,6 +243,12 @@ class IDEView extends React.Component { showEditorOptions={this.props.showEditorOptions} closeEditorOptions={this.props.closeEditorOptions} showKeyboardShortcutModal={this.props.showKeyboardShortcutModal} + infiniteLoop={this.props.ide.infiniteLoop} + detectInfiniteLoops={this.props.detectInfiniteLoops} + resetInfiniteLoops={this.props.resetInfiniteLoops} + stopSketch={this.props.stopSketch} + startSketch={this.props.startSketch} + isPlaying={this.props.ide.isPlaying} /> @@ -380,12 +388,15 @@ IDEView.propTypes = { newFolderModalVisible: PropTypes.bool.isRequired, shareModalVisible: PropTypes.bool.isRequired, editorOptionsVisible: PropTypes.bool.isRequired, - keyboardShortcutVisible: PropTypes.bool.isRequired + keyboardShortcutVisible: PropTypes.bool.isRequired, + infiniteLoop: PropTypes.bool.isRequired }).isRequired, startSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired, startTextOutput: PropTypes.func.isRequired, stopTextOutput: PropTypes.func.isRequired, + detectInfiniteLoops: PropTypes.func.isRequired, + resetInfiniteLoops: PropTypes.func.isRequired, project: PropTypes.shape({ id: PropTypes.string, name: PropTypes.string.isRequired, diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index 8d6d3a64..ca9c9950 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -15,7 +15,8 @@ const initialState = { newFolderModalVisible: false, shareModalVisible: false, editorOptionsVisible: false, - keyboardShortcutVisible: false + keyboardShortcutVisible: false, + infiniteLoop: false }; const ide = (state = initialState, action) => { @@ -70,6 +71,10 @@ const ide = (state = initialState, action) => { return Object.assign({}, state, { keyboardShortcutVisible: true }); case ActionTypes.CLOSE_KEYBOARD_SHORTCUT_MODAL: return Object.assign({}, state, { keyboardShortcutVisible: false }); + case ActionTypes.DETECT_INFINITE_LOOPS: + return Object.assign({}, state, { infiniteLoop: true }); + case ActionTypes.RESET_INFINITE_LOOPS: + return Object.assign({}, state, { infiniteLoop: false }); default: return state; } diff --git a/client/styles/components/_editor.scss b/client/styles/components/_editor.scss index a3aa9588..0b40661e 100644 --- a/client/styles/components/_editor.scss +++ b/client/styles/components/_editor.scss @@ -80,4 +80,12 @@ .editor--options & { display: block; } -} \ No newline at end of file +} + +.lint-error { + font-family: Inconsolata, monospace; + font-size: 100%; + background: #FFBEC1; + color: red; + padding: 2px 5px 3px; +} diff --git a/package.json b/package.json index 56e95a68..a01147f5 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "jshint": "^2.9.2", "jszip": "^3.0.0", "jszip-utils": "0.0.2", + "loop-protect": "git+https://git@github.com/sagar-sm/loop-protect.git", "moment": "^2.14.1", "mongoose": "^4.4.16", "node-uuid": "^1.4.7", From 318475fc03426ea94a38a3d35fb1889ceb6988fb Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Tue, 20 Sep 2016 18:34:20 -0400 Subject: [PATCH 02/13] fix merge conflicts, actually --- client/modules/IDE/reducers/ide.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/modules/IDE/reducers/ide.js b/client/modules/IDE/reducers/ide.js index f8c16aa8..840e3068 100644 --- a/client/modules/IDE/reducers/ide.js +++ b/client/modules/IDE/reducers/ide.js @@ -72,15 +72,12 @@ const ide = (state = initialState, action) => { return Object.assign({}, state, { keyboardShortcutVisible: true }); case ActionTypes.CLOSE_KEYBOARD_SHORTCUT_MODAL: return Object.assign({}, state, { keyboardShortcutVisible: false }); -<<<<<<< HEAD case ActionTypes.SET_UNSAVED_CHANGES: return Object.assign({}, state, { unsavedChanges: action.value }); -======= case ActionTypes.DETECT_INFINITE_LOOPS: return Object.assign({}, state, { infiniteLoop: true }); case ActionTypes.RESET_INFINITE_LOOPS: return Object.assign({}, state, { infiniteLoop: false }); ->>>>>>> 43052cb675fa0bc500467f5c7c6643f3636493ae default: return state; } From f017dd8a765174ab85806a8b62b7e15b2d57d2da Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Wed, 21 Sep 2016 23:39:50 -0400 Subject: [PATCH 03/13] clean up scss color variables, fix missing unthmeified variables --- client/styles/abstracts/_variables.scss | 71 ++-------------------- client/styles/base/_base.scss | 6 +- client/styles/components/_console.scss | 2 +- client/styles/components/_preferences.scss | 16 +++-- client/styles/components/_sidebar.scss | 5 +- client/styles/components/_toolbar.scss | 16 ++--- 6 files changed, 31 insertions(+), 85 deletions(-) diff --git a/client/styles/abstracts/_variables.scss b/client/styles/abstracts/_variables.scss index c9b225dc..f4fc23c3 100644 --- a/client/styles/abstracts/_variables.scss +++ b/client/styles/abstracts/_variables.scss @@ -28,9 +28,12 @@ $themes: ( shadow-color: rgba(0, 0, 0, 0.16), console-background-color: #eee, console-header-background-color: #d6d6d6, + console-header-color: #b1b1b1, ide-border-color: #f4f4f4, editor-gutter-color: #f7f7f7, file-selected-color: #f4f4f4, + input-text-color: #333, + input-border-color: #979797, ), dark: ( primary-text-color: $white, @@ -58,78 +61,14 @@ $themes: ( ide-border-color: #949494, editor-gutter-color: #363636, file-selected-color: #404040, + input-text-color: #333, + input-border-color: #979797, ) ); -$primary-text-color: #333; -$secondary-text-color: #6b6b6b; -$inactive-text-color: #b5b5b5; -$background-color: #fdfdfd; -$button-background-color: #f4f4f4; -$button-color: $black; -$button-border-color: #979797; -$toolbar-button-color: $p5js-pink; -$button-background-hover-color: $p5js-pink; -$button-background-active-color: #f10046; -$button-hover-color: $white; -$button-active-color: $white; -$modal-background-color: #f4f4f4; -$modal-button-background-color: #e6e6e6; -$modal-border-color: #B9D0E1; -$icon-color: #8b8b8b; -$icon-hover-color: #333; -$shadow-color: rgba(0, 0, 0, 0.16); -$console-background-color: #eee; -$console-header-background-color: #d6d6d6; -$ide-border-color: #f4f4f4; - - -// other variables i may or may not need later -$ide-border-color: #f4f4f4; -$editor-selected-line-color: #f3f3f3; -$input-border-color: #979797; - -$console-light-background-color: #eee; -$console-header-background-color: #d6d6d6; -$console-header-color: #b1b1b1; $console-warn-color: #ffbe05; $console-error-color: #ff5f52; $toast-background-color: #979797; $toast-text-color: $white; -//light and dark colors - -$light-primary-text-color: #333; -$light-secondary-text-color: #6b6b6b; -$light-inactive-text-color: #b5b5b5; -$light-background-color: #fdfdfd; - -$light-button-background-color: #f4f4f4; -$light-button-color: $black; -$light-button-border-color: #979797; -$light-toolbar-button-color: $p5js-pink; -$light-button-background-hover-color: $p5js-pink; -$light-button-background-active-color: #f10046; -$light-button-hover-color: $white; -$light-button-active-color: $white; -$light-modal-background-color: #f4f4f4; -$light-modal-button-background-color: #e6e6e6; -$light-modal-border-color: #B9D0E1; -$light-icon-color: #8b8b8b; -$light-icon-hover-color: $light-primary-text-color; -$light-shadow-color: rgba(0, 0, 0, 0.16); - -$dark-primary-text-color: $white; -$dark-secondary-text-color: #c2c2c2; -$dark-inactive-color: #7d7d7d; -$dark-background-color: #333; - -$dark-button-background-color: $white; -$dark-button-color: $black; -$dark-toolbar-button-color: $p5js-pink; -$dark-button-background-hover-color: $p5js-pink; -$dark-button-background-active-color: #f10046; -$dark-button-hover-color: $white; -$dark-button-active-color: $white; - diff --git a/client/styles/base/_base.scss b/client/styles/base/_base.scss index 80c885ab..d96bfbf8 100644 --- a/client/styles/base/_base.scss +++ b/client/styles/base/_base.scss @@ -36,11 +36,11 @@ input, button { input { padding: #{5 / $base-font-size}rem; - // border-radius: 2px; - border: 1px solid $input-border-color; + border: 1px solid; padding: #{10 / $base-font-size}rem; @include themify() { - color: $primary-text-color; + color: getThemifyVariable('input-text-color'); + border-color: getThemifyVariable('input-border-color'); } } diff --git a/client/styles/components/_console.scss b/client/styles/components/_console.scss index df17fd1d..0f382c77 100644 --- a/client/styles/components/_console.scss +++ b/client/styles/components/_console.scss @@ -39,8 +39,8 @@ .preview-console__header { @include themify() { background-color: getThemifyVariable('console-header-background-color'); + color: getThemifyVariable('console-header-color'); } - color: $console-header-color; padding: #{5 / $base-font-size}rem; display: flex; justify-content: space-between; diff --git a/client/styles/components/_preferences.scss b/client/styles/components/_preferences.scss index c539a757..408fd2fb 100644 --- a/client/styles/components/_preferences.scss +++ b/client/styles/components/_preferences.scss @@ -51,17 +51,19 @@ } .preference__subtitle { + @include themify() { + color: getThemifyVariable('inactive-text-color'); + } width: 100%; margin-bottom: #{10 / $base-font-size}rem; margin-top: 0; - color: $light-inactive-text-color; } .preference__value { @include themify() { border: 2px solid getThemifyVariable('button-border-color'); background-color: getThemifyVariable('button-background-color'); - color: $light-primary-text-color; + color: getThemifyVariable('input-text-color'); } text-align: center; border-radius: 0%; @@ -72,13 +74,15 @@ } .preference__label { + @include themify() { + color: getThemifyColor('inactive-text-color'); + &:hover { + color: getThemifyColor('inactive-text-color'); + } + } margin: 0; line-height: #{20 / $base-font-size}rem; - color: $light-inactive-text-color; font-size: #{9 / $base-font-size}rem; - &:hover { - color: $light-inactive-text-color; - } } .preference__vertical-list { diff --git a/client/styles/components/_sidebar.scss b/client/styles/components/_sidebar.scss index abd47b72..e9142ef0 100644 --- a/client/styles/components/_sidebar.scss +++ b/client/styles/components/_sidebar.scss @@ -29,7 +29,10 @@ } .sidebar__file-list { - border-top: 1px solid $ide-border-color; + @include themify() { + border-color: getThemifyVariable('ide-border-color') + } + border-top: 1px solid; .sidebar--contracted & { display: none; } diff --git a/client/styles/components/_toolbar.scss b/client/styles/components/_toolbar.scss index f66ef02a..cee5f6ad 100644 --- a/client/styles/components/_toolbar.scss +++ b/client/styles/components/_toolbar.scss @@ -49,8 +49,9 @@ .toolbar__project-name-container { @include themify() { - border-left: 2px dashed map-get($theme-map, 'inactive-text-color'); + border-color: getThemifyVariable('inactive-text-color'); } + border-left: 2px dashed; margin-left: #{10 / $base-font-size}rem; padding-left: #{10 / $base-font-size}rem; height: 70%; @@ -59,14 +60,13 @@ } .toolbar__project-name { - color: $light-inactive-text-color; + @include themify() { + color: getThemifyVariable('inactive-text-color'); + &:hover { + color: getThemifyVariable('primary-text-color'); + } + } cursor: pointer; - &:hover { - color: $light-primary-text-color; - } - &:focus { - color: $light-inactive-text-color; - } .toolbar__project-name-container--editing & { display: none; From 55b37866f62259cba725a9eb7639c0544c0067b5 Mon Sep 17 00:00:00 2001 From: Yining Shi Date: Sat, 24 Sep 2016 00:46:06 -0400 Subject: [PATCH 04/13] detect infinite loop, disable play button --- client/modules/IDE/components/Editor.js | 64 +++++++++---------- client/modules/IDE/components/PreviewFrame.js | 1 - client/modules/IDE/components/Toolbar.js | 6 +- client/modules/IDE/pages/IDEView.js | 1 + client/styles/components/_toolbar.scss | 12 ++++ 5 files changed, 49 insertions(+), 35 deletions(-) diff --git a/client/modules/IDE/components/Editor.js b/client/modules/IDE/components/Editor.js index 8e6078ff..a0afac56 100644 --- a/client/modules/IDE/components/Editor.js +++ b/client/modules/IDE/components/Editor.js @@ -65,14 +65,14 @@ class Editor extends React.Component { } }); - this._cm.on('change', debounce(200, () => { + this._cm.on('change', debounce(1000, () => { this.props.setUnsavedChanges(true); this.props.updateFileContent(this.props.file.name, this._cm.getValue()); - this.checkForInfiniteLoop(debounce(200, (infiniteLoop, prevs) => { + this.checkForInfiniteLoop((infiniteLoop, prevs) => { if (!infiniteLoop && prevs) { this.props.startSketch(); } - })); + }); })); this._cm.on('keyup', () => { @@ -147,15 +147,12 @@ class Editor extends React.Component { let prevLine; this.props.stopSketch(); this.props.resetInfiniteLoops(); + let iframe; for (let i = 0; i < this.widgets.length; ++i) { this._cm.removeLineWidget(this.widgets[i]); } this.widgets.length = 0; - const OriginalIframe = document.getElementById('OriginalIframe'); - if (OriginalIframe !== null) { - document.body.removeChild(OriginalIframe); - } loopProtect.alias = 'protect'; @@ -165,7 +162,7 @@ class Editor extends React.Component { infiniteLoop = true; callback(infiniteLoop, prevIsplaying); const msg = document.createElement('div'); - const loopError = `line ${line}: This loop is taking too long to run.`; + const loopError = `line ${line}: This loop is taking too long to run. This might be an infinite loop.`; msg.appendChild(document.createTextNode(loopError)); msg.className = 'lint-error'; this.widgets.push(this._cm.addLineWidget(line - 1, msg, { coverGutter: false, noHScroll: true })); @@ -175,32 +172,35 @@ class Editor extends React.Component { const processed = loopProtect(this.props.file.content); - const iframe = document.createElement('iframe'); - iframe.id = 'OriginalIframe'; - iframe.style.display = 'none'; + const iframeForLoop = document.getElementById('iframeForLoop'); + if (iframeForLoop === null) { + iframe = document.createElement('iframe'); + iframe.id = 'iframeForLoop'; + iframe.style.display = 'none'; + document.body.appendChild(iframe); + } else { + iframeForLoop.srcdoc = ''; + const win = iframeForLoop.contentWindow; + const doc = win.document; + doc.open(); - document.body.appendChild(iframe); + win.protect = loopProtect; - const win = iframe.contentWindow; - const doc = win.document; - doc.open(); - - win.protect = loopProtect; - - doc.write(` - - - - - - - - - - `); - doc.close(); + doc.write(` + + + + + + + + + `); + win.onerror = () => true; + doc.close(); + } callback(infiniteLoop, prevIsplaying, prevLine); } diff --git a/client/modules/IDE/components/PreviewFrame.js b/client/modules/IDE/components/PreviewFrame.js index 9fe1f6f1..6f858bab 100644 --- a/client/modules/IDE/components/PreviewFrame.js +++ b/client/modules/IDE/components/PreviewFrame.js @@ -206,7 +206,6 @@ class PreviewFrame extends React.Component { renderSketch() { const doc = ReactDOM.findDOMNode(this); if (this.props.infiniteLoop) { - window.alert('There is an infinite loop in the code, please remove it before running the sketch.'); this.props.resetInfiniteLoops(); doc.srcdoc = ''; srcDoc.set(doc, ' '); diff --git a/client/modules/IDE/components/Toolbar.js b/client/modules/IDE/components/Toolbar.js index 74e78b6a..0d9dee5b 100644 --- a/client/modules/IDE/components/Toolbar.js +++ b/client/modules/IDE/components/Toolbar.js @@ -55,10 +55,11 @@ class Toolbar extends React.Component { className="toolbar__play-sketch-button" onClick={() => { this.props.startTextOutput(); this.props.startSketch(); }} aria-label="play sketch" + disabled={this.props.infiniteLoop} > - -