diff --git a/client/images/console-debug-dark.svg b/client/images/console-debug-dark.svg new file mode 100644 index 00000000..58aebc9d --- /dev/null +++ b/client/images/console-debug-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/images/console-debug-light.svg b/client/images/console-debug-light.svg new file mode 100644 index 00000000..65a0b0a8 --- /dev/null +++ b/client/images/console-debug-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/images/console-error-dark.svg b/client/images/console-error-dark.svg new file mode 100644 index 00000000..56caaebc --- /dev/null +++ b/client/images/console-error-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/images/console-error-light.svg b/client/images/console-error-light.svg new file mode 100644 index 00000000..7ddff356 --- /dev/null +++ b/client/images/console-error-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/images/console-info-dark.svg b/client/images/console-info-dark.svg new file mode 100644 index 00000000..37e75260 --- /dev/null +++ b/client/images/console-info-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/images/console-info-light.svg b/client/images/console-info-light.svg new file mode 100644 index 00000000..f239a82f --- /dev/null +++ b/client/images/console-info-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/images/console-warn-dark.svg b/client/images/console-warn-dark.svg new file mode 100644 index 00000000..bcfcb475 --- /dev/null +++ b/client/images/console-warn-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/images/console-warn-light.svg b/client/images/console-warn-light.svg new file mode 100644 index 00000000..83a70db4 --- /dev/null +++ b/client/images/console-warn-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index fdb4e092..4f394f13 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -2,13 +2,63 @@ import PropTypes from 'prop-types'; import React from 'react'; import InlineSVG from 'react-inlinesvg'; import classNames from 'classnames'; +import { Console as ConsoleFeed } from 'console-feed'; +import { CONSOLE_FEED_WITHOUT_ICONS, CONSOLE_FEED_LIGHT_STYLES, CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_CONTRAST_STYLES } from '../../../styles/components/_console-feed.scss'; +import warnLightUrl from '../../../images/console-warn-light.svg'; +import warnDarkUrl from '../../../images/console-warn-dark.svg'; +import errorLightUrl from '../../../images/console-error-light.svg'; +import errorDarkUrl from '../../../images/console-error-dark.svg'; +import debugLightUrl from '../../../images/console-debug-light.svg'; +import debugDarkUrl from '../../../images/console-debug-dark.svg'; +import infoLightUrl from '../../../images/console-info-light.svg'; +import infoDarkUrl from '../../../images/console-info-dark.svg'; const upArrowUrl = require('../../../images/up-arrow.svg'); const downArrowUrl = require('../../../images/down-arrow.svg'); class Console extends React.Component { - componentDidUpdate() { + componentDidUpdate(prevProps) { this.consoleMessages.scrollTop = this.consoleMessages.scrollHeight; + if (this.props.theme !== prevProps.theme) { + this.props.clearConsole(); + this.props.dispatchConsoleEvent(this.props.consoleEvents); + } + } + + getConsoleFeedStyle(theme, times) { + const style = {}; + 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})` + }; + 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})` + }; + if (times > 1) { + Object.assign(style, CONSOLE_FEED_WITHOUT_ICONS); + } + switch (theme) { + case 'light': + return Object.assign(CONSOLE_FEED_LIGHT_STYLES, CONSOLE_FEED_LIGHT_ICONS, style); + case 'dark': + return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, style); + case 'contrast': + return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_DARK_ICONS, style); + default: + return ''; + } + } + + formatData(args) { + if (!Array.isArray(args)) { + return Array.of(args); + } + return args; } render() { @@ -39,17 +89,25 @@ class Console extends React.Component {
{ this.consoleMessages = element; }} className="preview-console__messages"> {this.props.consoleEvents.map((consoleEvent) => { - const { arguments: args, method } = consoleEvent; + const { arguments: args, method, times } = consoleEvent; + const { theme } = this.props; + Object.assign(consoleEvent, { data: this.formatData(args) }); if (Object.keys(args).length === 0) { return ( -
+
undefined
); } return ( -
- {Object.keys(args).map(key => {args[key]})} +
+ { times > 1 && +
{times}
+ } +
); })} @@ -67,7 +125,9 @@ Console.propTypes = { isExpanded: PropTypes.bool.isRequired, collapseConsole: PropTypes.func.isRequired, expandConsole: PropTypes.func.isRequired, - clearConsole: PropTypes.func.isRequired + clearConsole: PropTypes.func.isRequired, + dispatchConsoleEvent: PropTypes.func.isRequired, + theme: PropTypes.string.isRequired }; Console.defaultProps = { diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index b36a6332..74bd6ceb 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -116,8 +116,8 @@ class Editor extends React.Component { this.props.setUnsavedChanges(true); this.props.updateFileContent(this.props.file.name, this._cm.getValue()); if (this.props.autorefresh && this.props.isPlaying) { - this.props.startRefreshSketch(); this.props.clearConsole(); + this.props.startRefreshSketch(); } }, 400)); diff --git a/client/modules/IDE/components/PreviewFrame.jsx b/client/modules/IDE/components/PreviewFrame.jsx index ade57b97..8e4f1350 100644 --- a/client/modules/IDE/components/PreviewFrame.jsx +++ b/client/modules/IDE/components/PreviewFrame.jsx @@ -2,10 +2,9 @@ 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 loopProtectScript from 'loop-protect/dist/loop-protect.min'; import { JSHINT } from 'jshint'; import decomment from 'decomment'; import { getBlobUrl } from '../actions/files'; @@ -18,29 +17,17 @@ import { EXTERNAL_LINK_REGEX, NOT_EXTERNAL_LINK_REGEX } from '../../../../server/utils/fileUtils'; -import { hijackConsole, hijackConsoleErrorsScript, startTag, getAllScriptOffsets } +import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets } from '../../../utils/consoleUtils'; - class PreviewFrame extends React.Component { - componentDidMount() { - if (this.props.isPlaying) { - this.renderFrameContents(); - } + constructor(props) { + super(props); + this.handleConsoleEvent = this.handleConsoleEvent.bind(this); + } - window.addEventListener('message', (messageEvent) => { - console.log(messageEvent); - messageEvent.data.forEach((message) => { - const args = message.arguments; - Object.keys(args).forEach((key) => { - if (args[key].includes('Exiting potential infinite loop')) { - this.props.stopSketch(); - this.props.expandConsole(); - } - }); - }); - this.props.dispatchConsoleEvent(messageEvent.data); - }); + componentDidMount() { + window.addEventListener('message', this.handleConsoleEvent); } componentDidUpdate(prevProps) { @@ -86,12 +73,43 @@ class PreviewFrame extends React.Component { } componentWillUnmount() { + window.removeEventListener('message', this.handleConsoleEvent); ReactDOM.unmountComponentAtNode(this.iframeElement.contentDocument.body); } - clearPreview() { - const doc = this.iframeElement; - doc.srcDoc = ''; + handleConsoleEvent(messageEvent) { + if (Array.isArray(messageEvent.data)) { + messageEvent.data.every((message, index, arr) => { + const { arguments: 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.arguments, arr[nextIndex].arguments) && cur.method === arr[nextIndex].method) { + cur.times += 1; + arr.splice(nextIndex, 1); + if (nextIndex === arr.length) { + return false; + } + } + return true; + }); + + this.props.dispatchConsoleEvent(messageEvent.data); + } } addLoopProtect(sketchDoc) { @@ -121,9 +139,7 @@ class PreviewFrame extends React.Component { injectLocalFiles() { const htmlFile = this.props.htmlFile.content; let scriptOffs = []; - const resolvedFiles = this.resolveJSAndCSSLinks(this.props.files); - const parser = new DOMParser(); const sketchDoc = parser.parseFromString(htmlFile, 'text/html'); @@ -138,10 +154,6 @@ class PreviewFrame extends React.Component { this.resolveScripts(sketchDoc, resolvedFiles); this.resolveStyles(sketchDoc, resolvedFiles); - const scriptsToInject = [ - loopProtectScript, - hijackConsole - ]; const accessiblelib = sketchDoc.createElement('script'); accessiblelib.setAttribute( 'src', @@ -156,15 +168,13 @@ class PreviewFrame extends React.Component { const textSection = sketchDoc.createElement('section'); textSection.setAttribute('id', 'textOutput-content'); sketchDoc.getElementById('accessible-outputs').appendChild(textSection); - this.iframeElement.focus(); } if (this.props.gridOutput) { sketchDoc.body.appendChild(accessibleOutputs); sketchDoc.body.appendChild(accessiblelib); const gridSection = sketchDoc.createElement('section'); - gridSection.setAttribute('id', 'gridOutput-content'); + gridSection.setAttribute('id', 'tableOutput-content'); sketchDoc.getElementById('accessible-outputs').appendChild(gridSection); - this.iframeElement.focus(); } if (this.props.soundOutput) { sketchDoc.body.appendChild(accessibleOutputs); @@ -174,11 +184,9 @@ class PreviewFrame extends React.Component { sketchDoc.getElementById('accessible-outputs').appendChild(soundSection); } - scriptsToInject.forEach((scriptToInject) => { - const script = sketchDoc.createElement('script'); - script.text = scriptToInject; - sketchDoc.head.appendChild(script); - }); + const previewScripts = sketchDoc.createElement('script'); + previewScripts.src = '/previewScripts.js'; + sketchDoc.head.appendChild(previewScripts); const sketchDocString = `\n${sketchDoc.documentElement.outerHTML}`; scriptOffs = getAllScriptOffsets(sketchDocString); @@ -320,15 +328,6 @@ class PreviewFrame extends React.Component { } } - renderFrameContents() { - const doc = this.iframeElement.contentDocument; - if (doc.readyState === 'complete') { - this.renderSketch(); - } else { - setTimeout(this.renderFrameContents, 0); - } - } - render() { return (