Skip to content
2 changes: 2 additions & 0 deletions client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,7 @@ export const CREATE_FILE = 'CREATE_FILE';
export const EXPAND_SIDEBAR = 'EXPAND_SIDEBAR';
export const COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR';

export const CONSOLE_EVENT = 'CONSOLE_EVENT';

// eventually, handle errors more specifically and better
export const ERROR = 'ERROR';
7 changes: 7 additions & 0 deletions client/modules/IDE/actions/ide.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export function setSelectedFile(fileId) {
};
}

export function dispatchConsoleEvent(...args) {
return {
type: ActionTypes.CONSOLE_EVENT,
event: args[0].data
};
}

export function newFile() {
return {
type: ActionTypes.SHOW_MODAL
Expand Down
57 changes: 57 additions & 0 deletions client/modules/IDE/components/Console.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { PropTypes } from 'react';

/**
* How many console messages to store
* @type {Number}
*/
const consoleMax = 5;

class Console extends React.Component {

constructor(props) {
super(props);

/**
* An array of React Elements that include previous console messages
* @type {Array}
*/
this.children = [];
}

componentWillReceiveProps(nextProps) {
if (nextProps.isPlaying && !this.props.isPlaying) {
this.children = [];
} else if (nextProps.consoleEvent !== this.props.consoleEvent) {
const args = nextProps.consoleEvent.arguments;
const method = nextProps.consoleEvent.method;
const nextChild = (
<div key={this.children.length} className={method}>
{Object.keys(args).map((key) => <span key={`${this.children.length}-${key}`}>{args[key]}</span>)}
</div>
);
this.children.push(nextChild);
}
}

shouldComponentUpdate(nextProps) {
return (nextProps.consoleEvent !== this.props.consoleEvent) || (nextProps.isPlaying && !this.props.isPlaying);
}

render() {
const childrenToDisplay = this.children.slice(-consoleMax);

return (
<div ref="console" className="preview-console">
{childrenToDisplay}
</div>
);
}

}

Console.propTypes = {
consoleEvent: PropTypes.object,
isPlaying: PropTypes.bool.isRequired
};

export default Console;
72 changes: 63 additions & 9 deletions client/modules/IDE/components/PreviewFrame.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,68 @@ import ReactDOM from 'react-dom';
import escapeStringRegexp from 'escape-string-regexp';
import srcDoc from 'srcdoc-polyfill';

const hijackConsoleScript = `<script>
document.addEventListener('DOMContentLoaded', function() {
var iframeWindow = window;
var originalConsole = iframeWindow.console;
iframeWindow.console = {};

var methods = [
'debug', 'clear', 'error', 'info', 'log', 'warn'
];

methods.forEach( function(method) {
iframeWindow.console[method] = function() {
originalConsole[method].apply(originalConsole, arguments);

var args = Array.from(arguments);
args = args.map(function(i) {
// catch objects
return (typeof i === 'string') ? i : JSON.stringify(i);
});

// post message to parent window
window.parent.postMessage({
method: method,
arguments: args,
source: 'sketch'
}, '*');
};
});

// catch reference errors, via http://stackoverflow.com/a/12747364/2994108
window.onerror = function (msg, url, lineNo, columnNo, error) {
var string = msg.toLowerCase();
var substring = "script error";
var data = {};

if (string.indexOf(substring) > -1){
data = 'Script Error: See Browser Console for Detail';
} else {
data = msg + ' Line: ' + lineNo + 'column: ' + columnNo;
}
window.parent.postMessage({
method: 'error',
arguments: data,
source: 'sketch'
}, '*');
return false;
};
});
</script>`;

class PreviewFrame extends React.Component {

componentDidMount() {
if (this.props.isPlaying) {
this.renderFrameContents();
}

window.addEventListener('message', (msg) => {
if (msg.data.source === 'sketch') {
this.props.dispatchConsoleEvent(msg);
}
});
}

componentDidUpdate(prevProps) {
Expand Down Expand Up @@ -51,22 +107,18 @@ class PreviewFrame extends React.Component {
// htmlHeadContents = htmlHeadContents.slice(1, htmlHeadContents.length - 2);
// htmlHeadContents += '<link rel="stylesheet" type="text/css" href="/preview-styles.css" />\n';
// htmlFile = htmlFile.replace(/(?:<head.*?>)([\s\S]*?)(?:<\/head>)/gmi, `<head>\n${htmlHeadContents}\n</head>`);
htmlFile += hijackConsoleScript;

return htmlFile;
}

renderSketch() {
const doc = ReactDOM.findDOMNode(this);
if (this.props.isPlaying) {
// TODO add polyfill for this
// doc.srcdoc = this.injectLocalFiles();
srcDoc.set(doc, this.injectLocalFiles());
} else {
// doc.srcdoc = '';
srcDoc.set(doc, '');
doc.contentWindow.document.open();
doc.contentWindow.document.write('');
doc.contentWindow.document.close();
doc.srcdoc = '';
srcDoc.set(doc, ' ');
}
}

Expand All @@ -86,7 +138,7 @@ class PreviewFrame extends React.Component {
frameBorder="0"
title="sketch output"
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-modals allow-forms"
></iframe>
/>
);
}
}
Expand All @@ -99,7 +151,9 @@ PreviewFrame.propTypes = {
content: PropTypes.string.isRequired
}),
jsFiles: PropTypes.array.isRequired,
cssFiles: PropTypes.array.isRequired
cssFiles: PropTypes.array.isRequired,
dispatchConsoleEvent: PropTypes.func.isRequired,
children: PropTypes.element
};

