p5.js-web-editor/client/modules/IDE/components/PreviewFrame.js

287 lines
8.5 KiB
JavaScript
Raw Normal View History

2016-06-27 21:47:48 +02:00
import React, { PropTypes } from 'react';
2016-06-24 00:29:55 +02:00
import ReactDOM from 'react-dom';
2016-07-11 21:22:29 +02:00
import escapeStringRegexp from 'escape-string-regexp';
import srcDoc from 'srcdoc-polyfill';
2016-06-24 00:29:55 +02:00
2016-08-28 15:52:57 +02:00
const startTag = '@fs-';
2016-08-28 15:52:57 +02:00
2016-08-27 19:54:20 +02:00
function getAllScriptOffsets(htmlFile) {
2016-08-28 15:52:57 +02:00
const offs = [];
let found = true;
let lastInd = 0;
let ind = 0;
let endFilenameInd = 0;
let filename = '';
let lineOffset = 0;
2016-08-27 19:54:20 +02:00
while (found) {
2016-08-28 15:52:57 +02:00
ind = htmlFile.indexOf(startTag, lastInd);
if (ind === -1) {
2016-08-27 19:54:20 +02:00
found = false;
} else {
2016-08-28 15:52:57 +02:00
endFilenameInd = htmlFile.indexOf('.js', ind + startTag.length + 3);
filename = htmlFile.substring(ind + startTag.length, endFilenameInd);
lineOffset = htmlFile.substring(0, ind).split('\n').length;
offs.push([lineOffset, filename]);
2016-08-27 19:54:20 +02:00
lastInd = ind + 1;
}
}
2016-08-27 19:54:20 +02:00
return offs;
}
function hijackConsoleLogsScript() {
const s = `<script>
var iframeWindow = window;
var originalConsole = iframeWindow.console;
iframeWindow.console = {};
var methods = [
'debug', 'clear', 'error', 'info', 'log', 'warn'
];
function throttle(fn, threshhold, scope) {
var last, deferTimer;
return function() {
var context = scope || this;
var now = +new Date,
args = arguments;
if (last && now < last + threshhold) {
// hold on to it
clearTimeout(deferTimer);
deferTimer = setTimeout(function() {
last = now;
fn.apply(context, args);
}, threshhold);
} else {
last = now;
fn.apply(context, args);
}
};
}
methods.forEach( function(method) {
iframeWindow.console[method] = throttle(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'
}, '*');
2016-10-02 01:04:40 +02:00
}, 100);
});
</script>`;
return s;
}
function hijackConsoleErrorsScript(offs) {
2016-08-28 15:52:57 +02:00
const s = `<script>
2016-08-27 19:54:20 +02:00
function getScriptOff(line) {
2016-08-28 15:52:57 +02:00
var offs = ${offs};
2016-08-27 19:54:20 +02:00
var l = 0;
var file = '';
for (var i=0; i<offs.length; i++) {
var n = offs[i][0];
if (n < line && n > l) {
l = n;
file = offs[i][1];
}
}
return [line - l, file];
}
// catch reference errors, via http://stackoverflow.com/a/12747364/2994108
window.onerror = function (msg, url, lineNumber, 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 {
var fileInfo = getScriptOff(lineNumber);
data = msg + ' (' + fileInfo[1] + ': line ' + fileInfo[0] + ')';
}
window.parent.postMessage({
method: 'error',
arguments: data,
source: 'sketch'
}, '*');
return false;
};
</script>`;
2016-08-28 15:52:57 +02:00
return s;
}
2016-06-24 00:29:55 +02:00
class PreviewFrame extends React.Component {
componentDidMount() {
if (this.props.isPlaying) {
this.renderFrameContents();
}
2016-08-18 00:13:17 +02:00
if (this.props.dispatchConsoleEvent) {
window.addEventListener('message', (msg) => {
if (msg.data.source === 'sketch') {
this.props.dispatchConsoleEvent(msg);
}
});
}
2016-06-24 00:29:55 +02:00
}
2016-06-27 21:47:48 +02:00
componentDidUpdate(prevProps) {
// if sketch starts or stops playing, want to rerender
2016-06-27 21:47:48 +02:00
if (this.props.isPlaying !== prevProps.isPlaying) {
2016-07-11 21:22:29 +02:00
this.renderSketch();
2016-09-28 21:20:54 +02:00
return;
2016-06-27 21:47:48 +02:00
}
2016-09-29 00:05:14 +02:00
// if the user explicitly clicks on the play button
if (this.props.isPlaying && this.props.previewIsRefreshing) {
2016-06-27 21:47:48 +02:00
this.renderSketch();
2016-09-29 00:05:14 +02:00
return;
2016-06-27 21:47:48 +02:00
}
2016-08-18 00:13:17 +02:00
// small bug - if autorefresh is on, and the usr changes files
// in the sketch, preview will reload
2016-06-27 21:47:48 +02:00
}
componentWillUnmount() {
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).contentDocument.body);
}
2016-06-24 00:29:55 +02:00
clearPreview() {
2016-07-11 21:22:29 +02:00
const doc = ReactDOM.findDOMNode(this);
doc.srcDoc = '';
}
injectLocalFiles() {
let htmlFile = this.props.htmlFile.content;
2016-08-28 15:52:57 +02:00
let scriptOffs = [];
2016-07-20 03:36:21 +02:00
// have to build the array manually because the spread operator is only
// one level down...
htmlFile = hijackConsoleLogsScript() + htmlFile;
2016-09-07 04:37:29 +02:00
const mediaFiles = this.props.files.filter(file => file.url);
2016-07-20 03:36:21 +02:00
const jsFiles = [];
this.props.jsFiles.forEach(jsFile => {
const newJSFile = { ...jsFile };
2016-07-21 06:05:47 +02:00
let jsFileStrings = newJSFile.content.match(/(['"])((\\\1|.)*?)\1/gm);
2016-08-28 15:52:57 +02:00
const jsFileRegex = /^('|")(?!(http:\/\/|https:\/\/)).*\.(png|jpg|jpeg|gif|bmp|mp3|wav|aiff|ogg|json)('|")$/i;
2016-07-21 06:05:47 +02:00
jsFileStrings = jsFileStrings || [];
2016-07-20 01:36:50 +02:00
jsFileStrings.forEach(jsFileString => {
2016-08-28 15:52:57 +02:00
if (jsFileString.match(jsFileRegex)) {
2016-07-20 03:36:21 +02:00
const filePath = jsFileString.substr(1, jsFileString.length - 2);
const filePathArray = filePath.split('/');
const fileName = filePathArray[filePathArray.length - 1];
mediaFiles.forEach(file => {
2016-07-20 01:36:50 +02:00
if (file.name === fileName) {
2016-07-20 03:36:21 +02:00
newJSFile.content = newJSFile.content.replace(filePath, file.blobURL); // eslint-disable-line
2016-07-20 01:36:50 +02:00
}
});
}
});
2016-07-20 03:36:21 +02:00
jsFiles.push(newJSFile);
2016-07-20 01:36:50 +02:00
});
2016-07-11 21:22:29 +02:00
2016-07-20 01:36:50 +02:00
jsFiles.forEach(jsFile => {
2016-07-11 21:22:29 +02:00
const fileName = escapeStringRegexp(jsFile.name);
const fileRegex = new RegExp(`<script.*?src=('|")((\.\/)|\/)?${fileName}('|").*?>([\s\S]*?)<\/script>`, 'gmi');
2016-08-28 15:52:57 +02:00
const replacementString = `<script data-tag="${startTag}${jsFile.name}">\n${jsFile.content}\n</script>`;
htmlFile = htmlFile.replace(fileRegex, replacementString);
2016-07-11 21:22:29 +02:00
});
2016-07-12 03:54:08 +02:00
this.props.cssFiles.forEach(cssFile => {
const fileName = escapeStringRegexp(cssFile.name);
const fileRegex = new RegExp(`<link.*?href=('|")((\.\/)|\/)?${fileName}('|").*?>`, 'gmi');
htmlFile = htmlFile.replace(fileRegex, `<style>\n${cssFile.content}\n</style>`);
});
2016-08-15 18:12:25 +02:00
if (this.props.textOutput || this.props.isTextOutputPlaying) {
2016-08-12 21:50:33 +02:00
const htmlHead = htmlFile.match(/(?:<head.*?>)([\s\S]*?)(?:<\/head>)/gmi);
const headRegex = new RegExp('head', 'i');
let htmlHeadContents = htmlHead[0].split(headRegex)[1];
htmlHeadContents = htmlHeadContents.slice(1, htmlHeadContents.length - 2);
2016-08-16 00:06:09 +02:00
htmlHeadContents += '<script src="/loadData.js"></script>\n';
2016-08-12 21:50:33 +02:00
htmlHeadContents += '<script src="/interceptor-functions.js"></script>\n';
htmlHeadContents += '<script src="/intercept-p5.js"></script>\n';
2016-08-19 18:44:44 +02:00
htmlHeadContents += '<script type="text/javascript" src="/ntc.min.js"></script>';
2016-08-12 21:50:33 +02:00
htmlFile = htmlFile.replace(/(?:<head.*?>)([\s\S]*?)(?:<\/head>)/gmi, `<head>\n${htmlHeadContents}\n</head>`);
}
2016-08-28 15:52:57 +02:00
scriptOffs = getAllScriptOffsets(htmlFile);
htmlFile += hijackConsoleErrorsScript(JSON.stringify(scriptOffs));
2016-07-11 21:22:29 +02:00
return htmlFile;
2016-06-24 00:29:55 +02:00
}
renderSketch() {
2016-07-11 21:22:29 +02:00
const doc = ReactDOM.findDOMNode(this);
if (this.props.isPlaying && !this.props.infiniteLoop) {
srcDoc.set(doc, this.injectLocalFiles());
2016-09-29 00:05:14 +02:00
this.props.endSketchRefresh();
2016-07-11 21:22:29 +02:00
} else {
doc.srcdoc = '';
srcDoc.set(doc, ' ');
2016-07-11 21:22:29 +02:00
}
2016-06-24 00:29:55 +02:00
}
2016-06-27 21:47:48 +02:00
renderFrameContents() {
const doc = ReactDOM.findDOMNode(this).contentDocument;
if (doc.readyState === 'complete') {
2016-06-27 23:22:54 +02:00
this.renderSketch();
2016-06-27 21:47:48 +02:00
} else {
setTimeout(this.renderFrameContents, 0);
2016-06-24 00:29:55 +02:00
}
}
render() {
2016-06-27 21:47:48 +02:00
return (
<iframe
className="preview-frame"
2016-08-10 23:24:52 +02:00
aria-label="sketch output"
role="main"
2016-07-13 21:23:48 +02:00
tabIndex="0"
2016-06-27 21:47:48 +02:00
frameBorder="0"
title="sketch output"
2016-07-11 23:32:13 +02:00
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-modals allow-forms"
/>
2016-06-27 21:47:48 +02:00
);
2016-06-24 00:29:55 +02:00
}
}
2016-06-27 21:47:48 +02:00
PreviewFrame.propTypes = {
isPlaying: PropTypes.bool.isRequired,
isTextOutputPlaying: PropTypes.bool.isRequired,
textOutput: PropTypes.bool.isRequired,
head: PropTypes.object.isRequired,
2016-08-18 00:13:17 +02:00
content: PropTypes.string,
2016-07-11 21:22:29 +02:00
htmlFile: PropTypes.shape({
content: PropTypes.string.isRequired
}),
2016-07-12 03:54:08 +02:00
jsFiles: PropTypes.array.isRequired,
2016-07-20 01:36:50 +02:00
cssFiles: PropTypes.array.isRequired,
files: PropTypes.array.isRequired,
2016-08-18 00:13:17 +02:00
dispatchConsoleEvent: PropTypes.func,
children: PropTypes.element,
infiniteLoop: PropTypes.bool.isRequired,
2016-09-28 21:20:54 +02:00
resetInfiniteLoops: PropTypes.func.isRequired,
2016-09-29 00:05:14 +02:00
autorefresh: PropTypes.bool.isRequired,
endSketchRefresh: PropTypes.func.isRequired,
previewIsRefreshing: PropTypes.bool.isRequired,
2016-06-27 21:47:48 +02:00
};
2016-06-24 00:29:55 +02:00
export default PreviewFrame;