esnextbin
Version:
Prototype JavaScript programs in the browser with ESNext (a.k.a ES2015) syntax and features, using modules from NPM.
350 lines (295 loc) • 9.14 kB
JSX
import React from 'react';
import ReactDOM from 'react-dom';
import Mousetrap from 'mousetrap';
import Progress from 'react-progress-2';
import * as Babel from 'babel-standalone';
import querystring from 'querystring';
import prettier from 'prettier-standalone';
import Header from '../components/Header';
import Editors from '../components/Editors';
import Sandbox from '../components/Sandbox';
import * as Defaults from '../utils/DefaultsUtil';
import * as StorageUtils from '../utils/StorageUtils';
import * as GistAPIUtils from '../utils/GistAPIUtils';
class Main extends React.Component {
constructor() {
super();
this.query = {};
this.state = {
bundle: {},
bundling: false,
activeEditor: 'code',
shareModal: false,
autorunIsOn: false,
editorsData: {
code: Defaults.CODE,
transpiledCode: this._transpileCode(Defaults.CODE),
html: Defaults.HTML,
json: Defaults.PACKAGE_JSON,
error: void 0
}
};
}
componentDidMount() {
this.query = this._parseQuery();
const gistId = this.query.gist;
const sha = this.query.rev || this.query.sha;
if (gistId) {
StorageUtils.turnOffSession();
Progress.show();
GistAPIUtils.getGist({id: gistId, sha}, (err, gistSession) => {
Progress.hide();
if (err) {
console.log(err); // show special error on page
return;
}
const { transpiledCode, error } = this._transpileCodeAndCatch(gistSession.code);
const editorsData = this._updateEditorsData(Object.assign(gistSession, { transpiledCode, error }));
this.setState({ editorsData });
if (this.query.execute || this.query.exec) {
setTimeout(() => this.handleRunClick(), 0);
}
});
} else {
this.checkPreviousSession();
}
this.bindKeyShortcuts();
}
checkPreviousSession() {
const session = StorageUtils.getSession();
if (session) {
const newState = {};
const { autorun, ...editorsDataSession } = session;
if (autorun) {
newState.autorunIsOn = autorun;
}
const { transpiledCode, error } = this._transpileCodeAndCatch(session.code);
newState.editorsData = this._updateEditorsData(Object.assign(editorsDataSession, { transpiledCode, error }));
this.setState(newState);
}
}
bindKeyShortcuts() {
const mousetrap = Mousetrap(ReactDOM.findDOMNode(this));
mousetrap.bind(['command+e', 'ctrl+e'], (e) => {
e.preventDefault();
this.handleRunClick();
});
mousetrap.bind(['command+s', 'ctrl+s'], (e) => {
e.preventDefault();
this.handleSaveGist('public');
});
mousetrap.bind(['ctrl+alt+f'], (e) => {
e.preventDefault();
this.handlePrettierClick();
});
}
handleRunClick() {
if (this.state.bundling) return;
const bundle = this._getBundle();
bundle && this.setState({ bundle });
}
handlePrettierClick() {
const code = prettier.format(this.state.editorsData.code);
const editorsData = this._updateEditorsData({code});
this.setState({ editorsData });
}
handleChangeEditor(activeEditor) {
this.setState({ activeEditor });
}
handleStartBundle() {
if (this.state.bundling) return;
this.progressDelay = setTimeout(() => {
this.setState({bundling: true});
Progress.show();
}, 100);
}
handleEndBundle() {
clearTimeout(this.progressDelay);
if (this.triggerGist) {
const status = this.triggerGist;
const gistId = this.query.gist;
const { editorsData } = this.state;
const fn = (err, res, isFork) => {
Progress.hideAll();
if (err) {
console.log(err); // show special error on page
return;
}
if (!gistId || isFork) {
window.location.search = `gist=${res.body.id}`;
}
this.finishHandleEndBundle();
};
this.triggerGist = false;
if (gistId) {
GistAPIUtils.updateGist(gistId, editorsData, status, fn);
} else {
GistAPIUtils.createGist(editorsData, status, fn);
}
} else {
this.finishHandleEndBundle();
}
}
finishHandleEndBundle() {
Progress.hideAll();
this.setState({bundling: false});
}
handleSaveGist(status) {
Progress.show();
// talk with gist API on endBundle event of sandbox
this.triggerGist = status;
this.handleRunClick();
}
openShareModal() {
this.setState({shareModal: true});
}
closeShareModal() {
this.setState({shareModal: false});
}
handleReset() {
GistAPIUtils.unauthorize();
StorageUtils.cleanSession();
window.location.reload();
}
toggleAutorun() {
const autorunIsOn = !this.state.autorunIsOn;
StorageUtils.saveToSession('autorun', autorunIsOn);
this.setState({ autorunIsOn });
}
autorunOnChange() {
if (this.autorunDelay) {
clearTimeout(this.autorunDelay);
}
this.autorunDelay = setTimeout(() => {
this.handleRunClick();
}, 1000);
}
handleCodeChange(code) {
StorageUtils.saveToSession('code', code);
clearTimeout(this.errorDelay);
const { transpiledCode, error } = this._transpileCodeAndCatch(code);
if (error) {
this.errorDelay = setTimeout(() => {
const editorsData = this._updateEditorsData({error});
this.setState({ editorsData });
}, 1000);
}
const editorsData = this._updateEditorsData({code, transpiledCode, error: ''});
this.setState({ editorsData });
if (this.state.autorunIsOn) {
this.autorunOnChange();
}
}
handleHTMLChange(html) {
StorageUtils.saveToSession('html', html);
const editorsData = this._updateEditorsData({html, error: ''});
this.setState({ editorsData });
}
handlePackageChange(json) {
StorageUtils.saveToSession('json', json);
const editorsData = this._updateEditorsData({json, error: ''});
this.setState({ editorsData });
}
handleDependencies(modules) {
const { bundle } = this.state;
const updatedPackage = Object.assign({}, bundle.package, {
dependencies: modules.reduce((memo, mod) => {
memo[mod.name] = mod.version;
return memo;
}, {})
});
const editorsData = this._updateEditorsData({
json: JSON.stringify(updatedPackage, null, 2)
});
this.setState({ editorsData });
}
handleErrorBundle(err) {
console.log(err); // maybe show some popup or notification here?
this.finishHandleEndBundle();
}
render() {
const { bundle, editorsData, activeEditor, autorunIsOn, bundling } = this.state;
return (
<div className="main">
<Progress.Component />
<Header
height={Defaults.HEADER_HEIGHT}
activeEditor={activeEditor}
isBundling={bundling}
autorunIsOn={autorunIsOn}
onShareClick={::this.openShareModal}
onRunClick={::this.handleRunClick}
onPrettierClick={::this.handlePrettierClick}
onEditorClick={::this.handleChangeEditor}
onSaveGistClick={::this.handleSaveGist}
onResetEditors={::this.handleReset}
onToggleAutorun={::this.toggleAutorun}
/>
<div className="content" tabIndex="-1">
<Editors
active={activeEditor}
code={editorsData.code}
html={editorsData.html}
json={editorsData.json}
error={editorsData.error}
headerHeight={Defaults.HEADER_HEIGHT}
onCodeChange={::this.handleCodeChange}
onHTMLChange={::this.handleHTMLChange}
onPackageChange={::this.handlePackageChange}
/>
<Sandbox
bundle={bundle}
onModules={::this.handleDependencies}
onStartBundle={::this.handleStartBundle}
onErrorBundle={::this.handleErrorBundle}
onEndBundle={::this.handleEndBundle}
/>
</div>
</div>
);
}
_parseQuery() {
return querystring.parse(window.location.search.slice(1));
}
_updateEditorsData(newData) {
return Object.assign({}, this.state.editorsData, newData);
}
_getBundle() {
let { editorsData } = this.state;
let json;
try {
json = JSON.parse(editorsData.json);
} catch (error) {
editorsData = this._updateEditorsData({ error });
this.setState({ editorsData });
return;
}
return {
code: editorsData.transpiledCode,
raw: editorsData.code,
html: editorsData.html,
package: json
};
}
_transpileCode(code) {
return Babel.transform(code, Defaults.BABEL_OPTIONS).code;
}
_transpileCodeAndCatch(code) {
let transpiledCode;
let error;
if (code) {
try {
transpiledCode = this._transpileCode(code);
} catch (err) {
if (err._babel) {
transpiledCode = `/*
${err.message || 'Error while transpilation'}
*/`;
error = err;
}
}
}
return { transpiledCode, error };
}
}
export default Main;