export default PreviewFrame;
9 changes: 9 additions & 0 deletions client/modules/IDE/pages/IDEView.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Toolbar from '../components/Toolbar';
import Preferences from '../components/Preferences';
import NewFileModal from '../components/NewFileModal';
import Nav from '../../../components/Nav';
import Console from '../components/Console';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as FileActions from '../actions/files';
Expand Down Expand Up @@ -85,12 +86,18 @@ class IDEView extends React.Component {
<link type="text/css" rel="stylesheet" href="/preview-styles.css" />
}
isPlaying={this.props.ide.isPlaying}
dispatchConsoleEvent={this.props.dispatchConsoleEvent}
/>
<Console
consoleEvent={this.props.ide.consoleEvent}
isPlaying={this.props.ide.isPlaying}
/>
<NewFileModal
isVisible={this.props.ide.modalIsVisible}
closeModal={this.props.closeNewFileModal}
/>
</div>

);
}
}
Expand All @@ -105,6 +112,7 @@ IDEView.propTypes = {
saveProject: PropTypes.func.isRequired,
ide: PropTypes.shape({
isPlaying: PropTypes.bool.isRequired,
consoleEvent: PropTypes.object,
modalIsVisible: PropTypes.bool.isRequired,
sidebarIsExpanded: PropTypes.bool.isRequired
}).isRequired,
Expand Down Expand Up @@ -143,6 +151,7 @@ IDEView.propTypes = {
htmlFile: PropTypes.object.isRequired,
jsFiles: PropTypes.array.isRequired,
cssFiles: PropTypes.array.isRequired,
dispatchConsoleEvent: PropTypes.func.isRequired,
newFile: PropTypes.func.isRequired,
closeNewFileModal: PropTypes.func.isRequired,
expandSidebar: PropTypes.func.isRequired,
Expand Down
6 changes: 6 additions & 0 deletions client/modules/IDE/reducers/ide.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import * as ActionTypes from '../../../constants';
const initialState = {
isPlaying: false,
selectedFile: '1',
consoleEvent: {
method: undefined,
arguments: []
},
modalIsVisible: false,
sidebarIsExpanded: true
};
Expand All @@ -19,6 +23,8 @@ const ide = (state = initialState, action) => {
case ActionTypes.SET_PROJECT:
case ActionTypes.NEW_PROJECT:
return Object.assign({}, state, { selectedFile: action.selectedFile });
case ActionTypes.CONSOLE_EVENT:
return Object.assign({}, state, { consoleEvent: action.event });
case ActionTypes.SHOW_MODAL:
return Object.assign({}, state, { modalIsVisible: true });
case ActionTypes.HIDE_MODAL:
Expand Down
27 changes: 27 additions & 0 deletions client/styles/components/_console.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.preview-console {
position: fixed;
width:100%;
height:60px;
right:0px;
bottom: 0px;
background:grey;
z-index:1000;

& > {
position:relative;
text-align:left;
}

// assign styles to different types of console messages
.log {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for this project I'm using BEM naming convention, so these class names should be .preview-console__log, .preview-console__warn, and .preview-console__error. Let's also make these colors the same as the linting error colors, #ffbe05 and #ff5f52. all of the other styling is fine for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good! I'll fix that

color: black;
}

.error {
color: red;
}

.warn {
color: yellow;
}
}
1 change: 1 addition & 0 deletions client/styles/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
@import 'components/sketch-list';
@import 'components/sidebar';
@import 'components/modal';
@import 'components/console';

@import 'layout/ide';
@import 'layout/sketch-list';
2 changes: 1 addition & 1 deletion server/controllers/file.controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Project from '../models/Project'
import Project from '../models/project'

// Bug -> timestamps don't get created, but it seems like this will
// be fixed in mongoose soon
Expand Down