update eslint to latest version, fix lots of linting errors (#308)

* update eslint and dependencies, fix linting errors that can be fixed with --fix

* fix lots of linting errors

* update eslintrc, fix some linting errors

* fix all server side linting errors, untested

* fix errors that fixing linting errors had caused

* fix client side eslint errors

* fix client side linting errors

* fix refs lint errors

* fix more linting errors

* update eslint and dependencies, fix linting errors that can be fixed with --fix

* fix lots of linting errors

* update eslintrc, fix some linting errors

* fix all server side linting errors, untested

* fix errors that fixing linting errors had caused

* fix client side eslint errors

* fix client side linting errors

* fix refs lint errors

* fix more linting errors

* fix some accessibility linting errors

* fix a lot of linting errors

* fix a billion more linting errors

* hopefully fix all linting errors, still need to test

* fix bugs that fixing linting had caused
This commit is contained in:
Cassie Tarakajian 2017-02-22 14:29:35 -05:00 committed by GitHub
parent 059308fbfe
commit e87390adb9
73 changed files with 964 additions and 733 deletions

View file

@ -24,7 +24,20 @@
"no-console": 0, "no-console": 0,
"no-alert": 0, "no-alert": 0,
"no-underscore-dangle": 0, "no-underscore-dangle": 0,
"max-len": [1, 120, 4], "max-len": [1, 120, 2, {ignoreComments: true}],
"quote-props": [1, "consistent-as-needed"],
"no-unused-vars": [1, {"vars": "local", "args": "none"}],
"consistent-return": ["error", { "treatUndefinedAsUnspecified": true }],
"no-param-reassign": [2, { "props": false }],
"react/self-closing-comp": ["error", {
"component": true,
"html": false
}],
"newline-per-chained-call": 0,
"react/prefer-stateless-function": [2,
{ "ignorePureComponents": true
}],
"class-methods-use-this": 0
}, },
"plugins": [ "plugins": [
"react", "jsx-a11y", "import" "react", "jsx-a11y", "import"

View file

@ -1,85 +1,86 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
function Nav(props) { class Nav extends React.PureComponent {
render() {
return ( return (
<nav className="nav" role="navigation" title="main-navigation"> <nav className="nav" role="navigation" title="main-navigation">
<ul className="nav__items-left" title="project-menu"> <ul className="nav__items-left" title="project-menu">
<li className="nav__item"> <li className="nav__item">
<a <button
className="nav__new" className="nav__new"
onClick={() => { onClick={() => {
if (!props.unsavedChanges) { if (!this.props.unsavedChanges) {
props.newProject(); this.props.newProject();
} else if (props.warnIfUnsavedChanges()) { } else if (this.props.warnIfUnsavedChanges()) {
props.newProject(); this.props.newProject();
} }
}} }}
> >
New New
</a> </button>
</li> </li>
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (!props.project.owner || props.project.owner && props.project.owner.id === props.user.id) { if (!this.props.project.owner || (this.props.project.owner && this.props.project.owner.id === this.props.user.id)) {
return ( return (
<li className="nav__item"> <li className="nav__item">
<a <button
className="nav__save" className="nav__save"
onClick={() => { onClick={() => {
if (props.user.authenticated) { if (this.props.user.authenticated) {
props.saveProject(); this.props.saveProject();
} else { } else {
props.showErrorModal('forceAuthentication'); this.props.showErrorModal('forceAuthentication');
} }
}} }}
> >
Save Save
</a> </button>
</li> </li>
); );
} }
})()} })()}
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (props.project.id && props.user.authenticated) { if (this.props.project.id && this.props.user.authenticated) {
return ( return (
<li className="nav__item"> <li className="nav__item">
<a className="nav__clone" onClick={props.cloneProject}> <button className="nav__clone" onClick={this.props.cloneProject}>
Duplicate Duplicate
</a> </button>
</li> </li>
); );
} }
})()} })()}
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (props.project.id) { if (this.props.project.id) {
return ( return (
<li className="nav__item"> <li className="nav__item">
<a className="nav__export" onClick={() => props.exportProjectAsZip(props.project.id)}> <button className="nav__export" onClick={() => this.props.exportProjectAsZip(this.props.project.id)}>
Download Download
</a> </button>
</li> </li>
); );
} }
})()} })()}
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (props.project.id) { if (this.props.project.id) {
return ( return (
<li className="nav__item"> <li className="nav__item">
<a onClick={props.showShareModal}> <button onClick={this.props.showShareModal}>
Share Share
</a> </button>
</li> </li>
); );
} }
})()} })()}
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (props.user.authenticated) { if (this.props.user.authenticated) {
return ( return (
<li className="nav__item"> <li className="nav__item">
<p className="nav__open"> <p className="nav__open">
<Link <Link
to={`/${props.user.username}/sketches`} to={`/${this.props.user.username}/sketches`}
onClick={props.stopSketch} onClick={this.props.stopSketch}
> >
Open Open
</Link> </Link>
@ -100,6 +101,7 @@ function Nav(props) {
<a <a
href="https://p5js.org/reference/" href="https://p5js.org/reference/"
target="_blank" target="_blank"
rel="noopener noreferrer"
>Reference</a> >Reference</a>
</p> </p>
</li> </li>
@ -113,7 +115,7 @@ function Nav(props) {
</ul> </ul>
<ul className="nav__items-right" title="user-menu"> <ul className="nav__items-right" title="user-menu">
{(() => { {(() => {
if (!props.user.authenticated) { if (!this.props.user.authenticated) {
return ( return (
<li className="nav__item"> <li className="nav__item">
<p> <p>
@ -124,29 +126,34 @@ function Nav(props) {
} }
return ( return (
<li className="nav__item"> <li className="nav__item">
<a>Hello, {props.user.username}!</a> <a>Hello, {this.props.user.username}!</a>
<ul className="nav__dropdown"> <ul className="nav__dropdown">
<li className="nav__dropdown-heading"> <li className="nav__dropdown-heading">
<a>Hello, {props.user.username}!</a> <a>Hello, {this.props.user.username}!</a>
</li> </li>
<li> <li>
<Link to={`/${props.user.username}/sketches`}> <Link to={`/${this.props.user.username}/sketches`}>
My sketches My sketches
</Link> </Link>
</li> </li>
<li> <li>
<a onClick={props.logoutUser} > <button onClick={this.props.logoutUser} >
Log out Log out
</a> </button>
</li> </li>
</ul> </ul>
</li> </li>
); );
})()} })()}
</ul> </ul>
<div className="nav__announce">This is a preview version of the editor, that has not yet been officially released. It is in development, you can report bugs <a href="https://github.com/processing/p5.js-web-editor/issues" target="_blank">here</a>. Please use with caution.</div> <div className="nav__announce">
This is a preview version of the editor, that has not yet been officially released. It is in development, you can report bugs
<a href="https://github.com/processing/p5.js-web-editor/issues" target="_blank" rel="noopener noreferrer">here</a>.
Please use with caution.
</div>
</nav> </nav>
); );
}
} }
Nav.propTypes = { Nav.propTypes = {
@ -168,7 +175,16 @@ Nav.propTypes = {
logoutUser: PropTypes.func.isRequired, logoutUser: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired,
showShareModal: PropTypes.func.isRequired, showShareModal: PropTypes.func.isRequired,
showErrorModal: PropTypes.func.isRequired showErrorModal: PropTypes.func.isRequired,
unsavedChanges: PropTypes.bool.isRequired,
warnIfUnsavedChanges: PropTypes.func.isRequired
};
Nav.defaultProps = {
project: {
id: undefined,
owner: undefined
}
}; };
export default Nav; export default Nav;

View file

@ -53,7 +53,6 @@ export const COLLAPSE_CONSOLE = 'COLLAPSE_CONSOLE';
export const UPDATE_LINT_MESSAGE = 'UPDATE_LINT_MESSAGE'; export const UPDATE_LINT_MESSAGE = 'UPDATE_LINT_MESSAGE';
export const CLEAR_LINT_MESSAGE = 'CLEAR_LINT_MESSAGE'; export const CLEAR_LINT_MESSAGE = 'CLEAR_LINT_MESSAGE';
export const UPDATE_LINENUMBER = 'UPDATE_LINENUMBER';
export const SHOW_FILE_OPTIONS = 'SHOW_FILE_OPTIONS'; export const SHOW_FILE_OPTIONS = 'SHOW_FILE_OPTIONS';
export const HIDE_FILE_OPTIONS = 'HIDE_FILE_OPTIONS'; export const HIDE_FILE_OPTIONS = 'HIDE_FILE_OPTIONS';

View file

@ -30,11 +30,15 @@ class App extends React.Component {
} }
App.propTypes = { App.propTypes = {
children: PropTypes.object, children: PropTypes.element,
location: PropTypes.shape({ location: PropTypes.shape({
pathname: PropTypes.string pathname: PropTypes.string
}), }).isRequired,
setPreviousPath: PropTypes.func.isRequired, setPreviousPath: PropTypes.func.isRequired,
}; };
App.defaultProps = {
children: null
};
export default connect(() => ({}), { setPreviousPath })(App); export default connect(() => ({}), { setPreviousPath })(App);

View file

@ -11,7 +11,11 @@ function Overlay(props) {
} }
Overlay.propTypes = { Overlay.propTypes = {
children: PropTypes.object children: PropTypes.element
};
Overlay.defaultProps = {
children: null
}; };
export default Overlay; export default Overlay;

View file

@ -14,10 +14,3 @@ export function clearLintMessage() {
type: ActionTypes.CLEAR_LINT_MESSAGE type: ActionTypes.CLEAR_LINT_MESSAGE
}; };
} }
export function updateLineNumber(lineNumber) {
return {
type: ActionTypes.UPDATE_LINENUMBER,
lineNumber
};
}

View file

@ -1,9 +1,9 @@
import * as ActionTypes from '../../../constants';
import axios from 'axios'; import axios from 'axios';
import objectID from 'bson-objectid'; import objectID from 'bson-objectid';
import blobUtil from 'blob-util'; import blobUtil from 'blob-util';
import { setUnsavedChanges } from './ide';
import { reset } from 'redux-form'; import { reset } from 'redux-form';
import * as ActionTypes from '../../../constants';
import { setUnsavedChanges } from './ide';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
@ -18,11 +18,11 @@ function createUniqueName(name, parentId, files) {
.children.map(childFileId => files.find(file => file.id === childFileId)); .children.map(childFileId => files.find(file => file.id === childFileId));
let testName = name; let testName = name;
let index = 1; let index = 1;
let existingName = siblingFiles.find((file) => name === file.name); let existingName = siblingFiles.find(file => name === file.name);
while (existingName) { while (existingName) {
testName = appendToFilename(name, `-${index}`); testName = appendToFilename(name, `-${index}`);
index++; index += 1;
existingName = siblingFiles.find((file) => testName === file.name); // eslint-disable-line existingName = siblingFiles.find((file) => testName === file.name); // eslint-disable-line
} }
return testName; return testName;
@ -56,7 +56,7 @@ export function createFile(formProps) {
children: [] children: []
}; };
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true }) axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
.then(response => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.CREATE_FILE, type: ActionTypes.CREATE_FILE,
...response.data, ...response.data,
@ -113,7 +113,7 @@ export function createFolder(formProps) {
fileType: 'folder' fileType: 'folder'
}; };
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true }) axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
.then(response => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.CREATE_FILE, type: ActionTypes.CREATE_FILE,
...response.data, ...response.data,
@ -200,7 +200,7 @@ export function deleteFile(id, parentId) {
parentId parentId
}); });
}) })
.catch(response => { .catch((response) => {
dispatch({ dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,
error: response.data error: response.data

View file

@ -1,5 +1,5 @@
import * as ActionTypes from '../../../constants';
import axios from 'axios'; import axios from 'axios';
import * as ActionTypes from '../../../constants';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
@ -7,7 +7,7 @@ function updatePreferences(formParams, dispatch) {
axios.put(`${ROOT_URL}/preferences`, formParams, { withCredentials: true }) axios.put(`${ROOT_URL}/preferences`, formParams, { withCredentials: true })
.then(() => { .then(() => {
}) })
.catch((response) => dispatch({ .catch(response => dispatch({
type: ActionTypes.ERROR, type: ActionTypes.ERROR,
error: response.data error: response.data
})); }));

View file

@ -1,7 +1,8 @@
import * as ActionTypes from '../../../constants';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import axios from 'axios'; import axios from 'axios';
import objectID from 'bson-objectid'; import objectID from 'bson-objectid';
import moment from 'moment';
import * as ActionTypes from '../../../constants';
import { showToast, setToastText } from './toast'; import { showToast, setToastText } from './toast';
import { setUnsavedChanges, import { setUnsavedChanges,
justOpenedProject, justOpenedProject,
@ -9,7 +10,6 @@ import { setUnsavedChanges,
setProjectSavedTime, setProjectSavedTime,
resetProjectSavedTime, resetProjectSavedTime,
showErrorModal } from './ide'; showErrorModal } from './ide';
import moment from 'moment';
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
@ -21,7 +21,7 @@ export function getProject(id) {
dispatch(resetProjectSavedTime()); dispatch(resetProjectSavedTime());
} }
axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true }) axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
.then(response => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.SET_PROJECT, type: ActionTypes.SET_PROJECT,
project: response.data, project: response.data,
@ -86,7 +86,7 @@ export function saveProject(autosave = false) {
}); });
} else { } else {
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
.then(response => { .then((response) => {
dispatch(setUnsavedChanges(false)); dispatch(setUnsavedChanges(false));
dispatch(setProjectSavedTime(moment().format())); dispatch(setProjectSavedTime(moment().format()));
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
@ -108,7 +108,7 @@ export function saveProject(autosave = false) {
} }
} }
}) })
.catch(response => { .catch((response) => {
if (response.status === 403) { if (response.status === 403) {
dispatch(showErrorModal('staleSession')); dispatch(showErrorModal('staleSession'));
} else { } else {
@ -131,7 +131,7 @@ export function autosaveProject() {
export function createProject() { export function createProject() {
return (dispatch) => { return (dispatch) => {
axios.post(`${ROOT_URL}/projects`, {}, { withCredentials: true }) axios.post(`${ROOT_URL}/projects`, {}, { withCredentials: true })
.then(response => { .then((response) => {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({ dispatch({
type: ActionTypes.NEW_PROJECT, type: ActionTypes.NEW_PROJECT,
@ -192,7 +192,7 @@ export function cloneProject() {
// const newFiles = state.files; // const newFiles = state.files;
const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles }); const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles });
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
.then(response => { .then((response) => {
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
dispatch({ dispatch({
type: ActionTypes.NEW_PROJECT, type: ActionTypes.NEW_PROJECT,

View file

@ -1,5 +1,5 @@
import * as ActionTypes from '../../../constants';
import axios from 'axios'; import axios from 'axios';
import * as ActionTypes from '../../../constants';
import { showErrorModal, setPreviousPath } from './ide'; import { showErrorModal, setPreviousPath } from './ide';
import { resetProject } from './project'; import { resetProject } from './project';
@ -14,7 +14,7 @@ export function getProjects(username) {
url = `${ROOT_URL}/projects`; url = `${ROOT_URL}/projects`;
} }
axios.get(url, { withCredentials: true }) axios.get(url, { withCredentials: true })
.then(response => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.SET_PROJECTS, type: ActionTypes.SET_PROJECTS,
projects: response.data projects: response.data
@ -41,7 +41,7 @@ export function deleteProject(id) {
id id
}); });
}) })
.catch(response => { .catch((response) => {
if (response.status === 403) { if (response.status === 403) {
dispatch(showErrorModal('staleSession')); dispatch(showErrorModal('staleSession'));
} else { } else {

View file

@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import { createFile } from './files'; import { createFile } from './files';
const textFileRegex = /(text\/|application\/json)/;
const textFileRegex = /(text\/|application\/json)/;
const s3BucketHttps = `https://s3-us-west-2.amazonaws.com/${process.env.S3_BUCKET}/`; const s3BucketHttps = `https://s3-us-west-2.amazonaws.com/${process.env.S3_BUCKET}/`;
const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api';
const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB
@ -36,11 +36,11 @@ export function dropzoneAcceptCallback(file, done) {
// check mime type // check mime type
// if text, local interceptor // if text, local interceptor
if (file.type.match(textFileRegex) && file.size < MAX_LOCAL_FILE_SIZE) { if (file.type.match(textFileRegex) && file.size < MAX_LOCAL_FILE_SIZE) {
localIntercept(file).then(result => { localIntercept(file).then((result) => {
file.content = result; // eslint-disable-line file.content = result; // eslint-disable-line
done('Uploading plaintext file locally.'); done('Uploading plaintext file locally.');
}) })
.catch(result => { .catch((result) => {
done(`Failed to download file ${file.name}: ${result}`); done(`Failed to download file ${file.name}: ${result}`);
console.warn(file); console.warn(file);
}); });
@ -55,14 +55,14 @@ export function dropzoneAcceptCallback(file, done) {
{ {
withCredentials: true withCredentials: true
}) })
.then(response => { .then((response) => {
file.custom_status = 'ready'; // eslint-disable-line file.custom_status = 'ready'; // eslint-disable-line
file.postData = response.data; // eslint-disable-line file.postData = response.data; // eslint-disable-line
file.s3 = response.data.key; // eslint-disable-line file.s3 = response.data.key; // eslint-disable-line
file.previewTemplate.className += ' uploading'; // eslint-disable-line file.previewTemplate.className += ' uploading'; // eslint-disable-line
done(); done();
}) })
.catch(response => { .catch((response) => {
file.custom_status = 'rejected'; // eslint-disable-line file.custom_status = 'rejected'; // eslint-disable-line
if (response.data.responseText && response.data.responseText.message) { if (response.data.responseText && response.data.responseText.message) {
done(response.data.responseText.message); done(response.data.responseText.message);
@ -76,7 +76,7 @@ export function dropzoneAcceptCallback(file, done) {
export function dropzoneSendingCallback(file, xhr, formData) { export function dropzoneSendingCallback(file, xhr, formData) {
return () => { return () => {
if (!file.type.match(textFileRegex) || file.size >= MAX_LOCAL_FILE_SIZE) { if (!file.type.match(textFileRegex) || file.size >= MAX_LOCAL_FILE_SIZE) {
Object.keys(file.postData).forEach(key => { Object.keys(file.postData).forEach((key) => {
formData.append(key, file.postData[key]); formData.append(key, file.postData[key]);
}); });
formData.append('Content-type', ''); formData.append('Content-type', '');

View file

@ -1,10 +1,11 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
import { browserHistory } from 'react-router';
const exitUrl = require('../../../images/exit.svg'); const exitUrl = require('../../../images/exit.svg');
const squareLogoUrl = require('../../../images/p5js-square-logo.svg'); const squareLogoUrl = require('../../../images/p5js-square-logo.svg');
const playUrl = require('../../../images/play.svg'); const playUrl = require('../../../images/play.svg');
const asteriskUrl = require('../../../images/p5-asterisk.svg'); const asteriskUrl = require('../../../images/p5-asterisk.svg');
import { browserHistory } from 'react-router';
class About extends React.Component { class About extends React.Component {
constructor(props) { constructor(props) {
@ -13,7 +14,7 @@ class About extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.refs.about.focus(); this.aboutSection.focus();
} }
closeAboutModal() { closeAboutModal() {
@ -22,7 +23,7 @@ class About extends React.Component {
render() { render() {
return ( return (
<section className="about" ref="about" tabIndex="0"> <section className="about" ref={(element) => { this.aboutSection = element; }} tabIndex="0">
<header className="about__header"> <header className="about__header">
<h2 className="about__header-title">Welcome</h2> <h2 className="about__header-title">Welcome</h2>
<button className="about__exit-button" onClick={this.closeAboutModal}> <button className="about__exit-button" onClick={this.closeAboutModal}>
@ -36,6 +37,7 @@ class About extends React.Component {
<a <a
href="http://hello.p5js.org/" href="http://hello.p5js.org/"
target="_blank" target="_blank"
rel="noopener noreferrer"
> >
<InlineSVG className="about__play-video-button" src={playUrl} alt="Play Hello Video" /> <InlineSVG className="about__play-video-button" src={playUrl} alt="Play Hello Video" />
Play hello! video</a> Play hello! video</a>
@ -47,6 +49,7 @@ class About extends React.Component {
<a <a
href="https://p5js.org/examples/" href="https://p5js.org/examples/"
target="_blank" target="_blank"
rel="noopener noreferrer"
> >
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Examples</a> Examples</a>
@ -55,6 +58,7 @@ class About extends React.Component {
<a <a
href="https://p5js.org/tutorials/" href="https://p5js.org/tutorials/"
target="_blank" target="_blank"
rel="noopener noreferrer"
> >
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Tutorials</a> Tutorials</a>
@ -66,6 +70,7 @@ class About extends React.Component {
<a <a
href="https://p5js.org/libraries/" href="https://p5js.org/libraries/"
target="_blank" target="_blank"
rel="noopener noreferrer"
> >
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Libraries</a> Libraries</a>
@ -74,6 +79,7 @@ class About extends React.Component {
<a <a
href="https://p5js.org/reference/" href="https://p5js.org/reference/"
target="_blank" target="_blank"
rel="noopener noreferrer"
> >
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Reference</a> Reference</a>
@ -82,6 +88,7 @@ class About extends React.Component {
<a <a
href="https://forum.processing.org/two/" href="https://forum.processing.org/two/"
target="_blank" target="_blank"
rel="noopener noreferrer"
> >
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" /> <InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
Forum</a> Forum</a>
@ -93,18 +100,21 @@ class About extends React.Component {
<a <a
href="https://github.com/processing/p5.js-web-editor" href="https://github.com/processing/p5.js-web-editor"
target="_blank" target="_blank"
rel="noopener noreferrer"
>Contribute</a> >Contribute</a>
</p> </p>
<p className="about__footer-list"> <p className="about__footer-list">
<a <a
href="https://github.com/processing/p5.js-web-editor/issues/new" href="https://github.com/processing/p5.js-web-editor/issues/new"
target="_blank" target="_blank"
rel="noopener noreferrer"
>Report a bug</a> >Report a bug</a>
</p> </p>
<p className="about__footer-list"> <p className="about__footer-list">
<a <a
href="https://twitter.com/p5xjs?lang=en" href="https://twitter.com/p5xjs?lang=en"
target="_blank" target="_blank"
rel="noopener noreferrer"
>Twitter</a> >Twitter</a>
</p> </p>
<button className="about__ok-button" onClick={this.closeAboutModal}>OK!</button> <button className="about__ok-button" onClick={this.closeAboutModal}>OK!</button>

View file

@ -1,12 +1,13 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
const upArrowUrl = require('../../../images/up-arrow.svg'); const upArrowUrl = require('../../../images/up-arrow.svg');
const downArrowUrl = require('../../../images/down-arrow.svg'); const downArrowUrl = require('../../../images/down-arrow.svg');
import classNames from 'classnames';
class Console extends React.Component { class Console extends React.Component {
componentDidUpdate() { componentDidUpdate() {
this.refs.console_messages.scrollTop = this.refs.console_messages.scrollHeight; this.consoleMessages.scrollTop = this.consoleMessages.scrollHeight;
} }
render() { render() {
@ -16,13 +17,13 @@ class Console extends React.Component {
}); });
return ( return (
<div ref="console" className={consoleClass} role="main" tabIndex="0" title="console"> <div className={consoleClass} role="main" tabIndex="0" title="console">
<div className="preview-console__header"> <div className="preview-console__header">
<h2 className="preview-console__header-title">Console</h2> <h2 className="preview-console__header-title">Console</h2>
<div className="preview-console__header-buttons"> <div className="preview-console__header-buttons">
<a className="preview-console__clear" onClick={this.props.clearConsole} aria-label="clear console"> <button className="preview-console__clear" onClick={this.props.clearConsole} aria-label="clear console">
Clear Clear
</a> </button>
<button className="preview-console__collapse" onClick={this.props.collapseConsole} aria-label="collapse console"> <button className="preview-console__collapse" onClick={this.props.collapseConsole} aria-label="collapse console">
<InlineSVG src={downArrowUrl} /> <InlineSVG src={downArrowUrl} />
</button> </button>
@ -31,20 +32,20 @@ class Console extends React.Component {
</button> </button>
</div> </div>
</div> </div>
<div ref="console_messages" className="preview-console__messages"> <div ref={(element) => { this.consoleMessages = element; }} className="preview-console__messages">
{this.props.consoleEvents.map((consoleEvent, index) => { {this.props.consoleEvents.map((consoleEvent) => {
const args = consoleEvent.arguments; const args = consoleEvent.arguments;
const method = consoleEvent.method; const method = consoleEvent.method;
if (Object.keys(args).length === 0) { if (Object.keys(args).length === 0) {
return ( return (
<div key={index} className="preview-console__undefined"> <div key={consoleEvent.id} className="preview-console__undefined">
<span key={`${index}-0`}>{'undefined'}</span> <span key={`${consoleEvent.id}-0`}>{'undefined'}</span>
</div> </div>
); );
} }
return ( return (
<div key={index} className={`preview-console__${method}`}> <div key={consoleEvent.id} className={`preview-console__${method}`}>
{Object.keys(args).map((key) => <span key={`${index}-${key}`}>{args[key]}</span>)} {Object.keys(args).map(key => <span key={`${consoleEvent.id}-${key}`}>{args[key]}</span>)}
</div> </div>
); );
})} })}
@ -56,12 +57,18 @@ class Console extends React.Component {
} }
Console.propTypes = { Console.propTypes = {
consoleEvents: PropTypes.array, consoleEvents: PropTypes.arrayOf(PropTypes.shape({
isPlaying: PropTypes.bool.isRequired, method: PropTypes.string.isRequired,
args: PropTypes.arrayOf(PropTypes.string)
})),
isExpanded: PropTypes.bool.isRequired, isExpanded: PropTypes.bool.isRequired,
collapseConsole: PropTypes.func.isRequired, collapseConsole: PropTypes.func.isRequired,
expandConsole: PropTypes.func.isRequired, expandConsole: PropTypes.func.isRequired,
clearConsole: PropTypes.func.isRequired clearConsole: PropTypes.func.isRequired
}; };
Console.defaultProps = {
consoleEvents: []
};
export default Console; export default Console;

View file

@ -1,12 +1,7 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import EditorAccessibility from '../components/EditorAccessibility';
import CodeMirror from 'codemirror'; import CodeMirror from 'codemirror';
import beautifyJS from 'js-beautify'; import beautifyJS from 'js-beautify';
const beautifyCSS = beautifyJS.css;
const beautifyHTML = beautifyJS.html;
import '../../../utils/p5-javascript';
import 'codemirror/mode/css/css'; import 'codemirror/mode/css/css';
import '../../../utils/htmlmixed';
import 'codemirror/addon/selection/active-line'; import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/lint/lint'; import 'codemirror/addon/lint/lint';
import 'codemirror/addon/lint/javascript-lint'; import 'codemirror/addon/lint/javascript-lint';
@ -15,22 +10,27 @@ import 'codemirror/addon/lint/html-lint';
import 'codemirror/addon/comment/comment'; import 'codemirror/addon/comment/comment';
import 'codemirror/keymap/sublime'; import 'codemirror/keymap/sublime';
import 'codemirror/addon/search/jump-to-line'; import 'codemirror/addon/search/jump-to-line';
import { JSHINT } from 'jshint'; import { JSHINT } from 'jshint';
window.JSHINT = JSHINT;
import { CSSLint } from 'csslint'; import { CSSLint } from 'csslint';
window.CSSLint = CSSLint;
import { HTMLHint } from 'htmlhint'; import { HTMLHint } from 'htmlhint';
window.HTMLHint = HTMLHint;
const beepUrl = require('../../../sounds/audioAlert.mp3');
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
import { debounce } from 'lodash';
import '../../../utils/htmlmixed';
import '../../../utils/p5-javascript';
import Timer from '../components/Timer';
import EditorAccessibility from '../components/EditorAccessibility';
const beautifyCSS = beautifyJS.css;
const beautifyHTML = beautifyJS.html;
window.JSHINT = JSHINT;
window.CSSLint = CSSLint;
window.HTMLHint = HTMLHint;
const beepUrl = require('../../../sounds/audioAlert.mp3');
const downArrowUrl = require('../../../images/down-arrow.svg'); const downArrowUrl = require('../../../images/down-arrow.svg');
const unsavedChangesDotUrl = require('../../../images/unsaved-changes-dot.svg'); const unsavedChangesDotUrl = require('../../../images/unsaved-changes-dot.svg');
import classNames from 'classnames';
import { debounce } from 'lodash';
import Timer from '../components/Timer';
const rightArrowUrl = require('../../../images/right-arrow.svg'); const rightArrowUrl = require('../../../images/right-arrow.svg');
const leftArrowUrl = require('../../../images/left-arrow.svg'); const leftArrowUrl = require('../../../images/left-arrow.svg');
@ -42,7 +42,7 @@ class Editor extends React.Component {
componentDidMount() { componentDidMount() {
this.beep = new Audio(beepUrl); this.beep = new Audio(beepUrl);
this.widgets = []; this.widgets = [];
this._cm = CodeMirror(this.refs.container, { // eslint-disable-line this._cm = CodeMirror(this.codemirrorContainer, { // eslint-disable-line
theme: `p5-${this.props.theme}`, theme: `p5-${this.props.theme}`,
lineNumbers: true, lineNumbers: true,
styleActiveLine: true, styleActiveLine: true,
@ -64,8 +64,8 @@ class Editor extends React.Component {
} }
}, 2000), }, 2000),
options: { options: {
asi: true, 'asi': true,
eqeqeq: false, 'eqeqeq': false,
'-W041': false '-W041': false
} }
} }
@ -162,7 +162,7 @@ class Editor extends React.Component {
initializeDocuments(files) { initializeDocuments(files) {
this._docs = {}; this._docs = {};
files.forEach(file => { files.forEach((file) => {
if (file.name !== 'root') { if (file.name !== 'root') {
this._docs[file.id] = CodeMirror.Doc(file.content, this.getFileMode(file.name)); // eslint-disable-line this._docs[file.id] = CodeMirror.Doc(file.content, this.getFileMode(file.name)); // eslint-disable-line
} }
@ -189,7 +189,7 @@ class Editor extends React.Component {
if (this.props.editorOptionsVisible) { if (this.props.editorOptionsVisible) {
this.props.closeEditorOptions(); this.props.closeEditorOptions();
} else { } else {
this.refs.optionsButton.focus(); this.optionsButton.focus();
this.props.showEditorOptions(); this.props.showEditorOptions();
} }
} }
@ -198,7 +198,7 @@ class Editor extends React.Component {
render() { render() {
const editorSectionClass = classNames({ const editorSectionClass = classNames({
editor: true, 'editor': true,
'sidebar--contracted': !this.props.isExpanded, 'sidebar--contracted': !this.props.isExpanded,
'editor--options': this.props.editorOptionsVisible 'editor--options': this.props.editorOptionsVisible
}); });
@ -238,7 +238,7 @@ class Editor extends React.Component {
className="editor__options-button" className="editor__options-button"
aria-label="editor options" aria-label="editor options"
tabIndex="0" tabIndex="0"
ref="optionsButton" ref={(element) => { this.optionsButton = element; }}
onClick={() => { onClick={() => {
this.toggleEditorOptions(); this.toggleEditorOptions();
}} }}
@ -248,18 +248,17 @@ class Editor extends React.Component {
</button> </button>
<ul className="editor__options" title="editor options"> <ul className="editor__options" title="editor options">
<li> <li>
<a onClick={this.tidyCode}>Tidy</a> <button onClick={this.tidyCode}>Tidy</button>
</li> </li>
<li> <li>
<a onClick={this.props.showKeyboardShortcutModal}>Keyboard shortcuts</a> <button onClick={this.props.showKeyboardShortcutModal}>Keyboard shortcuts</button>
</li> </li>
</ul> </ul>
</header> </header>
<div ref="container" className="editor-holder" tabIndex="0"> <div ref={(element) => { this.codemirrorContainer = element; }} className="editor-holder" tabIndex="0">
</div> </div>
<EditorAccessibility <EditorAccessibility
lintMessages={this.props.lintMessages} lintMessages={this.props.lintMessages}
lineNumber={this.props.lineNumber}
/> />
</section> </section>
); );
@ -268,11 +267,14 @@ class Editor extends React.Component {
Editor.propTypes = { Editor.propTypes = {
lintWarning: PropTypes.bool.isRequired, lintWarning: PropTypes.bool.isRequired,
lineNumber: PropTypes.string.isRequired, lintMessages: PropTypes.arrayOf(PropTypes.shape({
lintMessages: PropTypes.array.isRequired, severity: PropTypes.string.isRequired,
line: PropTypes.number.isRequired,
message: PropTypes.string.isRequired,
id: PropTypes.number.isRequired
})).isRequired,
updateLintMessage: PropTypes.func.isRequired, updateLintMessage: PropTypes.func.isRequired,
clearLintMessage: PropTypes.func.isRequired, clearLintMessage: PropTypes.func.isRequired,
updateLineNumber: PropTypes.func.isRequired,
indentationAmount: PropTypes.number.isRequired, indentationAmount: PropTypes.number.isRequired,
isTabIndent: PropTypes.bool.isRequired, isTabIndent: PropTypes.bool.isRequired,
updateFileContent: PropTypes.func.isRequired, updateFileContent: PropTypes.func.isRequired,
@ -281,7 +283,7 @@ Editor.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired, content: PropTypes.string.isRequired,
id: PropTypes.string.isRequired id: PropTypes.string.isRequired
}), }).isRequired,
editorOptionsVisible: PropTypes.bool.isRequired, editorOptionsVisible: PropTypes.bool.isRequired,
showEditorOptions: PropTypes.func.isRequired, showEditorOptions: PropTypes.func.isRequired,
closeEditorOptions: PropTypes.func.isRequired, closeEditorOptions: PropTypes.func.isRequired,
@ -293,7 +295,11 @@ Editor.propTypes = {
theme: PropTypes.string.isRequired, theme: PropTypes.string.isRequired,
unsavedChanges: PropTypes.bool.isRequired, unsavedChanges: PropTypes.bool.isRequired,
projectSavedTime: PropTypes.string.isRequired, projectSavedTime: PropTypes.string.isRequired,
files: PropTypes.array.isRequired, files: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
})).isRequired,
isExpanded: PropTypes.bool.isRequired, isExpanded: PropTypes.bool.isRequired,
collapseSidebar: PropTypes.func.isRequired, collapseSidebar: PropTypes.func.isRequired,
expandSidebar: PropTypes.func.isRequired, expandSidebar: PropTypes.func.isRequired,
@ -301,4 +307,8 @@ Editor.propTypes = {
clearConsole: PropTypes.func.isRequired clearConsole: PropTypes.func.isRequired
}; };
Editor.defaultProps = {
isUserOwner: false
};
export default Editor; export default Editor;

View file

@ -5,11 +5,11 @@ class EditorAccessibility extends React.Component {
} }
render() { render() {
let messages = []; const messages = [];
if (this.props.lintMessages.length > 0) { if (this.props.lintMessages.length > 0) {
this.props.lintMessages.forEach((lintMessage, i) => { this.props.lintMessages.forEach((lintMessage, i) => {
messages.push( messages.push(
<li key={i}> <li key={lintMessage.id}>
{lintMessage.severity} in line {lintMessage.severity} in line
{lintMessage.line} : {lintMessage.line} :
{lintMessage.message} {lintMessage.message}
@ -35,8 +35,12 @@ class EditorAccessibility extends React.Component {
} }
EditorAccessibility.propTypes = { EditorAccessibility.propTypes = {
lintMessages: PropTypes.array.isRequired, lintMessages: PropTypes.arrayOf(PropTypes.shape({
lineNumber: PropTypes.string.isRequired, severity: PropTypes.string.isRequired,
line: PropTypes.number.isRequired,
message: PropTypes.string.isRequired,
id: PropTypes.number.isRequired
})).isRequired,
}; };
export default EditorAccessibility; export default EditorAccessibility;

View file

@ -1,11 +1,12 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
import { Link } from 'react-router'; import { Link } from 'react-router';
const exitUrl = require('../../../images/exit.svg');
class ErrorModal extends React.Component { class ErrorModal extends React.Component {
componentDidMount() { componentDidMount() {
this.refs.modal.focus(); this.errorModal.focus();
} }
@ -23,7 +24,7 @@ class ErrorModal extends React.Component {
staleSession() { staleSession() {
return ( return (
<p> <p>
It looks like you've been logged out. Please&nbsp; It looks like you&apos;ve been logged out. Please&nbsp;
<Link to="/login" onClick={this.props.closeModal}>log in</Link>. <Link to="/login" onClick={this.props.closeModal}>log in</Link>.
</p> </p>
); );
@ -39,7 +40,7 @@ class ErrorModal extends React.Component {
render() { render() {
return ( return (
<section className="error-modal" ref="modal" tabIndex="0"> <section className="error-modal" ref={(element) => { this.errorModal = element; }} tabIndex="0">
<header className="error-modal__header"> <header className="error-modal__header">
<h2 className="error-modal__title">Error</h2> <h2 className="error-modal__title">Error</h2>
<button className="error-modal__exit-button" onClick={this.props.closeModal}> <button className="error-modal__exit-button" onClick={this.props.closeModal}>
@ -63,7 +64,7 @@ class ErrorModal extends React.Component {
} }
ErrorModal.propTypes = { ErrorModal.propTypes = {
type: PropTypes.string, type: PropTypes.string.isRequired,
closeModal: PropTypes.func.isRequired closeModal: PropTypes.func.isRequired
}; };

View file

@ -1,14 +1,15 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
import classNames from 'classnames';
import * as IDEActions from '../actions/ide';
import * as FileActions from '../actions/files';
const downArrowUrl = require('../../../images/down-arrow.svg'); const downArrowUrl = require('../../../images/down-arrow.svg');
const folderRightUrl = require('../../../images/triangle-arrow-right.svg'); const folderRightUrl = require('../../../images/triangle-arrow-right.svg');
const folderDownUrl = require('../../../images/triangle-arrow-down.svg'); const folderDownUrl = require('../../../images/triangle-arrow-down.svg');
const fileUrl = require('../../../images/file.svg'); const fileUrl = require('../../../images/file.svg');
import classNames from 'classnames';
export class FileNode extends React.Component { export class FileNode extends React.Component {
constructor(props) { constructor(props) {
@ -54,7 +55,7 @@ export class FileNode extends React.Component {
if (this.props.isOptionsOpen) { if (this.props.isOptionsOpen) {
this.props.hideFileOptions(this.props.id); this.props.hideFileOptions(this.props.id);
} else { } else {
this.refs[`fileOptions-${this.props.id}`].focus(); this[`fileOptions-${this.props.id}`].focus();
this.props.showFileOptions(this.props.id); this.props.showFileOptions(this.props.id);
} }
} }
@ -68,7 +69,7 @@ export class FileNode extends React.Component {
} }
render() { render() {
let itemClass = classNames({ const itemClass = classNames({
'sidebar__root-item': this.props.name === 'root', 'sidebar__root-item': this.props.name === 'root',
'sidebar__file-item': this.props.name !== 'root', 'sidebar__file-item': this.props.name !== 'root',
'sidebar__file-item--selected': this.props.isSelectedFile, 'sidebar__file-item--selected': this.props.isSelectedFile,
@ -77,11 +78,7 @@ export class FileNode extends React.Component {
'sidebar__file-item--closed': this.props.isFolderClosed 'sidebar__file-item--closed': this.props.isFolderClosed
}); });
return ( return (
<div <div className={itemClass}>
className={itemClass}
onClick={this.handleFileClick}
onBlur={() => setTimeout(() => this.props.hideFileOptions(this.props.id), 200)}
>
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (this.props.name !== 'root') { if (this.props.name !== 'root') {
return ( return (
@ -97,28 +94,28 @@ export class FileNode extends React.Component {
} }
return ( return (
<div> <div>
<span <button
className="sidebar__file-item-closed" className="sidebar__file-item-closed"
onClick={() => this.props.showFolderChildren(this.props.id)} onClick={() => this.props.showFolderChildren(this.props.id)}
> >
<InlineSVG className="folder-right" src={folderRightUrl} /> <InlineSVG className="folder-right" src={folderRightUrl} />
</span> </button>
<span <button
className="sidebar__file-item-open" className="sidebar__file-item-open"
onClick={() => this.props.hideFolderChildren(this.props.id)} onClick={() => this.props.hideFolderChildren(this.props.id)}
> >
<InlineSVG className="folder-down" src={folderDownUrl} /> <InlineSVG className="folder-down" src={folderDownUrl} />
</span> </button>
</div> </div>
); );
})()} })()}
<a className="sidebar__file-item-name">{this.props.name}</a> <button className="sidebar__file-item-name" onClick={this.handleFileClick}>{this.props.name}</button>
<input <input
type="text" type="text"
className="sidebar__file-item-input" className="sidebar__file-item-input"
value={this.props.name} value={this.props.name}
onChange={this.handleFileNameChange} onChange={this.handleFileNameChange}
ref="fileNameInput" ref={(element) => { this.fileNameInput = element; }}
onBlur={() => { onBlur={() => {
this.validateFileName(); this.validateFileName();
this.props.hideEditFileName(this.props.id); this.props.hideEditFileName(this.props.id);
@ -128,21 +125,26 @@ export class FileNode extends React.Component {
<button <button
className="sidebar__file-item-show-options" className="sidebar__file-item-show-options"
aria-label="view file options" aria-label="view file options"
ref={`fileOptions-${this.props.id}`} ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }}
tabIndex="0" tabIndex="0"
onClick={this.toggleFileOptions} onClick={this.toggleFileOptions}
onBlur={() => setTimeout(() => this.props.hideFileOptions(this.props.id), 200)}
> >
<InlineSVG src={downArrowUrl} /> <InlineSVG src={downArrowUrl} />
</button> </button>
<div ref="fileOptions" className="sidebar__file-item-options"> <div className="sidebar__file-item-options">
<ul title="file options"> <ul title="file options">
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
if (this.props.fileType === 'folder') { if (this.props.fileType === 'folder') {
return ( return (
<li> <li>
<a aria-label="add file" onClick={this.props.newFile} > <button
aria-label="add file"
onClick={this.props.newFile}
className="sidebar__file-item-option"
>
Add File Add File
</a> </button>
</li> </li>
); );
} }
@ -151,26 +153,31 @@ export class FileNode extends React.Component {
if (this.props.fileType === 'folder') { if (this.props.fileType === 'folder') {
return ( return (
<li> <li>
<a aria-label="add folder" onClick={this.props.newFolder} > <button
aria-label="add folder"
onClick={this.props.newFolder}
className="sidebar__file-item-option"
>
Add Folder Add Folder
</a> </button>
</li> </li>
); );
} }
})()} })()}
<li> <li>
<a <button
onClick={() => { onClick={() => {
this.originalFileName = this.props.name; this.originalFileName = this.props.name;
this.props.showEditFileName(this.props.id); this.props.showEditFileName(this.props.id);
setTimeout(() => this.refs.fileNameInput.focus(), 0); setTimeout(() => this.fileNameInput.focus(), 0);
}} }}
className="sidebar__file-item-option"
> >
Rename Rename
</a> </button>
</li> </li>
<li> <li>
<a <button
onClick={() => { onClick={() => {
if (window.confirm(`Are you sure you want to delete ${this.props.name}?`)) { if (window.confirm(`Are you sure you want to delete ${this.props.name}?`)) {
this.isDeleting = true; this.isDeleting = true;
@ -178,9 +185,10 @@ export class FileNode extends React.Component {
setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100); setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100);
} }
}} }}
className="sidebar__file-item-option"
> >
Delete Delete
</a> </button>
</li> </li>
</ul> </ul>
</div> </div>
@ -205,7 +213,7 @@ export class FileNode extends React.Component {
FileNode.propTypes = { FileNode.propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
parentId: PropTypes.string, parentId: PropTypes.string,
children: PropTypes.array, children: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
fileType: PropTypes.string.isRequired, fileType: PropTypes.string.isRequired,
isSelectedFile: PropTypes.bool, isSelectedFile: PropTypes.bool,
@ -226,9 +234,22 @@ FileNode.propTypes = {
hideFolderChildren: PropTypes.func.isRequired hideFolderChildren: PropTypes.func.isRequired
}; };
FileNode.defaultProps = {
id: '1',
name: 'test',
fileType: 'file',
children: [],
parentId: '0',
isSelectedFile: false,
isOptionsOpen: false,
isEditingName: false,
isFolderClosed: false
};
function mapStateToProps(state, ownProps) { function mapStateToProps(state, ownProps) {
// this is a hack, state is updated before ownProps // this is a hack, state is updated before ownProps
return state.files.find((file) => file.id === ownProps.id) || { ...ownProps, name: 'test', fileType: 'file' }; return state.files.find(file => file.id === ownProps.id) || { ...ownProps, name: 'test', fileType: 'file' };
// return state.files.find(file => file.id === ownProps.id);
} }
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {

View file

@ -1,9 +1,10 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import Dropzone from 'dropzone'; import Dropzone from 'dropzone';
const s3Bucket = `https://s3-us-west-2.amazonaws.com/${process.env.S3_BUCKET}/`;
import * as UploaderActions from '../actions/uploader';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as UploaderActions from '../actions/uploader';
const s3Bucket = `https://s3-us-west-2.amazonaws.com/${process.env.S3_BUCKET}/`;
class FileUploader extends React.Component { class FileUploader extends React.Component {
componentDidMount() { componentDidMount() {

View file

@ -1,5 +1,6 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg'); const exitUrl = require('../../../images/exit.svg');
class KeyboardShortcutModal extends React.Component { class KeyboardShortcutModal extends React.Component {

View file

@ -1,11 +1,12 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
import NewFileForm from './NewFileForm';
import classNames from 'classnames'; import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
import NewFileForm from './NewFileForm';
import FileUploader from './FileUploader';
const exitUrl = require('../../../images/exit.svg'); const exitUrl = require('../../../images/exit.svg');
import FileUploader from './FileUploader';
// At some point this will probably be generalized to a generic modal // At some point this will probably be generalized to a generic modal
// in which you can insert different content // in which you can insert different content
@ -21,16 +22,16 @@ class NewFileModal extends React.Component {
} }
focusOnModal() { focusOnModal() {
this.refs.modal.focus(); this.modal.focus();
} }
render() { render() {
const modalClass = classNames({ const modalClass = classNames({
modal: true, 'modal': true,
'modal--reduced': !this.props.canUploadMedia 'modal--reduced': !this.props.canUploadMedia
}); });
return ( return (
<section className={modalClass} tabIndex="0" ref="modal"> <section className={modalClass} tabIndex="0" ref={(element) => { this.modal = element; }}>
<div className="modal-content"> <div className="modal-content">
<div className="modal__header"> <div className="modal__header">
<h2 className="modal__title">Add File</h2> <h2 className="modal__title">Add File</h2>

View file

@ -8,7 +8,7 @@ class NewFolderForm extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.refs.fileName.focus(); this.fileName.focus();
} }
render() { render() {
@ -27,7 +27,7 @@ class NewFolderForm extends React.Component {
id="name" id="name"
type="text" type="text"
placeholder="Name" placeholder="Name"
ref="fileName" ref={(element) => { this.fileName = element; }}
{...domOnlyProps(name)} {...domOnlyProps(name)}
/> />
<input type="submit" value="Add Folder" aria-label="add folder" /> <input type="submit" value="Add Folder" aria-label="add folder" />

View file

@ -1,17 +1,18 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
import NewFolderForm from './NewFolderForm'; import NewFolderForm from './NewFolderForm';
const exitUrl = require('../../../images/exit.svg');
class NewFolderModal extends React.Component { class NewFolderModal extends React.Component {
componentDidMount() { componentDidMount() {
this.refs.modal.focus(); this.newFolderModal.focus();
} }
render() { render() {
return ( return (
<section className="modal" ref="modal" tabIndex="0"> <section className="modal" ref={(element) => { this.newFolderModal = element; }} tabIndex="0">
<div className="modal-content-folder"> <div className="modal-content-folder">
<div className="modal__header"> <div className="modal__header">
<h2 className="modal__title">Add Folder</h2> <h2 className="modal__title">Add Folder</h2>

View file

@ -49,7 +49,7 @@ class Preferences extends React.Component {
render() { render() {
const beep = new Audio(beepUrl); const beep = new Audio(beepUrl);
const preferencesContainerClass = classNames({ const preferencesContainerClass = classNames({
preferences: true, 'preferences': true,
'preferences--selected': this.props.isVisible 'preferences--selected': this.props.isVisible
}); });
@ -79,15 +79,14 @@ class Preferences extends React.Component {
</button> </button>
<input <input
className="preference__value" className="preference__value"
aria-live="status"
aria-live="polite" aria-live="polite"
aria-atomic="true" aria-atomic="true"
role="status" role="status"
value={this.props.fontSize} value={this.props.fontSize}
onChange={this.handleUpdateFont} onChange={this.handleUpdateFont}
ref="fontSizeInput" ref={(element) => { this.fontSizeInput = element; }}
onClick={() => { onClick={() => {
this.refs.fontSizeInput.select(); this.fontSizeInput.select();
}} }}
> >
</input> </input>
@ -113,15 +112,14 @@ class Preferences extends React.Component {
</button> </button>
<input <input
className="preference__value" className="preference__value"
aria-live="status"
aria-live="polite" aria-live="polite"
aria-atomic="true" aria-atomic="true"
role="status" role="status"
value={this.props.indentationAmount} value={this.props.indentationAmount}
onChange={this.handleUpdateIndentation} onChange={this.handleUpdateIndentation}
ref="indentationInput" ref={(element) => { this.indentationInput = element; }}
onClick={() => { onClick={() => {
this.refs.indentationInput.select(); this.indentationInput.select();
}} }}
> >
</input> </input>
@ -248,13 +246,13 @@ class Preferences extends React.Component {
checked={!this.props.lintWarning} checked={!this.props.lintWarning}
/> />
<label htmlFor="lint-warning-off" className="preference__option">Off</label> <label htmlFor="lint-warning-off" className="preference__option">Off</label>
<div <button
className="preference__preview-button" className="preference__preview-button"
onClick={() => beep.play()} onClick={() => beep.play()}
aria-label="preview sound" aria-label="preview sound"
> >
Preview sound Preview sound
</div> </button>
</div> </div>
</div> </div>
<div className="preference"> <div className="preference">
@ -303,7 +301,7 @@ class Preferences extends React.Component {
id="text-output-off" id="text-output-off"
className="preference__radio-button" className="preference__radio-button"
value="Off" value="Off"
checked={!Boolean(this.props.textOutput)} checked={!(this.props.textOutput)}
/> />
<label htmlFor="text-output-off" className="preference__option preference__canvas">Off</label> <label htmlFor="text-output-off" className="preference__option preference__canvas">Off</label>

View file

@ -86,9 +86,8 @@ class PreviewFrame extends React.Component {
this.renderFrameContents(); this.renderFrameContents();
} }
if (this.props.dispatchConsoleEvent) {
window.addEventListener('message', (messageEvent) => { window.addEventListener('message', (messageEvent) => {
messageEvent.data.forEach(message => { messageEvent.data.forEach((message) => {
const args = message.arguments; const args = message.arguments;
Object.keys(args).forEach((key) => { Object.keys(args).forEach((key) => {
if (args[key].includes('Exiting potential infinite loop')) { if (args[key].includes('Exiting potential infinite loop')) {
@ -100,7 +99,6 @@ class PreviewFrame extends React.Component {
this.props.dispatchConsoleEvent(messageEvent.data); this.props.dispatchConsoleEvent(messageEvent.data);
}); });
} }
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
// if sketch starts or stops playing, want to rerender // if sketch starts or stops playing, want to rerender
@ -128,7 +126,6 @@ class PreviewFrame extends React.Component {
if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) { if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) {
this.renderSketch(); this.renderSketch();
return;
} }
// small bug - if autorefresh is on, and the usr changes files // small bug - if autorefresh is on, and the usr changes files
@ -136,11 +133,11 @@ class PreviewFrame extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(this).contentDocument.body); ReactDOM.unmountComponentAtNode(this.iframeElement.contentDocument.body);
} }
clearPreview() { clearPreview() {
const doc = ReactDOM.findDOMNode(this); const doc = this.iframeElement;
doc.srcDoc = ''; doc.srcDoc = '';
} }
@ -204,7 +201,7 @@ class PreviewFrame extends React.Component {
scriptsToInject = scriptsToInject.concat(interceptorScripts); scriptsToInject = scriptsToInject.concat(interceptorScripts);
} }
scriptsToInject.forEach(scriptToInject => { scriptsToInject.forEach((scriptToInject) => {
const script = sketchDoc.createElement('script'); const script = sketchDoc.createElement('script');
script.src = scriptToInject; script.src = scriptToInject;
sketchDoc.head.appendChild(script); sketchDoc.head.appendChild(script);
@ -223,7 +220,7 @@ class PreviewFrame extends React.Component {
resolvePathsForElementsWithAttribute(attr, sketchDoc, files) { resolvePathsForElementsWithAttribute(attr, sketchDoc, files) {
const elements = sketchDoc.querySelectorAll(`[${attr}]`); const elements = sketchDoc.querySelectorAll(`[${attr}]`);
const elementsArray = Array.prototype.slice.call(elements); const elementsArray = Array.prototype.slice.call(elements);
elementsArray.forEach(element => { elementsArray.forEach((element) => {
if (element.getAttribute(attr).match(MEDIA_FILE_REGEX_NO_QUOTES)) { if (element.getAttribute(attr).match(MEDIA_FILE_REGEX_NO_QUOTES)) {
const resolvedFile = resolvePathToFile(element.getAttribute(attr), files); const resolvedFile = resolvePathToFile(element.getAttribute(attr), files);
if (resolvedFile) { if (resolvedFile) {
@ -235,7 +232,7 @@ class PreviewFrame extends React.Component {
resolveJSAndCSSLinks(files) { resolveJSAndCSSLinks(files) {
const newFiles = []; const newFiles = [];
files.forEach(file => { files.forEach((file) => {
const newFile = { ...file }; const newFile = { ...file };
if (file.name.match(/.*\.js$/i)) { if (file.name.match(/.*\.js$/i)) {
newFile.content = this.resolveJSLinksInString(newFile.content, files); newFile.content = this.resolveJSLinksInString(newFile.content, files);
@ -251,7 +248,7 @@ class PreviewFrame extends React.Component {
let newContent = content; let newContent = content;
let jsFileStrings = content.match(STRING_REGEX); let jsFileStrings = content.match(STRING_REGEX);
jsFileStrings = jsFileStrings || []; jsFileStrings = jsFileStrings || [];
jsFileStrings.forEach(jsFileString => { jsFileStrings.forEach((jsFileString) => {
if (jsFileString.match(MEDIA_FILE_REGEX)) { if (jsFileString.match(MEDIA_FILE_REGEX)) {
const filePath = jsFileString.substr(1, jsFileString.length - 2); const filePath = jsFileString.substr(1, jsFileString.length - 2);
const resolvedFile = resolvePathToFile(filePath, files); const resolvedFile = resolvePathToFile(filePath, files);
@ -275,7 +272,7 @@ class PreviewFrame extends React.Component {
let newContent = content; let newContent = content;
let cssFileStrings = content.match(STRING_REGEX); let cssFileStrings = content.match(STRING_REGEX);
cssFileStrings = cssFileStrings || []; cssFileStrings = cssFileStrings || [];
cssFileStrings.forEach(cssFileString => { cssFileStrings.forEach((cssFileString) => {
if (cssFileString.match(MEDIA_FILE_REGEX)) { if (cssFileString.match(MEDIA_FILE_REGEX)) {
const filePath = cssFileString.substr(1, cssFileString.length - 2); const filePath = cssFileString.substr(1, cssFileString.length - 2);
const resolvedFile = resolvePathToFile(filePath, files); const resolvedFile = resolvePathToFile(filePath, files);
@ -292,7 +289,7 @@ class PreviewFrame extends React.Component {
resolveScripts(sketchDoc, files) { resolveScripts(sketchDoc, files) {
const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTML = sketchDoc.getElementsByTagName('script');
const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML);
scriptsInHTMLArray.forEach(script => { scriptsInHTMLArray.forEach((script) => {
if (script.getAttribute('src') && script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null) { if (script.getAttribute('src') && script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null) {
const resolvedFile = resolvePathToFile(script.getAttribute('src'), files); const resolvedFile = resolvePathToFile(script.getAttribute('src'), files);
if (resolvedFile) { if (resolvedFile) {
@ -313,13 +310,13 @@ class PreviewFrame extends React.Component {
resolveStyles(sketchDoc, files) { resolveStyles(sketchDoc, files) {
const inlineCSSInHTML = sketchDoc.getElementsByTagName('style'); const inlineCSSInHTML = sketchDoc.getElementsByTagName('style');
const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML); const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML);
inlineCSSInHTMLArray.forEach(style => { inlineCSSInHTMLArray.forEach((style) => {
style.innerHTML = this.resolveCSSLinksInString(style.innerHTML, files); // eslint-disable-line style.innerHTML = this.resolveCSSLinksInString(style.innerHTML, files); // eslint-disable-line
}); });
const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]'); const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]');
const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML); const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML);
cssLinksInHTMLArray.forEach(css => { cssLinksInHTMLArray.forEach((css) => {
if (css.getAttribute('href') && css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null) { if (css.getAttribute('href') && css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null) {
const resolvedFile = resolvePathToFile(css.getAttribute('href'), files); const resolvedFile = resolvePathToFile(css.getAttribute('href'), files);
if (resolvedFile) { if (resolvedFile) {
@ -337,7 +334,7 @@ class PreviewFrame extends React.Component {
} }
renderSketch() { renderSketch() {
const doc = ReactDOM.findDOMNode(this); const doc = this.iframeElement;
if (this.props.isPlaying) { if (this.props.isPlaying) {
srcDoc.set(doc, this.injectLocalFiles()); srcDoc.set(doc, this.injectLocalFiles());
if (this.props.endSketchRefresh) { if (this.props.endSketchRefresh) {
@ -350,7 +347,7 @@ class PreviewFrame extends React.Component {
} }
renderFrameContents() { renderFrameContents() {
const doc = ReactDOM.findDOMNode(this).contentDocument; const doc = this.iframeElement.contentDocument;
if (doc.readyState === 'complete') { if (doc.readyState === 'complete') {
this.renderSketch(); this.renderSketch();
} else { } else {
@ -366,8 +363,8 @@ class PreviewFrame extends React.Component {
role="main" role="main"
tabIndex="0" tabIndex="0"
frameBorder="0" frameBorder="0"
ref="iframe"
title="sketch output" title="sketch output"
ref={(element) => { this.iframeElement = element; }}
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms" sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms"
/> />
); );
@ -379,14 +376,16 @@ PreviewFrame.propTypes = {
isTextOutputPlaying: PropTypes.bool.isRequired, isTextOutputPlaying: PropTypes.bool.isRequired,
textOutput: PropTypes.number.isRequired, textOutput: PropTypes.number.isRequired,
setTextOutput: PropTypes.func.isRequired, setTextOutput: PropTypes.func.isRequired,
content: PropTypes.string,
htmlFile: PropTypes.shape({ htmlFile: PropTypes.shape({
content: PropTypes.string.isRequired content: PropTypes.string.isRequired
}), }).isRequired,
files: PropTypes.array.isRequired, files: PropTypes.arrayOf(PropTypes.shape({
dispatchConsoleEvent: PropTypes.func, content: PropTypes.string.isRequired,
children: PropTypes.element, name: PropTypes.string.isRequired,
autorefresh: PropTypes.bool.isRequired, url: PropTypes.string,
id: PropTypes.string.isRequired
})).isRequired,
dispatchConsoleEvent: PropTypes.func.isRequired,
endSketchRefresh: PropTypes.func.isRequired, endSketchRefresh: PropTypes.func.isRequired,
previewIsRefreshing: PropTypes.bool.isRequired, previewIsRefreshing: PropTypes.bool.isRequired,
fullView: PropTypes.bool, fullView: PropTypes.bool,
@ -395,4 +394,8 @@ PreviewFrame.propTypes = {
expandConsole: PropTypes.func.isRequired expandConsole: PropTypes.func.isRequired
}; };
PreviewFrame.defaultProps = {
fullView: false
};
export default PreviewFrame; export default PreviewFrame;

View file

@ -1,15 +1,16 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg'); const exitUrl = require('../../../images/exit.svg');
class ShareModal extends React.Component { class ShareModal extends React.Component {
componentDidMount() { componentDidMount() {
this.refs.shareModal.focus(); this.shareModal.focus();
} }
render() { render() {
const hostname = window.location.origin; const hostname = window.location.origin;
return ( return (
<section className="share-modal" ref="shareModal" tabIndex="0"> <section className="share-modal" ref={(element) => { this.shareModal = element; }} tabIndex="0">
<header className="share-modal__header"> <header className="share-modal__header">
<h2>Share Sketch</h2> <h2>Share Sketch</h2>
<button className="about__exit-button" onClick={this.props.closeShareModal}> <button className="about__exit-button" onClick={this.props.closeShareModal}>
@ -17,26 +18,29 @@ class ShareModal extends React.Component {
</button> </button>
</header> </header>
<div className="share-modal__section"> <div className="share-modal__section">
<label className="share-modal__label">Embed</label> <label className="share-modal__label" htmlFor="share-modal__embed">Embed</label>
<input <input
type="text" type="text"
className="share-modal__input" className="share-modal__input"
id="share-modal__embed"
value={`<iframe src="${hostname}/embed/${this.props.projectId}"></iframe>`} value={`<iframe src="${hostname}/embed/${this.props.projectId}"></iframe>`}
/> />
</div> </div>
<div className="share-modal__section"> <div className="share-modal__section">
<label className="share-modal__label">Fullscreen</label> <label className="share-modal__label" htmlFor="share-modal__fullscreen">Fullscreen</label>
<input <input
type="text" type="text"
className="share-modal__input" className="share-modal__input"
id="share-modal__fullscreen"
value={`${hostname}/full/${this.props.projectId}`} value={`${hostname}/full/${this.props.projectId}`}
/> />
</div> </div>
<div className="share-modal__section"> <div className="share-modal__section">
<label className="share-modal__label">Edit</label> <label className="share-modal__label" htmlFor="share-modal__edit">Edit</label>
<input <input
type="text" type="text"
className="share-modal__input" className="share-modal__input"
id="share-modal__edit"
value={`${hostname}/${this.props.ownerUsername}/sketches/${this.props.projectId}`} value={`${hostname}/${this.props.ownerUsername}/sketches/${this.props.projectId}`}
/> />
</div> </div>
@ -48,7 +52,7 @@ class ShareModal extends React.Component {
ShareModal.propTypes = { ShareModal.propTypes = {
projectId: PropTypes.string.isRequired, projectId: PropTypes.string.isRequired,
closeShareModal: PropTypes.func.isRequired, closeShareModal: PropTypes.func.isRequired,
ownerUsername: PropTypes.string ownerUsername: PropTypes.string.isRequired
}; };
export default ShareModal; export default ShareModal;

View file

@ -1,10 +1,10 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
// import SidebarItem from './SidebarItem'; import ConnectedFileNode from './FileNode';
const folderUrl = require('../../../images/folder.svg'); const folderUrl = require('../../../images/folder.svg');
const downArrowUrl = require('../../../images/down-arrow.svg'); const downArrowUrl = require('../../../images/down-arrow.svg');
import ConnectedFileNode from './FileNode';
class Sidebar extends React.Component { class Sidebar extends React.Component {
constructor(props) { constructor(props) {
@ -22,14 +22,14 @@ class Sidebar extends React.Component {
if (this.props.projectOptionsVisible) { if (this.props.projectOptionsVisible) {
this.props.closeProjectOptions(); this.props.closeProjectOptions();
} else { } else {
this.refs.sidebarOptions.focus(); this.sidebarOptions.focus();
this.props.openProjectOptions(); this.props.openProjectOptions();
} }
} }
render() { render() {
const sidebarClass = classNames({ const sidebarClass = classNames({
sidebar: true, 'sidebar': true,
'sidebar--contracted': !this.props.isExpanded, 'sidebar--contracted': !this.props.isExpanded,
'sidebar--project-options': this.props.projectOptionsVisible 'sidebar--project-options': this.props.projectOptionsVisible
}); });
@ -48,7 +48,7 @@ class Sidebar extends React.Component {
aria-label="add file or folder" aria-label="add file or folder"
className="sidebar__add" className="sidebar__add"
tabIndex="0" tabIndex="0"
ref="sidebarOptions" ref={(element) => { this.sidebarOptions = element; }}
onClick={this.toggleProjectOptions} onClick={this.toggleProjectOptions}
onBlur={() => setTimeout(this.props.closeProjectOptions, 200)} onBlur={() => setTimeout(this.props.closeProjectOptions, 200)}
> >
@ -56,14 +56,14 @@ class Sidebar extends React.Component {
</button> </button>
<ul className="sidebar__project-options"> <ul className="sidebar__project-options">
<li> <li>
<a aria-label="add folder" onClick={this.props.newFolder} > <button aria-label="add folder" onClick={this.props.newFolder} >
Add folder Add folder
</a> </button>
</li> </li>
<li> <li>
<a aria-label="add file" onClick={this.props.newFile} > <button aria-label="add file" onClick={this.props.newFile} >
Add file Add file
</a> </button>
</li> </li>
</ul> </ul>
</div> </div>
@ -75,17 +75,14 @@ class Sidebar extends React.Component {
} }
Sidebar.propTypes = { Sidebar.propTypes = {
files: PropTypes.array.isRequired, files: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
id: PropTypes.string.isRequired
})).isRequired,
setSelectedFile: PropTypes.func.isRequired, setSelectedFile: PropTypes.func.isRequired,
isExpanded: PropTypes.bool.isRequired, isExpanded: PropTypes.bool.isRequired,
projectOptionsVisible: PropTypes.bool.isRequired, projectOptionsVisible: PropTypes.bool.isRequired,
newFile: PropTypes.func.isRequired, newFile: PropTypes.func.isRequired,
showFileOptions: PropTypes.func.isRequired,
hideFileOptions: PropTypes.func.isRequired,
deleteFile: PropTypes.func.isRequired,
showEditFileName: PropTypes.func.isRequired,
hideEditFileName: PropTypes.func.isRequired,
updateFileName: PropTypes.func.isRequired,
openProjectOptions: PropTypes.func.isRequired, openProjectOptions: PropTypes.func.isRequired,
closeProjectOptions: PropTypes.func.isRequired, closeProjectOptions: PropTypes.func.isRequired,
newFolder: PropTypes.func.isRequired newFolder: PropTypes.func.isRequired

View file

@ -3,10 +3,11 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import moment from 'moment'; import moment from 'moment';
import { Link, browserHistory } from 'react-router'; import { Link, browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import * as SketchActions from '../actions/projects'; import * as SketchActions from '../actions/projects';
import * as ProjectActions from '../actions/project'; import * as ProjectActions from '../actions/project';
import * as ToastActions from '../actions/toast'; import * as ToastActions from '../actions/toast';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg'); const exitUrl = require('../../../images/exit.svg');
const trashCan = require('../../../images/trash-can.svg'); const trashCan = require('../../../images/trash-can.svg');
@ -47,10 +48,11 @@ class SketchList extends React.Component {
</thead> </thead>
<tbody> <tbody>
{this.props.sketches.map(sketch => {this.props.sketches.map(sketch =>
// eslint-disable-next-line
<tr <tr
className="sketches-table__row visibility-toggle" className="sketches-table__row visibility-toggle"
key={sketch.id} key={sketch.id}
onClick={() => browserHistory.push(`/${username}/sketches/${sketch._id}`)} onClick={() => browserHistory.push(`/${username}/sketches/${sketch.id}`)}
> >
<td className="sketch-list__trash-column"> <td className="sketch-list__trash-column">
{(() => { // eslint-disable-line {(() => { // eslint-disable-line
@ -71,7 +73,7 @@ class SketchList extends React.Component {
} }
})()} })()}
</td> </td>
<td scope="row"><Link to={`/${username}/sketches/${sketch._id}`}>{sketch.name}</Link></td> <th scope="row"><Link to={`/${username}/sketches/${sketch.id}`}>{sketch.name}</Link></th>
<td>{moment(sketch.createdAt).format('MMM D, YYYY h:mm A')}</td> <td>{moment(sketch.createdAt).format('MMM D, YYYY h:mm A')}</td>
<td>{moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')}</td> <td>{moment(sketch.updatedAt).format('MMM D, YYYY h:mm A')}</td>
</tr> </tr>
@ -85,14 +87,23 @@ class SketchList extends React.Component {
} }
SketchList.propTypes = { SketchList.propTypes = {
user: PropTypes.object.isRequired, user: PropTypes.shape({
username: PropTypes.string
}).isRequired,
getProjects: PropTypes.func.isRequired, getProjects: PropTypes.func.isRequired,
sketches: PropTypes.array.isRequired, sketches: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired
})).isRequired,
username: PropTypes.string, username: PropTypes.string,
deleteProject: PropTypes.func.isRequired, deleteProject: PropTypes.func.isRequired,
previousPath: PropTypes.string.isRequired, previousPath: PropTypes.string.isRequired,
showToast: PropTypes.func.isRequired, };
setToastText: PropTypes.func.isRequired
SketchList.defaultProps = {
username: undefined
}; };
function mapStateToProps(state) { function mapStateToProps(state) {

View file

@ -2,14 +2,14 @@ import React from 'react';
class TextOutput extends React.Component { class TextOutput extends React.Component {
componentDidMount() { componentDidMount() {
this.refs.canvasTextOutput.focus(); this.canvasTextOutput.focus();
} }
render() { render() {
return ( return (
<section <section
className="text-output" className="text-output"
id="canvas-sub" id="canvas-sub"
ref="canvasTextOutput" ref={(element) => { this.canvasTextOutput = element; }}
tabIndex="0" tabIndex="0"
aria-label="text-output" aria-label="text-output"
title="canvas text output" title="canvas text output"

View file

@ -49,4 +49,8 @@ Timer.propTypes = {
isUserOwner: PropTypes.bool isUserOwner: PropTypes.bool
}; };
Timer.defaultProps = {
isUserOwner: false
};
export default Timer; export default Timer;

View file

@ -2,9 +2,10 @@ import React, { PropTypes } from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg');
import * as ToastActions from '../actions/toast'; import * as ToastActions from '../actions/toast';
const exitUrl = require('../../../images/exit.svg');
function Toast(props) { function Toast(props) {
return ( return (
<section className="toast"> <section className="toast">

View file

@ -1,12 +1,13 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
const InlineSVG = require('react-inlinesvg'); import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg';
const playUrl = require('../../../images/play.svg'); const playUrl = require('../../../images/play.svg');
const logoUrl = require('../../../images/p5js-logo.svg'); const logoUrl = require('../../../images/p5js-logo.svg');
const stopUrl = require('../../../images/stop.svg'); const stopUrl = require('../../../images/stop.svg');
const preferencesUrl = require('../../../images/preferences.svg'); const preferencesUrl = require('../../../images/preferences.svg');
const editProjectNameUrl = require('../../../images/pencil.svg'); const editProjectNameUrl = require('../../../images/pencil.svg');
import classNames from 'classnames';
class Toolbar extends React.Component { class Toolbar extends React.Component {
constructor(props) { constructor(props) {
@ -38,19 +39,19 @@ class Toolbar extends React.Component {
} }
render() { render() {
let playButtonClass = classNames({ const playButtonClass = classNames({
'toolbar__play-button': true, 'toolbar__play-button': true,
'toolbar__play-button--selected': this.props.isPlaying 'toolbar__play-button--selected': this.props.isPlaying
}); });
let stopButtonClass = classNames({ const stopButtonClass = classNames({
'toolbar__stop-button': true, 'toolbar__stop-button': true,
'toolbar__stop-button--selected': !this.props.isPlaying 'toolbar__stop-button--selected': !this.props.isPlaying
}); });
let preferencesButtonClass = classNames({ const preferencesButtonClass = classNames({
'toolbar__preferences-button': true, 'toolbar__preferences-button': true,
'toolbar__preferences-button--selected': this.props.preferencesIsVisible 'toolbar__preferences-button--selected': this.props.preferencesIsVisible
}); });
let nameContainerClass = classNames({ const nameContainerClass = classNames({
'toolbar__project-name-container': true, 'toolbar__project-name-container': true,
'toolbar__project-name-container--editing': this.props.project.isEditingName 'toolbar__project-name-container--editing': this.props.project.isEditingName
}); });
@ -110,7 +111,7 @@ class Toolbar extends React.Component {
e.preventDefault(); e.preventDefault();
this.originalProjectName = this.props.project.name; this.originalProjectName = this.props.project.name;
this.props.showEditProjectName(); this.props.showEditProjectName();
setTimeout(() => this.refs.projectNameInput.focus(), 0); setTimeout(() => this.projectNameInput.focus(), 0);
} }
}} }}
> >
@ -122,7 +123,7 @@ class Toolbar extends React.Component {
className="toolbar__project-name-input" className="toolbar__project-name-input"
value={this.props.project.name} value={this.props.project.name}
onChange={this.handleProjectNameChange} onChange={this.handleProjectNameChange}
ref="projectNameInput" ref={(element) => { this.projectNameInput = element; }}
onBlur={() => { onBlur={() => {
this.validateProjectName(); this.validateProjectName();
this.props.hideEditProjectName(); this.props.hideEditProjectName();
@ -157,7 +158,6 @@ class Toolbar extends React.Component {
Toolbar.propTypes = { Toolbar.propTypes = {
isPlaying: PropTypes.bool.isRequired, isPlaying: PropTypes.bool.isRequired,
preferencesIsVisible: PropTypes.bool.isRequired, preferencesIsVisible: PropTypes.bool.isRequired,
startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired,
startTextOutput: PropTypes.func.isRequired, startTextOutput: PropTypes.func.isRequired,
stopTextOutput: PropTypes.func.isRequired, stopTextOutput: PropTypes.func.isRequired,
@ -182,4 +182,9 @@ Toolbar.propTypes = {
clearConsole: PropTypes.func.isRequired clearConsole: PropTypes.func.isRequired
}; };
Toolbar.defaultProps = {
owner: undefined,
currentUser: undefined
};
export default Toolbar; export default Toolbar;

View file

@ -38,18 +38,34 @@ class FullView extends React.Component {
FullView.propTypes = { FullView.propTypes = {
params: PropTypes.shape({ params: PropTypes.shape({
project_id: PropTypes.string project_id: PropTypes.string
}), }).isRequired,
project: PropTypes.shape({ project: PropTypes.shape({
name: PropTypes.string, name: PropTypes.string,
owner: PropTypes.shape({ owner: PropTypes.shape({
username: PropTypes.string username: PropTypes.string
}) })
}).isRequired, }).isRequired,
htmlFile: PropTypes.object, htmlFile: PropTypes.shape({
jsFiles: PropTypes.array, id: PropTypes.string.isRequired,
cssFiles: PropTypes.array, content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
}).isRequired,
jsFiles: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
})).isRequired,
cssFiles: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
})).isRequired,
getProject: PropTypes.func.isRequired, getProject: PropTypes.func.isRequired,
files: PropTypes.array files: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired
})).isRequired,
}; };
function mapStateToProps(state) { function mapStateToProps(state) {

View file

@ -1,4 +1,8 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import SplitPane from 'react-split-pane';
import Editor from '../components/Editor'; import Editor from '../components/Editor';
import Sidebar from '../components/Sidebar'; import Sidebar from '../components/Sidebar';
import PreviewFrame from '../components/PreviewFrame'; import PreviewFrame from '../components/PreviewFrame';
@ -13,9 +17,6 @@ import ErrorModal from '../components/ErrorModal';
import Nav from '../../../components/Nav'; import Nav from '../../../components/Nav';
import Console from '../components/Console'; import Console from '../components/Console';
import Toast from '../components/Toast'; import Toast from '../components/Toast';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import * as FileActions from '../actions/files'; import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide'; import * as IDEActions from '../actions/ide';
import * as ProjectActions from '../actions/project'; import * as ProjectActions from '../actions/project';
@ -25,7 +26,6 @@ import * as UserActions from '../../User/actions';
import * as ToastActions from '../actions/toast'; import * as ToastActions from '../actions/toast';
import * as ConsoleActions from '../actions/console'; import * as ConsoleActions from '../actions/console';
import { getHTMLFile } from '../reducers/files'; import { getHTMLFile } from '../reducers/files';
import SplitPane from 'react-split-pane';
import Overlay from '../../App/components/Overlay'; import Overlay from '../../App/components/Overlay';
import SketchList from '../components/SketchList'; import SketchList from '../components/SketchList';
import About from '../components/About'; import About from '../components/About';
@ -55,7 +55,7 @@ class IDEView extends React.Component {
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1; this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
document.addEventListener('keydown', this.handleGlobalKeydown, false); document.addEventListener('keydown', this.handleGlobalKeydown, false);
this.props.router.setRouteLeaveHook(this.props.route, (route) => this.warnIfUnsavedChanges(route)); this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route));
window.onbeforeunload = () => this.warnIfUnsavedChanges(); window.onbeforeunload = () => this.warnIfUnsavedChanges();
@ -104,7 +104,7 @@ class IDEView extends React.Component {
} }
if (this.props.route.path !== prevProps.route.path) { if (this.props.route.path !== prevProps.route.path) {
this.props.router.setRouteLeaveHook(this.props.route, (route) => this.warnIfUnsavedChanges(route)); this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route));
} }
} }
@ -120,16 +120,16 @@ class IDEView extends React.Component {
} }
_handleConsolePaneOnDragFinished() { _handleConsolePaneOnDragFinished() {
this.consoleSize = this.refs.consolePane.state.draggedSize; this.consoleSize = this.consolePane.state.draggedSize;
this.refs.consolePane.setState({ this.consolePane.setState({
resized: false, resized: false,
draggedSize: undefined, draggedSize: undefined,
}); });
} }
_handleSidebarPaneOnDragFinished() { _handleSidebarPaneOnDragFinished() {
this.sidebarSize = this.refs.sidebarPane.state.draggedSize; this.sidebarSize = this.sidebarPane.state.draggedSize;
this.refs.sidebarPane.setState({ this.sidebarPane.setState({
resized: false, resized: false,
draggedSize: undefined draggedSize: undefined
}); });
@ -140,7 +140,7 @@ class IDEView extends React.Component {
if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) { if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (this.isUserOwner() || this.props.user.authenticated && !this.props.project.owner) { if (this.isUserOwner() || (this.props.user.authenticated && !this.props.project.owner)) {
this.props.saveProject(); this.props.saveProject();
} }
// 13 === enter // 13 === enter
@ -170,7 +170,7 @@ class IDEView extends React.Component {
warnIfUnsavedChanges(route) { // eslint-disable-line warnIfUnsavedChanges(route) { // eslint-disable-line
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) { if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
// don't warn // don't warn
} else if (route && this.props.location.pathname === '/login' || this.props.location.pathname === '/signup') { } else if (route && (this.props.location.pathname === '/login' || this.props.location.pathname === '/signup')) {
// don't warn // don't warn
} else if (this.props.ide.unsavedChanges) { } else if (this.props.ide.unsavedChanges) {
if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) { if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) {
@ -202,7 +202,6 @@ class IDEView extends React.Component {
<Toolbar <Toolbar
className="Toolbar" className="Toolbar"
isPlaying={this.props.ide.isPlaying} isPlaying={this.props.ide.isPlaying}
startSketch={this.props.startSketch}
stopSketch={this.props.stopSketch} stopSketch={this.props.stopSketch}
startTextOutput={this.props.startTextOutput} startTextOutput={this.props.startTextOutput}
stopTextOutput={this.props.stopTextOutput} stopTextOutput={this.props.stopTextOutput}
@ -246,7 +245,7 @@ class IDEView extends React.Component {
<SplitPane <SplitPane
split="vertical" split="vertical"
defaultSize={this.sidebarSize} defaultSize={this.sidebarSize}
ref="sidebarPane" ref={(element) => { this.sidebarPane = element; }}
onDragFinished={this._handleSidebarPaneOnDragFinished} onDragFinished={this._handleSidebarPaneOnDragFinished}
allowResize={this.props.ide.sidebarIsExpanded} allowResize={this.props.ide.sidebarIsExpanded}
minSize={20} minSize={20}
@ -270,15 +269,15 @@ class IDEView extends React.Component {
<SplitPane <SplitPane
split="vertical" split="vertical"
defaultSize={'50%'} defaultSize={'50%'}
onChange={() => (this.refs.overlay.style.display = 'block')} onChange={() => (this.overlay.style.display = 'block')}
onDragFinished={() => (this.refs.overlay.style.display = 'none')} onDragFinished={() => (this.overlay.style.display = 'none')}
> >
<SplitPane <SplitPane
split="horizontal" split="horizontal"
primary="second" primary="second"
defaultSize={this.consoleSize} defaultSize={this.consoleSize}
minSize={29} minSize={29}
ref="consolePane" ref={(element) => { this.consolePane = element; }}
onDragFinished={this._handleConsolePaneOnDragFinished} onDragFinished={this._handleConsolePaneOnDragFinished}
allowResize={this.props.ide.consoleIsExpanded} allowResize={this.props.ide.consoleIsExpanded}
className="editor-preview-subpanel" className="editor-preview-subpanel"
@ -286,7 +285,6 @@ class IDEView extends React.Component {
<Editor <Editor
lintWarning={this.props.preferences.lintWarning} lintWarning={this.props.preferences.lintWarning}
lintMessages={this.props.editorAccessibility.lintMessages} lintMessages={this.props.editorAccessibility.lintMessages}
updateLineNumber={this.props.updateLineNumber}
updateLintMessage={this.props.updateLintMessage} updateLintMessage={this.props.updateLintMessage}
clearLintMessage={this.props.clearLintMessage} clearLintMessage={this.props.clearLintMessage}
file={this.props.selectedFile} file={this.props.selectedFile}
@ -295,8 +293,6 @@ class IDEView extends React.Component {
indentationAmount={this.props.preferences.indentationAmount} indentationAmount={this.props.preferences.indentationAmount}
isTabIndent={this.props.preferences.isTabIndent} isTabIndent={this.props.preferences.isTabIndent}
files={this.props.files} files={this.props.files}
lintMessages={this.props.editorAccessibility.lintMessages}
lineNumber={this.props.editorAccessibility.lineNumber}
editorOptionsVisible={this.props.ide.editorOptionsVisible} editorOptionsVisible={this.props.ide.editorOptionsVisible}
showEditorOptions={this.props.showEditorOptions} showEditorOptions={this.props.showEditorOptions}
closeEditorOptions={this.props.closeEditorOptions} closeEditorOptions={this.props.closeEditorOptions}
@ -317,7 +313,6 @@ class IDEView extends React.Component {
/> />
<Console <Console
consoleEvents={this.props.console} consoleEvents={this.props.console}
isPlaying={this.props.ide.isPlaying}
isExpanded={this.props.ide.consoleIsExpanded} isExpanded={this.props.ide.consoleIsExpanded}
expandConsole={this.props.expandConsole} expandConsole={this.props.expandConsole}
collapseConsole={this.props.collapseConsole} collapseConsole={this.props.collapseConsole}
@ -325,7 +320,7 @@ class IDEView extends React.Component {
/> />
</SplitPane> </SplitPane>
<div className="preview-frame-holder"> <div className="preview-frame-holder">
<div className="preview-frame-overlay" ref="overlay"> <div className="preview-frame-overlay" ref={(element) => { this.overlay = element; }}>
</div> </div>
<div> <div>
{(() => { {(() => {
@ -351,7 +346,6 @@ class IDEView extends React.Component {
endSketchRefresh={this.props.endSketchRefresh} endSketchRefresh={this.props.endSketchRefresh}
stopSketch={this.props.stopSketch} stopSketch={this.props.stopSketch}
setBlobUrl={this.props.setBlobUrl} setBlobUrl={this.props.setBlobUrl}
stopSketch={this.props.stopSketch}
expandConsole={this.props.expandConsole} expandConsole={this.props.expandConsole}
/> />
</div> </div>
@ -449,10 +443,10 @@ IDEView.propTypes = {
project_id: PropTypes.string, project_id: PropTypes.string,
username: PropTypes.string, username: PropTypes.string,
reset_password_token: PropTypes.string, reset_password_token: PropTypes.string,
}), }).isRequired,
location: PropTypes.shape({ location: PropTypes.shape({
pathname: PropTypes.string pathname: PropTypes.string
}), }).isRequired,
getProject: PropTypes.func.isRequired, getProject: PropTypes.func.isRequired,
user: PropTypes.shape({ user: PropTypes.shape({
authenticated: PropTypes.bool.isRequired, authenticated: PropTypes.bool.isRequired,
@ -483,12 +477,9 @@ IDEView.propTypes = {
justOpenedProject: PropTypes.bool.isRequired, justOpenedProject: PropTypes.bool.isRequired,
errorType: PropTypes.string errorType: PropTypes.string
}).isRequired, }).isRequired,
startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired, stopSketch: PropTypes.func.isRequired,
startTextOutput: PropTypes.func.isRequired, startTextOutput: PropTypes.func.isRequired,
stopTextOutput: PropTypes.func.isRequired, stopTextOutput: PropTypes.func.isRequired,
detectInfiniteLoops: PropTypes.func.isRequired,
resetInfiniteLoops: PropTypes.func.isRequired,
project: PropTypes.shape({ project: PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
@ -501,11 +492,9 @@ IDEView.propTypes = {
openPreferences: PropTypes.func.isRequired, openPreferences: PropTypes.func.isRequired,
editorAccessibility: PropTypes.shape({ editorAccessibility: PropTypes.shape({
lintMessages: PropTypes.array.isRequired, lintMessages: PropTypes.array.isRequired,
lineNumber: PropTypes.string.isRequired
}).isRequired, }).isRequired,
updateLintMessage: PropTypes.func.isRequired, updateLintMessage: PropTypes.func.isRequired,
clearLintMessage: PropTypes.func.isRequired, clearLintMessage: PropTypes.func.isRequired,
updateLineNumber: PropTypes.func.isRequired,
preferences: PropTypes.shape({ preferences: PropTypes.shape({
fontSize: PropTypes.number.isRequired, fontSize: PropTypes.number.isRequired,
indentationAmount: PropTypes.number.isRequired, indentationAmount: PropTypes.number.isRequired,
@ -524,14 +513,22 @@ IDEView.propTypes = {
setAutosave: PropTypes.func.isRequired, setAutosave: PropTypes.func.isRequired,
setLintWarning: PropTypes.func.isRequired, setLintWarning: PropTypes.func.isRequired,
setTextOutput: PropTypes.func.isRequired, setTextOutput: PropTypes.func.isRequired,
files: PropTypes.array.isRequired, files: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
})).isRequired,
updateFileContent: PropTypes.func.isRequired, updateFileContent: PropTypes.func.isRequired,
selectedFile: PropTypes.shape({ selectedFile: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired content: PropTypes.string.isRequired
}), }).isRequired,
setSelectedFile: PropTypes.func.isRequired, setSelectedFile: PropTypes.func.isRequired,
htmlFile: PropTypes.object.isRequired, htmlFile: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
}).isRequired,
dispatchConsoleEvent: PropTypes.func.isRequired, dispatchConsoleEvent: PropTypes.func.isRequired,
newFile: PropTypes.func.isRequired, newFile: PropTypes.func.isRequired,
closeNewFileModal: PropTypes.func.isRequired, closeNewFileModal: PropTypes.func.isRequired,
@ -565,13 +562,11 @@ IDEView.propTypes = {
toast: PropTypes.shape({ toast: PropTypes.shape({
isVisible: PropTypes.bool.isRequired isVisible: PropTypes.bool.isRequired
}).isRequired, }).isRequired,
showToast: PropTypes.func.isRequired,
setToastText: PropTypes.func.isRequired,
autosaveProject: PropTypes.func.isRequired, autosaveProject: PropTypes.func.isRequired,
router: PropTypes.shape({ router: PropTypes.shape({
setRouteLeaveHook: PropTypes.func setRouteLeaveHook: PropTypes.func
}).isRequired, }).isRequired,
route: PropTypes.object.isRequired, route: PropTypes.oneOfType([PropTypes.object, PropTypes.element]).isRequired,
setUnsavedChanges: PropTypes.func.isRequired, setUnsavedChanges: PropTypes.func.isRequired,
setTheme: PropTypes.func.isRequired, setTheme: PropTypes.func.isRequired,
setAutorefresh: PropTypes.func.isRequired, setAutorefresh: PropTypes.func.isRequired,
@ -580,8 +575,10 @@ IDEView.propTypes = {
startRefreshSketch: PropTypes.func.isRequired, startRefreshSketch: PropTypes.func.isRequired,
setBlobUrl: PropTypes.func.isRequired, setBlobUrl: PropTypes.func.isRequired,
setPreviousPath: PropTypes.func.isRequired, setPreviousPath: PropTypes.func.isRequired,
resetProject: PropTypes.func.isRequired, console: PropTypes.arrayOf(PropTypes.shape({
console: PropTypes.array.isRequired, method: PropTypes.string.isRequired,
args: PropTypes.arrayOf(PropTypes.string)
})).isRequired,
clearConsole: PropTypes.func.isRequired, clearConsole: PropTypes.func.isRequired,
showErrorModal: PropTypes.func.isRequired, showErrorModal: PropTypes.func.isRequired,
hideErrorModal: PropTypes.func.isRequired hideErrorModal: PropTypes.func.isRequired

View file

@ -2,11 +2,18 @@ import * as ActionTypes from '../../../constants';
const consoleMax = 200; const consoleMax = 200;
const initialState = []; const initialState = [];
let messageId = 0;
const console = (state = initialState, action) => { const console = (state = initialState, action) => {
let messages;
switch (action.type) { switch (action.type) {
case ActionTypes.CONSOLE_EVENT: case ActionTypes.CONSOLE_EVENT:
return state.concat(action.event).slice(-consoleMax); messages = [...action.event];
messages.forEach((message) => {
message.id = messageId;
messageId += 1;
});
return state.concat(messages).slice(-consoleMax);
case ActionTypes.CLEAR_CONSOLE: case ActionTypes.CLEAR_CONSOLE:
return []; return [];
default: default:

View file

@ -1,21 +1,20 @@
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
const initialState = { const initialState = {
lineNumber: 'line',
lintMessages: [] lintMessages: []
}; };
let messageId = 0;
const editorAccessibility = (state = initialState, action) => { const editorAccessibility = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case ActionTypes.UPDATE_LINT_MESSAGE: case ActionTypes.UPDATE_LINT_MESSAGE:
messageId += 1;
return Object.assign({}, state, { return Object.assign({}, state, {
lintMessages: state.lintMessages.concat( lintMessages: state.lintMessages.concat(
{ severity: action.severity, line: action.line, message: action.message }) { severity: action.severity, line: action.line, message: action.message, id: messageId })
}); });
case ActionTypes.CLEAR_LINT_MESSAGE: case ActionTypes.CLEAR_LINT_MESSAGE:
return Object.assign({}, state, { lintMessages: [] }); return Object.assign({}, state, { lintMessages: [] });
case ActionTypes.UPDATE_LINENUMBER:
return Object.assign({}, state, { lineNumber: `line ${action.lineNumber}` });
default: default:
return state; return state;
} }

View file

@ -1,5 +1,5 @@
import * as ActionTypes from '../../../constants';
import objectID from 'bson-objectid'; import objectID from 'bson-objectid';
import * as ActionTypes from '../../../constants';
const defaultSketch = `function setup() { const defaultSketch = `function setup() {
createCanvas(400, 400); createCanvas(400, 400);
@ -42,7 +42,8 @@ const initialState = () => {
id: r, id: r,
_id: r, _id: r,
children: [a, b, c], children: [a, b, c],
fileType: 'folder' fileType: 'folder',
content: ''
}, },
{ {
name: 'sketch.js', name: 'sketch.js',
@ -92,7 +93,7 @@ function deleteChild(state, parentId, id) {
function deleteMany(state, ids) { function deleteMany(state, ids) {
const newState = [...state]; const newState = [...state];
ids.forEach(id => { ids.forEach((id) => {
let fileIndex; let fileIndex;
newState.find((file, index) => { newState.find((file, index) => {
if (file.id === id) { if (file.id === id) {
@ -111,7 +112,7 @@ const files = (state, action) => {
} }
switch (action.type) { switch (action.type) {
case ActionTypes.UPDATE_FILE_CONTENT: case ActionTypes.UPDATE_FILE_CONTENT:
return state.map(file => { return state.map((file) => {
if (file.name !== action.name) { if (file.name !== action.name) {
return file; return file;
} }
@ -119,7 +120,7 @@ const files = (state, action) => {
return Object.assign({}, file, { content: action.content }); return Object.assign({}, file, { content: action.content });
}); });
case ActionTypes.SET_BLOB_URL: case ActionTypes.SET_BLOB_URL:
return state.map(file => { return state.map((file) => {
if (file.name !== action.name) { if (file.name !== action.name) {
return file; return file;
} }
@ -151,7 +152,7 @@ const files = (state, action) => {
fileType: action.fileType || 'file' }]; fileType: action.fileType || 'file' }];
} }
case ActionTypes.SHOW_FILE_OPTIONS: case ActionTypes.SHOW_FILE_OPTIONS:
return state.map(file => { return state.map((file) => {
if (file.id !== action.id) { if (file.id !== action.id) {
return file; return file;
} }
@ -159,7 +160,7 @@ const files = (state, action) => {
return Object.assign({}, file, { isOptionsOpen: true }); return Object.assign({}, file, { isOptionsOpen: true });
}); });
case ActionTypes.HIDE_FILE_OPTIONS: case ActionTypes.HIDE_FILE_OPTIONS:
return state.map(file => { return state.map((file) => {
if (file.id !== action.id) { if (file.id !== action.id) {
return file; return file;
} }
@ -167,7 +168,7 @@ const files = (state, action) => {
return Object.assign({}, file, { isOptionsOpen: false }); return Object.assign({}, file, { isOptionsOpen: false });
}); });
case ActionTypes.UPDATE_FILE_NAME: case ActionTypes.UPDATE_FILE_NAME:
return state.map(file => { return state.map((file) => {
if (file.id !== action.id) { if (file.id !== action.id) {
return file; return file;
} }
@ -188,7 +189,7 @@ const files = (state, action) => {
// return newState.filter(file => file.id !== action.id); // return newState.filter(file => file.id !== action.id);
} }
case ActionTypes.SHOW_EDIT_FILE_NAME: case ActionTypes.SHOW_EDIT_FILE_NAME:
return state.map(file => { return state.map((file) => {
if (file.id !== action.id) { if (file.id !== action.id) {
return file; return file;
} }
@ -196,7 +197,7 @@ const files = (state, action) => {
return Object.assign({}, file, { isEditingName: true }); return Object.assign({}, file, { isEditingName: true });
}); });
case ActionTypes.HIDE_EDIT_FILE_NAME: case ActionTypes.HIDE_EDIT_FILE_NAME:
return state.map(file => { return state.map((file) => {
if (file.id !== action.id) { if (file.id !== action.id) {
return file; return file;
} }
@ -204,21 +205,21 @@ const files = (state, action) => {
return Object.assign({}, file, { isEditingName: false }); return Object.assign({}, file, { isEditingName: false });
}); });
case ActionTypes.SET_SELECTED_FILE: case ActionTypes.SET_SELECTED_FILE:
return state.map(file => { return state.map((file) => {
if (file.id === action.selectedFile) { if (file.id === action.selectedFile) {
return Object.assign({}, file, { isSelectedFile: true }); return Object.assign({}, file, { isSelectedFile: true });
} }
return Object.assign({}, file, { isSelectedFile: false }); return Object.assign({}, file, { isSelectedFile: false });
}); });
case ActionTypes.SHOW_FOLDER_CHILDREN: case ActionTypes.SHOW_FOLDER_CHILDREN:
return state.map(file => { return state.map((file) => {
if (file.id === action.id) { if (file.id === action.id) {
return Object.assign({}, file, { isFolderClosed: false }); return Object.assign({}, file, { isFolderClosed: false });
} }
return file; return file;
}); });
case ActionTypes.HIDE_FOLDER_CHILDREN: case ActionTypes.HIDE_FOLDER_CHILDREN:
return state.map(file => { return state.map((file) => {
if (file.id === action.id) { if (file.id === action.id) {
return Object.assign({}, file, { isFolderClosed: true }); return Object.assign({}, file, { isFolderClosed: true });
} }
@ -229,9 +230,9 @@ const files = (state, action) => {
} }
}; };
export const getHTMLFile = (state) => state.filter(file => file.name.match(/.*\.html$/i))[0]; export const getHTMLFile = state => state.filter(file => file.name.match(/.*\.html$/i))[0];
export const getJSFiles = (state) => state.filter(file => file.name.match(/.*\.js$/i)); export const getJSFiles = state => state.filter(file => file.name.match(/.*\.js$/i));
export const getCSSFiles = (state) => state.filter(file => file.name.match(/.*\.css$/i)); export const getCSSFiles = state => state.filter(file => file.name.match(/.*\.css$/i));
export const getLinkedFiles = (state) => state.filter(file => file.url); export const getLinkedFiles = state => state.filter(file => file.url);
export default files; export default files;

View file

@ -1,5 +1,5 @@
import * as ActionTypes from '../../../constants';
import generate from 'project-name-generator'; import generate from 'project-name-generator';
import * as ActionTypes from '../../../constants';
const initialState = () => { const initialState = () => {
const generatedString = generate({ words: 2 }).spaced; const generatedString = generate({ words: 2 }).spaced;

View file

@ -5,7 +5,7 @@ const sketches = (state = [], action) => {
case ActionTypes.SET_PROJECTS: case ActionTypes.SET_PROJECTS:
return action.projects; return action.projects;
case ActionTypes.DELETE_PROJECT: case ActionTypes.DELETE_PROJECT:
return state.filter((sketch) => return state.filter(sketch =>
sketch.id !== action.id sketch.id !== action.id
); );
default: default:

View file

@ -1,6 +1,6 @@
import * as ActionTypes from '../../constants';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import axios from 'axios'; import axios from 'axios';
import * as ActionTypes from '../../constants';
import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide';
@ -16,7 +16,7 @@ export function authError(error) {
export function signUpUser(previousPath, formValues) { export function signUpUser(previousPath, formValues) {
return (dispatch) => { return (dispatch) => {
axios.post(`${ROOT_URL}/signup`, formValues, { withCredentials: true }) axios.post(`${ROOT_URL}/signup`, formValues, { withCredentials: true })
.then(response => { .then((response) => {
dispatch({ type: ActionTypes.AUTH_USER, dispatch({ type: ActionTypes.AUTH_USER,
user: response.data user: response.data
}); });
@ -48,7 +48,7 @@ export function loginUserFailure(error) {
export function validateAndLoginUser(previousPath, formProps, dispatch) { export function validateAndLoginUser(previousPath, formProps, dispatch) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
loginUser(formProps) loginUser(formProps)
.then(response => { .then((response) => {
dispatch({ type: ActionTypes.AUTH_USER, dispatch({ type: ActionTypes.AUTH_USER,
user: response.data user: response.data
}); });
@ -60,7 +60,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) {
browserHistory.push(previousPath); browserHistory.push(previousPath);
resolve(); resolve();
}) })
.catch(response => { .catch((response) => {
reject({ password: response.data.message, _error: 'Login failed!' }); reject({ password: response.data.message, _error: 'Login failed!' });
}); });
}); });
@ -69,7 +69,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) {
export function getUser() { export function getUser() {
return (dispatch) => { return (dispatch) => {
axios.get(`${ROOT_URL}/session`, { withCredentials: true }) axios.get(`${ROOT_URL}/session`, { withCredentials: true })
.then(response => { .then((response) => {
dispatch({ dispatch({
type: ActionTypes.AUTH_USER, type: ActionTypes.AUTH_USER,
user: response.data user: response.data
@ -79,7 +79,7 @@ export function getUser() {
preferences: response.data.preferences preferences: response.data.preferences
}); });
}) })
.catch(response => { .catch((response) => {
dispatch(authError(response.data.error)); dispatch(authError(response.data.error));
}); });
}; };
@ -88,13 +88,13 @@ export function getUser() {
export function validateSession() { export function validateSession() {
return (dispatch, getState) => { return (dispatch, getState) => {
axios.get(`${ROOT_URL}/session`, { withCredentials: true }) axios.get(`${ROOT_URL}/session`, { withCredentials: true })
.then(response => { .then((response) => {
const state = getState(); const state = getState();
if (state.user.username !== response.data.username) { if (state.user.username !== response.data.username) {
dispatch(showErrorModal('staleSession')); dispatch(showErrorModal('staleSession'));
} }
}) })
.catch(response => { .catch((response) => {
if (response.status === 404) { if (response.status === 404) {
dispatch(showErrorModal('staleSession')); dispatch(showErrorModal('staleSession'));
} }

View file

@ -40,9 +40,14 @@ LoginForm.propTypes = {
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
validateAndLoginUser: PropTypes.func.isRequired, validateAndLoginUser: PropTypes.func.isRequired,
submitting: PropTypes.bool, submitting: PropTypes.bool,
invalid: PropTypes.bool,
pristine: PropTypes.bool, pristine: PropTypes.bool,
previousPath: PropTypes.string.isRequired previousPath: PropTypes.string.isRequired
}; };
LoginForm.defaultProps = {
submitting: false,
pristine: true,
invalid: false
};
export default LoginForm; export default LoginForm;

View file

@ -44,7 +44,13 @@ NewPasswordForm.propTypes = {
pristine: PropTypes.bool, pristine: PropTypes.bool,
params: PropTypes.shape({ params: PropTypes.shape({
reset_password_token: PropTypes.string, reset_password_token: PropTypes.string,
}), }).isRequired,
};
NewPasswordForm.defaultProps = {
invalid: false,
pristine: true,
submitting: false
}; };
export default NewPasswordForm; export default NewPasswordForm;

View file

@ -31,7 +31,13 @@ ResetPasswordForm.propTypes = {
pristine: PropTypes.bool, pristine: PropTypes.bool,
user: PropTypes.shape({ user: PropTypes.shape({
resetPasswordInitiate: PropTypes.bool resetPasswordInitiate: PropTypes.bool
}) }).isRequired
};
ResetPasswordForm.defaultProps = {
submitting: false,
pristine: true,
invalid: false
}; };
export default ResetPasswordForm; export default ResetPasswordForm;

View file

@ -69,4 +69,10 @@ SignupForm.propTypes = {
previousPath: PropTypes.string.isRequired previousPath: PropTypes.string.isRequired
}; };
SignupForm.defaultProps = {
submitting: false,
pristine: true,
invalid: false
};
export default SignupForm; export default SignupForm;

View file

@ -1,10 +1,10 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
import { Link, browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg';
import { validateAndLoginUser } from '../actions'; import { validateAndLoginUser } from '../actions';
import LoginForm from '../components/LoginForm'; import LoginForm from '../components/LoginForm';
// import GithubButton from '../components/GithubButton'; // import GithubButton from '../components/GithubButton';
import { Link, browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg';
const exitUrl = require('../../../images/exit.svg'); const exitUrl = require('../../../images/exit.svg');
const logoUrl = require('../../../images/p5js-logo.svg'); const logoUrl = require('../../../images/p5js-logo.svg');
@ -41,7 +41,7 @@ class LoginView extends React.Component {
{/* <h2 className="form-container__divider">Or</h2> {/* <h2 className="form-container__divider">Or</h2>
<GithubButton buttonText="Login with Github" /> */} <GithubButton buttonText="Login with Github" /> */}
<p className="form__navigation-options"> <p className="form__navigation-options">
Don't have an account?&nbsp; Don&apos;t have an account?&nbsp;
<Link className="form__signup-button" to="/signup">Sign Up</Link> <Link className="form__signup-button" to="/signup">Sign Up</Link>
</p> </p>
<p className="form__navigation-options"> <p className="form__navigation-options">

View file

@ -1,11 +1,12 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
import NewPasswordForm from '../components/NewPasswordForm';
import * as UserActions from '../actions';
import { bindActionCreators } from 'redux';
import classNames from 'classnames'; import classNames from 'classnames';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
import { bindActionCreators } from 'redux';
import NewPasswordForm from '../components/NewPasswordForm';
import * as UserActions from '../actions';
const exitUrl = require('../../../images/exit.svg'); const exitUrl = require('../../../images/exit.svg');
const logoUrl = require('../../../images/p5js-logo.svg'); const logoUrl = require('../../../images/p5js-logo.svg');
@ -55,11 +56,11 @@ class NewPasswordView extends React.Component {
NewPasswordView.propTypes = { NewPasswordView.propTypes = {
params: PropTypes.shape({ params: PropTypes.shape({
reset_password_token: PropTypes.string, reset_password_token: PropTypes.string,
}), }).isRequired,
validateResetPasswordToken: PropTypes.func.isRequired, validateResetPasswordToken: PropTypes.func.isRequired,
user: PropTypes.shape({ user: PropTypes.shape({
resetPasswordInvalid: PropTypes.bool resetPasswordInvalid: PropTypes.bool
}) }).isRequired
}; };
function validate(formProps) { function validate(formProps) {

View file

@ -1,11 +1,12 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { Link, browserHistory } from 'react-router'; import { Link, browserHistory } from 'react-router';
import * as UserActions from '../actions';
import { bindActionCreators } from 'redux';
import { reduxForm } from 'redux-form';
import ResetPasswordForm from '../components/ResetPasswordForm';
import classNames from 'classnames'; import classNames from 'classnames';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
import { bindActionCreators } from 'redux';
import { reduxForm } from 'redux-form';
import * as UserActions from '../actions';
import ResetPasswordForm from '../components/ResetPasswordForm';
const exitUrl = require('../../../images/exit.svg'); const exitUrl = require('../../../images/exit.svg');
const logoUrl = require('../../../images/p5js-logo.svg'); const logoUrl = require('../../../images/p5js-logo.svg');
@ -43,7 +44,7 @@ class ResetPasswordView extends React.Component {
<h2 className="form-container__title">Reset Your Password</h2> <h2 className="form-container__title">Reset Your Password</h2>
<ResetPasswordForm {...this.props} /> <ResetPasswordForm {...this.props} />
<p className="reset-password__submitted"> <p className="reset-password__submitted">
Your password reset email should arrive shortly. If you don't see it, check Your password reset email should arrive shortly. If you don&apos;t see it, check
in your spam folder as sometimes it can end up there. in your spam folder as sometimes it can end up there.
</p> </p>
<p className="form__navigation-options"> <p className="form__navigation-options">

View file

@ -1,11 +1,12 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import * as UserActions from '../actions';
import { reduxForm } from 'redux-form';
import SignupForm from '../components/SignupForm';
import axios from 'axios'; import axios from 'axios';
import { Link, browserHistory } from 'react-router'; import { Link, browserHistory } from 'react-router';
import InlineSVG from 'react-inlinesvg'; import InlineSVG from 'react-inlinesvg';
import { reduxForm } from 'redux-form';
import * as UserActions from '../actions';
import SignupForm from '../components/SignupForm';
const exitUrl = require('../../../images/exit.svg'); const exitUrl = require('../../../images/exit.svg');
const logoUrl = require('../../../images/p5js-logo.svg'); const logoUrl = require('../../../images/p5js-logo.svg');
@ -66,7 +67,7 @@ function asyncValidate(formProps, dispatch, props) {
queryParams[fieldToValidate] = formProps[fieldToValidate]; queryParams[fieldToValidate] = formProps[fieldToValidate];
queryParams.check_type = fieldToValidate; queryParams.check_type = fieldToValidate;
return axios.get('/api/signup/duplicate_check', { params: queryParams }) return axios.get('/api/signup/duplicate_check', { params: queryParams })
.then(response => { .then((response) => {
if (response.data.exists) { if (response.data.exists) {
const error = {}; const error = {};
error[fieldToValidate] = response.data.message; error[fieldToValidate] = response.data.message;
@ -90,7 +91,7 @@ function validate(formProps) {
if (!formProps.email) { if (!formProps.email) {
errors.email = 'Please enter an email.'; errors.email = 'Please enter an email.';
} else if (!formProps.email.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i)) { } else if (!formProps.email.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i)) {
errors.email = 'Please enter a valid email address.'; errors.email = 'Please enter a valid email address.';
} }

View file

@ -1,4 +1,5 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { reducer as form } from 'redux-form';
import files from './modules/IDE/reducers/files'; import files from './modules/IDE/reducers/files';
import ide from './modules/IDE/reducers/ide'; import ide from './modules/IDE/reducers/ide';
import preferences from './modules/IDE/reducers/preferences'; import preferences from './modules/IDE/reducers/preferences';
@ -8,7 +9,6 @@ import user from './modules/User/reducers';
import sketches from './modules/IDE/reducers/projects'; import sketches from './modules/IDE/reducers/projects';
import toast from './modules/IDE/reducers/toast'; import toast from './modules/IDE/reducers/toast';
import console from './modules/IDE/reducers/console'; import console from './modules/IDE/reducers/console';
import { reducer as form } from 'redux-form';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
form, form,

View file

@ -14,7 +14,7 @@ const checkAuth = (store) => {
store.dispatch(getUser()); store.dispatch(getUser());
}; };
const routes = (store) => const routes = store =>
( (
<Route path="/" component={App}> <Route path="/" component={App}>
<IndexRoute component={IDEView} onEnter={checkAuth(store)} /> <IndexRoute component={IDEView} onEnter={checkAuth(store)} />

View file

@ -175,3 +175,16 @@
height:1px; height:1px;
overflow:hidden; overflow:hidden;
} }
%link {
@include themify() {
text-decoration: none;
color: getThemifyVariable('inactive-text-color');
cursor: pointer;
&:hover {
text-decoration: none;
color: getThemifyVariable('primary-text-color');
}
}
}

View file

@ -6,10 +6,13 @@ html, body {
font-size: #{$base-font-size}px; font-size: #{$base-font-size}px;
} }
body, input, button { body, input {
@include themify() { @include themify() {
color: getThemifyVariable('primary-text-color'); color: getThemifyVariable('primary-text-color');
} }
}
body, input, button {
font-family: Montserrat, sans-serif; font-family: Montserrat, sans-serif;
} }
@ -20,13 +23,7 @@ body, input, button {
a { a {
@include themify() { @include themify() {
text-decoration: none; @extend %link;
color: getThemifyVariable('inactive-text-color');
cursor: pointer;
&:hover {
text-decoration: none;
color: getThemifyVariable('primary-text-color');
}
} }
} }
@ -34,10 +31,6 @@ input, button {
font-size: 1rem; font-size: 1rem;
} }
button:focus {
outline: none;
}
input { input {
padding: #{5 / $base-font-size}rem; padding: #{5 / $base-font-size}rem;
border: 1px solid ; border: 1px solid ;
@ -55,6 +48,14 @@ input[type="submit"] {
} }
} }
button {
@include themify() {
@extend %link;
}
background: transparent;
border: none;
}
h2 { h2 {
font-size: #{21 / $base-font-size}em; font-size: #{21 / $base-font-size}em;
} }
@ -71,3 +72,6 @@ h6 {
thead { thead {
text-align: left; text-align: left;
} }
th {
text-align: left;
}

View file

@ -97,6 +97,11 @@
} }
.preview-console__clear { .preview-console__clear {
@include themify() {
@extend %link;
}
background: transparent;
border: none;
padding-right: #{10 / $base-font-size}rem; padding-right: #{10 / $base-font-size}rem;
.preview-console--collapsed & { .preview-console--collapsed & {
display: none; display: none;

View file

@ -24,7 +24,8 @@
margin-right: #{-6 / $base-font-size}rem; margin-right: #{-6 / $base-font-size}rem;
} }
.preference button { .preference__minus-button,
.preference__plus-button {
@include themify() { @include themify() {
@extend %preferences-button; @extend %preferences-button;
width: #{32 / $base-font-size}rem; width: #{32 / $base-font-size}rem;

View file

@ -56,6 +56,7 @@
height: #{20 / $base-font-size}rem; height: #{20 / $base-font-size}rem;
font-size: #{12 / $base-font-size}rem; font-size: #{12 / $base-font-size}rem;
cursor: pointer; cursor: pointer;
position: relative;
@include themify() { @include themify() {
color: map-get($theme-map, 'inactive-text-color'); color: map-get($theme-map, 'inactive-text-color');
&:hover > .file-item__content .sidebar__file-item-name { &:hover > .file-item__content .sidebar__file-item-name {
@ -106,6 +107,15 @@
.sidebar__file-item--editing & { .sidebar__file-item--editing & {
display: none; display: none;
} }
&:before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: '';
width: 100%;
}
} }
.sidebar__file-item-show-options { .sidebar__file-item-show-options {
@ -225,6 +235,8 @@
& svg { & svg {
height: #{10 / $base-font-size}rem; height: #{10 / $base-font-size}rem;
} }
background-color: transparent;
border: none;
} }
.sidebar__file-item-closed { .sidebar__file-item-closed {
@ -260,6 +272,14 @@
width: #{145 / $base-font-size}rem; width: #{145 / $base-font-size}rem;
} }
.sidebar__file-item-option {
@include themify() {
@extend %link;
}
background-color: transparent;
border: none;
}
.sidebar__file-item--closed .file-item__children { .sidebar__file-item--closed .file-item__children {
display: none; display: none;

View file

@ -10,7 +10,7 @@
"build": "NODE_ENV=production webpack --config webpack.config.prod.js --progress", "build": "NODE_ENV=production webpack --config webpack.config.prod.js --progress",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"fetch-examples": "node fetch-examples.js", "fetch-examples": "node fetch-examples.js",
"postinstall" : "git submodule update --remote --recursive" "postinstall": "git submodule update --remote --recursive"
}, },
"main": "index.js", "main": "index.js",
"author": "Cassie Tarakajian", "author": "Cassie Tarakajian",
@ -20,27 +20,25 @@
"url": "git+https://github.com/catarak/p5.js-web-editor.git" "url": "git+https://github.com/catarak/p5.js-web-editor.git"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^6.1.0", "babel-eslint": "^7.1.1",
"babel-loader": "^6.2.4", "babel-loader": "^6.2.4",
"babel-plugin-transform-react-constant-elements": "^6.8.0", "babel-plugin-transform-react-constant-elements": "^6.8.0",
"babel-plugin-transform-react-inline-elements": "^6.8.0", "babel-plugin-transform-react-inline-elements": "^6.8.0",
"babel-plugin-transform-react-remove-prop-types": "^0.2.6", "babel-plugin-transform-react-remove-prop-types": "^0.2.6",
"babel-polyfill": "^6.8.0",
"babel-preset-es2015": "^6.6.0", "babel-preset-es2015": "^6.6.0",
"babel-preset-es2015-native-modules": "^6.9.2", "babel-preset-es2015-native-modules": "^6.9.2",
"babel-preset-react": "^6.5.0", "babel-preset-react": "^6.5.0",
"babel-preset-react-hmre": "^1.1.1", "babel-preset-react-hmre": "^1.1.1",
"babel-preset-react-optimize": "^1.0.1", "babel-preset-react-optimize": "^1.0.1",
"babel-preset-stage-0": "^6.5.0", "babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.8.0",
"chunk-manifest-webpack-plugin": "^0.1.0", "chunk-manifest-webpack-plugin": "^0.1.0",
"css-loader": "^0.23.1", "css-loader": "^0.23.1",
"cssnano": "^3.7.1", "cssnano": "^3.7.1",
"eslint": "^2.13.1", "eslint": "^3.14.0",
"eslint-config-airbnb": "^9.0.1", "eslint-config-airbnb": "^14.0.0",
"eslint-plugin-import": "^1.9.2", "eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^1.5.3", "eslint-plugin-jsx-a11y": "^3.0.2",
"eslint-plugin-react": "^5.2.2", "eslint-plugin-react": "^6.9.0",
"extract-text-webpack-plugin": "^1.0.1", "extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5", "file-loader": "^0.8.5",
"node-sass": "^3.7.0", "node-sass": "^3.7.0",
@ -49,14 +47,8 @@
"postcss-focus": "^1.0.0", "postcss-focus": "^1.0.0",
"postcss-loader": "^0.9.1", "postcss-loader": "^0.9.1",
"postcss-reporter": "^1.3.3", "postcss-reporter": "^1.3.3",
"redux-devtools": "^3.3.1",
"redux-devtools-dock-monitor": "^1.1.1",
"redux-devtools-log-monitor": "^1.0.11",
"sass-loader": "^3.2.0", "sass-loader": "^3.2.0",
"style-loader": "^0.13.1", "style-loader": "^0.13.1",
"webpack": "^1.14.0",
"webpack-dev-middleware": "^1.6.1",
"webpack-hot-middleware": "^2.10.0",
"webpack-manifest-plugin": "^1.1.0" "webpack-manifest-plugin": "^1.1.0"
}, },
"engines": { "engines": {
@ -67,6 +59,8 @@
"async": "^2.0.0", "async": "^2.0.0",
"axios": "^0.12.0", "axios": "^0.12.0",
"babel-core": "^6.8.0", "babel-core": "^6.8.0",
"babel-polyfill": "^6.8.0",
"babel-register": "^6.8.0",
"bcrypt-nodejs": "0.0.3", "bcrypt-nodejs": "0.0.3",
"blob-util": "^1.2.1", "blob-util": "^1.2.1",
"body-parser": "^1.15.1", "body-parser": "^1.15.1",
@ -106,6 +100,9 @@
"react-router": "^2.6.0", "react-router": "^2.6.0",
"react-split-pane": "^0.1.44", "react-split-pane": "^0.1.44",
"redux": "^3.5.2", "redux": "^3.5.2",
"redux-devtools": "^3.3.1",
"redux-devtools-dock-monitor": "^1.1.1",
"redux-devtools-log-monitor": "^1.0.11",
"redux-form": "^5.3.3", "redux-form": "^5.3.3",
"redux-thunk": "^2.1.0", "redux-thunk": "^2.1.0",
"request": "^2.76.0", "request": "^2.76.0",
@ -113,6 +110,9 @@
"s3-policy": "^0.2.0", "s3-policy": "^0.2.0",
"shortid": "^2.2.6", "shortid": "^2.2.6",
"srcdoc-polyfill": "^0.2.0", "srcdoc-polyfill": "^0.2.0",
"webpack": "^1.14.0",
"webpack-dev-middleware": "^1.6.1",
"webpack-hot-middleware": "^2.10.0",
"xhr": "^2.2.1" "xhr": "^2.2.1"
} }
} }

View file

@ -1,9 +1,9 @@
import User from '../models/user';
const passport = require('passport'); const passport = require('passport');
const GitHubStrategy = require('passport-github').Strategy; const GitHubStrategy = require('passport-github').Strategy;
const LocalStrategy = require('passport-local').Strategy; const LocalStrategy = require('passport-local').Strategy;
import User from '../models/user';
passport.serializeUser((user, done) => { passport.serializeUser((user, done) => {
done(null, user.id); done(null, user.id);
}); });
@ -30,7 +30,7 @@ passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, don
return done(null, false, { msg: 'Invalid email or password.' }); return done(null, false, { msg: 'Invalid email or password.' });
}); });
}) })
.catch((err) => done(null, false, { msg: err })); .catch(err => done(null, false, { msg: err }));
})); }));
/** /**
@ -42,20 +42,19 @@ passport.use(new GitHubStrategy({
callbackURL: '/auth/github/callback', callbackURL: '/auth/github/callback',
passReqToCallback: true passReqToCallback: true
}, (req, accessToken, refreshToken, profile, done) => { }, (req, accessToken, refreshToken, profile, done) => {
User.findOne({ github: profile.id }, (err, existingUser) => { User.findOne({ github: profile.id }, (findByGithubErr, existingUser) => {
if (existingUser) { if (existingUser) {
return done(null, existingUser); done(null, existingUser);
return;
} }
User.findOne({ email: profile._json.email }, (err, existingEmailUser) => { User.findOne({ email: profile._json.email }, (findByEmailErr, existingEmailUser) => {
if (existingEmailUser) { if (existingEmailUser) {
existingEmailUser.email = existingEmailUser.email || profile._json.email; existingEmailUser.email = existingEmailUser.email || profile._json.email;
existingEmailUser.github = profile.id; existingEmailUser.github = profile.id;
existingEmailUser.username = existingEmailUser.username || profile.username; existingEmailUser.username = existingEmailUser.username || profile.username;
existingEmailUser.tokens.push({ kind: 'github', accessToken }); existingEmailUser.tokens.push({ kind: 'github', accessToken });
existingEmailUser.name = existingEmailUser.name || profile.displayName; existingEmailUser.name = existingEmailUser.name || profile.displayName;
existingEmailUser.save((err) => { existingEmailUser.save(saveErr => done(null, existingEmailUser));
return done(null, existingEmailUser);
});
} else { } else {
const user = new User(); const user = new User();
user.email = profile._json.email; user.email = profile._json.email;
@ -63,9 +62,7 @@ passport.use(new GitHubStrategy({
user.username = profile.username; user.username = profile.username;
user.tokens.push({ kind: 'github', accessToken }); user.tokens.push({ kind: 'github', accessToken });
user.name = profile.displayName; user.name = profile.displayName;
user.save((err) => { user.save(saveErr => done(null, user));
return done(null, user);
});
} }
}); });
}); });

View file

@ -26,3 +26,5 @@ export function signS3(req, res) {
}; };
return res.json(result); return res.json(result);
} }
export default signS3;

View file

@ -1,10 +1,10 @@
import jsdom, { serializeDocument } from 'jsdom';
import Project from '../models/project'; import Project from '../models/project';
import { import {
injectMediaUrls, injectMediaUrls,
resolvePathsForElementsWithAttribute, resolvePathsForElementsWithAttribute,
resolveScripts, resolveScripts,
resolveStyles } from '../utils/previewGeneration'; resolveStyles } from '../utils/previewGeneration';
import jsdom, { serializeDocument } from 'jsdom';
export function serveProject(req, res) { export function serveProject(req, res) {
Project.findById(req.params.project_id) Project.findById(req.params.project_id)
@ -32,3 +32,5 @@ export function serveProject(req, res) {
}); });
}); });
} }
export default serveProject;

View file

@ -16,16 +16,18 @@ export function createFile(req, res) {
}, (err, updatedProject) => { }, (err, updatedProject) => {
if (err) { if (err) {
console.log(err); console.log(err);
return res.json({ success: false }); res.json({ success: false });
return;
} }
const newFile = updatedProject.files[updatedProject.files.length - 1]; const newFile = updatedProject.files[updatedProject.files.length - 1];
updatedProject.files.id(req.body.parentId).children.push(newFile.id); updatedProject.files.id(req.body.parentId).children.push(newFile.id);
updatedProject.save(innerErr => { updatedProject.save((innerErr) => {
if (innerErr) { if (innerErr) {
console.log(innerErr); console.log(innerErr);
return res.json({ success: false }); res.json({ success: false });
return;
} }
return res.json(updatedProject.files[updatedProject.files.length - 1]); res.json(updatedProject.files[updatedProject.files.length - 1]);
}); });
}); });
} }
@ -38,13 +40,13 @@ function getAllDescendantIds(files, nodeId) {
} }
function deleteMany(files, ids) { function deleteMany(files, ids) {
ids.forEach(id => { ids.forEach((id) => {
files.id(id).remove(); files.id(id).remove();
}); });
} }
function deleteChild(files, parentId, id) { function deleteChild(files, parentId, id) {
files = files.map((file) => { return files.map((file) => {
if (file.id === parentId) { if (file.id === parentId) {
file.children = file.children.filter(child => child !== id); file.children = file.children.filter(child => child !== id);
return file; return file;
@ -57,11 +59,11 @@ export function deleteFile(req, res) {
Project.findById(req.params.project_id, (err, project) => { Project.findById(req.params.project_id, (err, project) => {
const idsToDelete = getAllDescendantIds(project.files, req.params.file_id); const idsToDelete = getAllDescendantIds(project.files, req.params.file_id);
deleteMany(project.files, [req.params.file_id, ...idsToDelete]); deleteMany(project.files, [req.params.file_id, ...idsToDelete]);
deleteChild(project.files, req.query.parentId, req.params.file_id); project.files = deleteChild(project.files, req.query.parentId, req.params.file_id);
// project.files.id(req.params.file_id).remove(); // project.files.id(req.params.file_id).remove();
// const childrenArray = project.files.id(req.query.parentId).children; // const childrenArray = project.files.id(req.query.parentId).children;
// project.files.id(req.query.parentId).children = childrenArray.filter(id => id !== req.params.file_id); // project.files.id(req.query.parentId).children = childrenArray.filter(id => id !== req.params.file_id);
project.save(innerErr => { project.save((innerErr) => {
res.json(project.files); res.json(project.files);
}); });
}); });
@ -70,12 +72,14 @@ export function deleteFile(req, res) {
export function getFileContent(req, res) { export function getFileContent(req, res) {
Project.findById(req.params.project_id, (err, project) => { Project.findById(req.params.project_id, (err, project) => {
if (err) { if (err) {
return res.status(404).send({ success: false, message: 'Project with that id does not exist.' }); res.status(404).send({ success: false, message: 'Project with that id does not exist.' });
return;
} }
const filePath = req.params[0]; const filePath = req.params[0];
const resolvedFile = resolvePathToFile(filePath, project.files); const resolvedFile = resolvePathToFile(filePath, project.files);
if (!resolvedFile) { if (!resolvedFile) {
return res.status(404).send({ success: false, message: 'File with that name and path does not exist.' }); res.status(404).send({ success: false, message: 'File with that name and path does not exist.' });
return;
} }
res.send(resolvedFile.content); res.send(resolvedFile.content);
}); });

View file

@ -1,13 +1,13 @@
import Project from '../models/project';
import User from '../models/user';
import archiver from 'archiver'; import archiver from 'archiver';
import request from 'request'; import request from 'request';
import moment from 'moment'; import Project from '../models/project';
import User from '../models/user';
export function createProject(req, res) { export function createProject(req, res) {
if (!req.user) { if (!req.user) {
return res.status(403).send({ success: false, message: 'Session does not match owner of project.' }); res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
return;
} }
let projectValues = { let projectValues = {
@ -17,20 +17,27 @@ export function createProject(req, res) {
projectValues = Object.assign(projectValues, req.body); projectValues = Object.assign(projectValues, req.body);
Project.create(projectValues, (err, newProject) => { Project.create(projectValues, (err, newProject) => {
if (err) { return res.json({ success: false }); } if (err) {
res.json({ success: false });
return;
}
Project.populate(newProject, Project.populate(newProject,
{ path: 'user', select: 'username' }, { path: 'user', select: 'username' },
(innerErr, newProjectWithUser) => { (innerErr, newProjectWithUser) => {
if (innerErr) { return res.json({ success: false }); } if (innerErr) {
return res.json(newProjectWithUser); res.json({ success: false });
return;
}
res.json(newProjectWithUser);
}); });
}); });
} }
export function updateProject(req, res) { export function updateProject(req, res) {
Project.findById(req.params.project_id, (err, project) => { Project.findById(req.params.project_id, (findProjectErr, project) => {
if (!req.user || !project.user.equals(req.user._id)) { if (!req.user || !project.user.equals(req.user._id)) {
return res.status(403).send({ success: false, message: 'Session does not match owner of project.' }); res.status(403).send({ success: false, message: 'Session does not match owner of project.' });
return;
} }
// if (req.body.updatedAt && moment(req.body.updatedAt) < moment(project.updatedAt)) { // if (req.body.updatedAt && moment(req.body.updatedAt) < moment(project.updatedAt)) {
// return res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' }); // return res.status(409).send({ success: false, message: 'Attempted to save stale version of project.' });
@ -40,27 +47,29 @@ export function updateProject(req, res) {
$set: req.body $set: req.body
}) })
.populate('user', 'username') .populate('user', 'username')
.exec((err, updatedProject) => { .exec((updateProjectErr, updatedProject) => {
if (err) { if (updateProjectErr) {
console.log(err); console.log(updateProjectErr);
return res.json({ success: false }); res.json({ success: false });
return;
} }
if (updatedProject.files.length !== req.body.files.length) { if (updatedProject.files.length !== req.body.files.length) {
const oldFileIds = updatedProject.files.map(file => file.id); const oldFileIds = updatedProject.files.map(file => file.id);
const newFileIds = req.body.files.map(file => file.id); const newFileIds = req.body.files.map(file => file.id);
const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1); const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1);
staleIds.forEach(staleId => { staleIds.forEach((staleId) => {
updatedProject.files.id(staleId).remove(); updatedProject.files.id(staleId).remove();
}); });
updatedProject.save((innerErr) => { updatedProject.save((innerErr) => {
if (innerErr) { if (innerErr) {
console.log(innerErr); console.log(innerErr);
return res.json({ success: false }); res.json({ success: false });
return;
} }
return res.json(updatedProject); res.json(updatedProject);
}); });
} }
return res.json(updatedProject); res.json(updatedProject);
}); });
}); });
} }
@ -77,15 +86,17 @@ export function getProject(req, res) {
} }
export function deleteProject(req, res) { export function deleteProject(req, res) {
Project.findById(req.params.project_id, (err, project) => { Project.findById(req.params.project_id, (findProjectErr, project) => {
if (!req.user || !project.user.equals(req.user._id)) { if (!req.user || !project.user.equals(req.user._id)) {
return res.status(403).json({ success: false, message: 'Session does not match owner of project.' }); res.status(403).json({ success: false, message: 'Session does not match owner of project.' });
return;
} }
Project.remove({ _id: req.params.project_id }, (err) => { Project.remove({ _id: req.params.project_id }, (removeProjectError) => {
if (err) { if (removeProjectError) {
return res.status(404).send({ message: 'Project with that id does not exist' }); res.status(404).send({ message: 'Project with that id does not exist' });
return;
} }
return res.json({ success: true }); res.json({ success: true });
}); });
}); });
} }
@ -100,7 +111,7 @@ export function getProjects(req, res) {
}); });
} else { } else {
// could just move this to client side // could just move this to client side
return res.json([]); res.json([]);
} }
} }
@ -108,18 +119,18 @@ export function getProjectsForUser(req, res) {
if (req.params.username) { if (req.params.username) {
User.findOne({ username: req.params.username }, (err, user) => { User.findOne({ username: req.params.username }, (err, user) => {
if (!user) { if (!user) {
return res.status(404).json({ message: 'User with that username does not exist.' }); res.status(404).json({ message: 'User with that username does not exist.' });
return;
} }
Project.find({ user: user._id }) // eslint-disable-line no-underscore-dangle Project.find({ user: user._id }) // eslint-disable-line no-underscore-dangle
.sort('-createdAt') .sort('-createdAt')
.select('name files id createdAt updatedAt') .select('name files id createdAt updatedAt')
.exec((err, projects) => res.json(projects)); .exec((innerErr, projects) => res.json(projects));
}); });
} else { } else {
// could just move this to client side // could just move this to client side
return res.json([]); res.json([]);
} }
return null;
} }
function buildZip(project, req, res) { function buildZip(project, req, res) {
@ -127,7 +138,6 @@ function buildZip(project, req, res) {
const rootFile = project.files.find(file => file.name === 'root'); const rootFile = project.files.find(file => file.name === 'root');
const numFiles = project.files.filter(file => file.fileType !== 'folder').length; const numFiles = project.files.filter(file => file.fileType !== 'folder').length;
const files = project.files; const files = project.files;
const projectName = project.name;
let numCompletedFiles = 0; let numCompletedFiles = 0;
zip.on('error', (err) => { zip.on('error', (err) => {
@ -140,14 +150,13 @@ function buildZip(project, req, res) {
function addFileToZip(file, path) { function addFileToZip(file, path) {
if (file.fileType === 'folder') { if (file.fileType === 'folder') {
const newPath = file.name === 'root' ? path : `${path}${file.name}/`; const newPath = file.name === 'root' ? path : `${path}${file.name}/`;
file.children.forEach(fileId => { file.children.forEach((fileId) => {
const childFile = files.find(f => f.id === fileId); const childFile = files.find(f => f.id === fileId);
(() => { (() => {
addFileToZip(childFile, newPath); addFileToZip(childFile, newPath);
})(); })();
}); });
} else { } else if (file.url) {
if (file.url) {
request({ method: 'GET', url: file.url, encoding: null }, (err, response, body) => { request({ method: 'GET', url: file.url, encoding: null }, (err, response, body) => {
zip.append(body, { name: `${path}${file.name}` }); zip.append(body, { name: `${path}${file.name}` });
numCompletedFiles += 1; numCompletedFiles += 1;
@ -163,7 +172,6 @@ function buildZip(project, req, res) {
} }
} }
} }
}
addFileToZip(rootFile, '/'); addFileToZip(rootFile, '/');
} }

View file

@ -1,8 +1,8 @@
import User from '../models/user';
import crypto from 'crypto'; import crypto from 'crypto';
import async from 'async'; import async from 'async';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import mg from 'nodemailer-mailgun-transport'; import mg from 'nodemailer-mailgun-transport';
import User from '../models/user';
export function createUser(req, res, next) { export function createUser(req, res, next) {
const user = new User({ const user = new User({
@ -12,17 +12,25 @@ export function createUser(req, res, next) {
}); });
User.findOne({ email: req.body.email }, User.findOne({ email: req.body.email },
(err, existingUser) => { // eslint-disable-line consistent-return (err, existingUser) => {
if (err) { res.status(404).send({ error: err }); } if (err) {
res.status(404).send({ error: err });
return;
}
if (existingUser) { if (existingUser) {
return res.status(422).send({ error: 'Email is in use' }); res.status(422).send({ error: 'Email is in use' });
return;
} }
user.save((saveErr) => { // eslint-disable-line consistent-return user.save((saveErr) => {
if (saveErr) { return next(saveErr); } if (saveErr) {
req.logIn(user, (loginErr) => { // eslint-disable-line consistent-return next(saveErr);
return;
}
req.logIn(user, (loginErr) => {
if (loginErr) { if (loginErr) {
return next(loginErr); next(loginErr);
return;
} }
res.json({ res.json({
email: req.user.email, email: req.user.email,
@ -58,21 +66,24 @@ export function duplicateUserCheck(req, res) {
export function updatePreferences(req, res) { export function updatePreferences(req, res) {
User.findById(req.user.id, (err, user) => { User.findById(req.user.id, (err, user) => {
if (err) { if (err) {
return res.status(500).json({ error: err }); res.status(500).json({ error: err });
return;
} }
if (!user) { if (!user) {
return res.status(404).json({ error: 'Document not found' }); res.status(404).json({ error: 'Document not found' });
return;
} }
const preferences = Object.assign({}, user.preferences, req.body.preferences); const preferences = Object.assign({}, user.preferences, req.body.preferences);
user.preferences = preferences; user.preferences = preferences;
user.save((err) => { user.save((saveErr) => {
if (err) { if (saveErr) {
return res.status(500).json({ error: err }); res.status(500).json({ error: saveErr });
return;
} }
return res.json(user.preferences); res.json(user.preferences);
}); });
}); });
} }
@ -88,13 +99,14 @@ export function resetPasswordInitiate(req, res) {
(token, done) => { (token, done) => {
User.findOne({ email: req.body.email }, (err, user) => { User.findOne({ email: req.body.email }, (err, user) => {
if (!user) { if (!user) {
return res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' }); res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
return;
} }
user.resetPasswordToken = token; user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
user.save((err) => { user.save((saveErr) => {
done(err, token, user); done(saveErr, token, user);
}); });
}); });
}, },
@ -124,41 +136,41 @@ export function resetPasswordInitiate(req, res) {
], (err) => { ], (err) => {
if (err) { if (err) {
console.log(err); console.log(err);
return res.json({ success: false }); res.json({ success: false });
return;
} }
// send email here res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
return res.json({ success: true, message: 'If the email is registered with the editor, an email has been sent.' });
}); });
} }
export function validateResetPasswordToken(req, res) { export function validateResetPasswordToken(req, res) {
User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => { User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => {
if (!user) { if (!user) {
return res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' }); res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' });
return;
} }
res.json({ success: true }); res.json({ success: true });
}); });
} }
export function updatePassword(req, res) { export function updatePassword(req, res) {
User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, function (err, user) { User.findOne({ resetPasswordToken: req.params.token, resetPasswordExpires: { $gt: Date.now() } }, (err, user) => {
if (!user) { if (!user) {
return res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' }); res.status(401).json({ success: false, message: 'Password reset token is invalid or has expired.' });
return;
} }
user.password = req.body.password; user.password = req.body.password;
user.resetPasswordToken = undefined; user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined; user.resetPasswordExpires = undefined;
user.save(function (err) { user.save((saveErr) => {
req.logIn(user, function (err) { req.logIn(user, loginErr => res.json({
return res.json({
email: req.user.email, email: req.user.email,
username: req.user.username, username: req.user.username,
preferences: req.user.preferences, preferences: req.user.preferences,
id: req.user._id id: req.user._id
}); }));
});
}); });
}); });

View file

@ -3,10 +3,9 @@ import Q from 'q';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import objectID from 'bson-objectid'; import objectID from 'bson-objectid';
import shortid from 'shortid'; import shortid from 'shortid';
import eachSeries from 'async/eachSeries';
import User from './models/user'; import User from './models/user';
import Project from './models/project'; import Project from './models/project';
import async from 'async';
import eachSeries from 'async/eachSeries';
const defaultHTML = const defaultHTML =
`<!DOCTYPE html> `<!DOCTYPE html>
@ -30,8 +29,8 @@ const defaultCSS =
} }
`; `;
const client_id = process.env.GITHUB_ID; const clientId = process.env.GITHUB_ID;
const client_secret = process.env.GITHUB_SECRET; const clientSecret = process.env.GITHUB_SECRET;
const headers = { 'User-Agent': 'p5js-web-editor/0.0.1' }; const headers = { 'User-Agent': 'p5js-web-editor/0.0.1' };
@ -41,144 +40,106 @@ mongoose.connection.on('error', () => {
process.exit(1); process.exit(1);
}); });
getp5User();
function getp5User() {
User.findOne({ username: 'p5' }, (err, user) => {
if (err) throw err;
if (!user) {
user = new User({
username: 'p5',
email: 'p5-examples@gmail.com',
password: 'test'
});
user.save(err => {
if (err) throw err;
console.log('Created a user p5' + user);
});
}
Project.find({ user: user._id }, (err, projects) => {
// if there are already some sketches, delete them
console.log('Deleting old projects...');
projects.forEach(project => {
Project.remove({ _id: project._id }, err => {
if (err) throw err;
});
});
});
return getCategories()
.then(getSketchesInCategories)
.then(getSketchContent)
.then(createProjectsInP5user);
});
}
function getCategories() { function getCategories() {
let categories = []; const categories = [];
const options = { const options = {
url: 'https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/en?client_id=' + url: `https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/en?client_id=${
client_id + '&client_secret=' + client_secret, clientId}&client_secret=${clientSecret}`,
method: 'GET', method: 'GET',
headers headers
}; };
return rp(options).then(res => { return rp(options).then((res) => {
const json = JSON.parse(res); const json = JSON.parse(res);
json.forEach(metadata => { json.forEach((metadata) => {
let category = ''; let category = '';
for (let j = 1; j < metadata.name.split('_').length; j++) { for (let j = 1; j < metadata.name.split('_').length; j += 1) {
category += metadata.name.split('_')[j] + ' '; category += `${metadata.name.split('_')[j]} `;
} }
categories.push({ url: metadata.url, name: category }); categories.push({ url: metadata.url, name: category });
}); });
return categories; return categories;
}).catch(err => { }).catch((err) => {
throw err; throw err;
}); });
} }
function getSketchesInCategories(categories) { function getSketchesInCategories(categories) {
return Q.all(categories.map(category => { return Q.all(categories.map((category) => {
const options = { const options = {
url: category.url.replace('?ref=master', '') + '?client_id=' + client_id + '&client_secret=' + client_secret, url: `${category.url.replace('?ref=master', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
method: 'GET', method: 'GET',
headers headers
}; };
return rp(options).then(res => { return rp(options).then((res) => {
let projectsInOneCategory = []; const projectsInOneCategory = [];
const examples = JSON.parse(res); const examples = JSON.parse(res);
examples.forEach(example => { examples.forEach((example) => {
let projectName; let projectName;
if (example.name === '02_Instance_Container.js') { if (example.name === '02_Instance_Container.js') {
for (let i = 1; i < 5; i++) { for (let i = 1; i < 5; i += 1) {
const instanceProjectName = category.name + ': ' + 'Instance Container ' + i; const instanceProjectName = `${category.name}: Instance Container ${i}`;
projectsInOneCategory.push({ sketchUrl: example.download_url, projectName: instanceProjectName }); projectsInOneCategory.push({ sketchUrl: example.download_url, projectName: instanceProjectName });
} }
} else { } else {
if (example.name.split('_')[1]) { if (example.name.split('_')[1]) {
projectName = category.name + ': ' + example.name.split('_').slice(1).join(' ').replace('.js', ''); projectName = `${category.name}: ${example.name.split('_').slice(1).join(' ').replace('.js', '')}`;
} else { } else {
projectName = category.name + ': ' + example.name.replace('.js', ''); projectName = `${category.name}: ${example.name.replace('.js', '')}`;
} }
projectsInOneCategory.push({ sketchUrl: example.download_url, projectName }); projectsInOneCategory.push({ sketchUrl: example.download_url, projectName });
} }
}); });
return projectsInOneCategory; return projectsInOneCategory;
}).catch(err => { }).catch((err) => {
throw err; throw err;
}); });
})); }));
} }
function getSketchContent(projectsInAllCategories) { function getSketchContent(projectsInAllCategories) {
return Q.all(projectsInAllCategories.map(projectsInOneCategory => { return Q.all(projectsInAllCategories.map(projectsInOneCategory => Q.all(projectsInOneCategory.map((project) => {
return Q.all(projectsInOneCategory.map(project => {
const options = { const options = {
url: project.sketchUrl.replace('?ref=master', '') + '?client_id=' + client_id + '&client_secret=' + client_secret, url: `${project.sketchUrl.replace('?ref=master', '')}?client_id=${clientId}&client_secret=${clientSecret}`,
method: 'GET', method: 'GET',
headers headers
}; };
return rp(options).then(res => { return rp(options).then((res) => {
const noNumberprojectName = project.projectName.replace(/(\d+)/g, ''); const noNumberprojectName = project.projectName.replace(/(\d+)/g, '');
if (noNumberprojectName === 'Instance Mode : Instance Container ') { if (noNumberprojectName === 'Instance Mode : Instance Container ') {
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i += 1) {
let splitedRes = res.split('*/')[1].split('</html>')[i] + '</html>\n'; const splitedRes = `${res.split('*/')[1].split('</html>')[i]}</html>\n`;
project.sketchContent = splitedRes.replace('p5.js', 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js'); project.sketchContent = splitedRes.replace('p5.js', 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js');
} }
} else { } else {
project.sketchContent = res; project.sketchContent = res;
} }
return project; return project;
}).catch(err => { }).catch((err) => {
throw err; throw err;
}); });
})); }))));
}));
} }
function createProjectsInP5user(projectsInAllCategories) { function createProjectsInP5user(projectsInAllCategories) {
let assetsfiles = [];
const options = { const options = {
url: 'https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/assets?client_id=' + url: `https://api.github.com/repos/processing/p5.js-website/contents/dist/assets/examples/assets?client_id=${
client_id + '&client_secret=' + client_secret, clientId}&client_secret=${clientSecret}`,
method: 'GET', method: 'GET',
headers headers
}; };
rp(options).then(res => { rp(options).then((res) => {
const assets = JSON.parse(res); const assets = JSON.parse(res);
User.findOne({ username: 'p5' }, (err, user) => { User.findOne({ username: 'p5' }, (err, user) => {
if (err) throw err; if (err) throw err;
async.eachSeries(projectsInAllCategories, (projectsInOneCategory, categoryCallback) => { eachSeries(projectsInAllCategories, (projectsInOneCategory, categoryCallback) => {
async.eachSeries(projectsInOneCategory, (project, projectCallback) => { eachSeries(projectsInOneCategory, (project, projectCallback) => {
let newProject; let newProject;
const a = objectID().toHexString(); const a = objectID().toHexString();
const b = objectID().toHexString(); const b = objectID().toHexString();
@ -267,12 +228,12 @@ function createProjectsInP5user(projectsInAllCategories) {
}); });
} }
let assetsInProject = project.sketchContent.match(/assets\/[\w-]+\.[\w]*/g) || project.sketchContent.match(/assets\/[\w-]*/g) || []; const assetsInProject = project.sketchContent.match(/assets\/[\w-]+\.[\w]*/g) || project.sketchContent.match(/assets\/[\w-]*/g) || [];
assetsInProject.forEach((assetName, i) => { assetsInProject.forEach((assetNamePath, i) => {
assetName = assetName.split('assets/')[1]; let assetName = assetNamePath.split('assets/')[1];
assets.forEach(asset => { assets.forEach((asset) => {
if (asset.name === assetName || asset.name.split('.')[0] === assetName) { if (asset.name === assetName || asset.name.split('.')[0] === assetName) {
assetName = asset.name; assetName = asset.name;
} }
@ -301,25 +262,61 @@ function createProjectsInP5user(projectsInAllCategories) {
children: [], children: [],
fileType: 'file' fileType: 'file'
}); });
console.log('create assets: ' + assetName); console.log(`create assets: ${assetName}`);
// add asset file inside the newly created assets folder at index 4 // add asset file inside the newly created assets folder at index 4
newProject.files[4].children.push(fileID); newProject.files[4].children.push(fileID);
} }
}); });
newProject.save((err, newProject) => { newProject.save((saveErr, savedProject) => {
if (err) throw err; if (saveErr) throw saveErr;
console.log('Created a new project in p5 user: ' + newProject.name); console.log(`Created a new project in p5 user: ${savedProject.name}`);
projectCallback(); projectCallback();
}); });
}, (err) => { }, (categoryErr) => {
categoryCallback(); categoryCallback();
}); });
}, (err) => { }, (examplesErr) => {
process.exit(); process.exit();
}); });
}); });
}).catch(err => { }).catch((err) => {
throw err; throw err;
}); });
} }
function getp5User() {
User.findOne({ username: 'p5' }, (err, user) => {
if (err) throw err;
let p5User = user;
if (!p5User) {
p5User = new User({
username: 'p5',
email: 'p5-examples@gmail.com',
password: 'test'
});
p5User.save((saveErr) => {
if (saveErr) throw saveErr;
console.log(`Created a user p5${p5User}`);
});
}
Project.find({ user: p5User._id }, (projectsErr, projects) => {
// if there are already some sketches, delete them
console.log('Deleting old projects...');
projects.forEach((project) => {
Project.remove({ _id: project._id }, (removeErr) => {
if (removeErr) throw removeErr;
});
});
});
return getCategories()
.then(getSketchesInCategories)
.then(getSketchContent)
.then(createProjectsInP5user);
});
}
getp5User();

View file

@ -1,7 +1,8 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
const Schema = mongoose.Schema;
import shortid from 'shortid'; import shortid from 'shortid';
const Schema = mongoose.Schema;
const fileSchema = new Schema({ const fileSchema = new Schema({
name: { type: String, default: 'sketch.js' }, name: { type: String, default: 'sketch.js' },
content: { type: String, default: '' }, content: { type: String, default: '' },
@ -11,7 +12,7 @@ const fileSchema = new Schema({
isSelectedFile: { type: Boolean } isSelectedFile: { type: Boolean }
}, { timestamps: true, _id: true }); }, { timestamps: true, _id: true });
fileSchema.virtual('id').get(function () { fileSchema.virtual('id').get(function getFileId() {
return this._id.toHexString(); return this._id.toHexString();
}); });
@ -26,7 +27,7 @@ const projectSchema = new Schema({
_id: { type: String, default: shortid.generate } _id: { type: String, default: shortid.generate }
}, { timestamps: true }); }, { timestamps: true });
projectSchema.virtual('id').get(function () { projectSchema.virtual('id').get(function getProjectId() {
return this._id; return this._id;
}); });

View file

@ -1,7 +1,9 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const bcrypt = require('bcrypt-nodejs'); const bcrypt = require('bcrypt-nodejs');
const Schema = mongoose.Schema;
const userSchema = new Schema({ const userSchema = new Schema({
name: { type: String, default: '' }, name: { type: String, default: '' },
username: { type: String, required: true, unique: true }, username: { type: String, required: true, unique: true },
@ -39,7 +41,7 @@ userSchema.pre('save', function checkPassword(next) { // eslint-disable-line con
}); });
}); });
userSchema.virtual('id').get(function () { userSchema.virtual('id').get(function idToString() {
return this._id.toHexString(); return this._id.toHexString();
}); });

View file

@ -1,8 +1,9 @@
import { Router } from 'express'; import { Router } from 'express';
const router = new Router();
import { renderIndex } from '../views/index'; import { renderIndex } from '../views/index';
import { get404Sketch } from '../views/404Page'; import { get404Sketch } from '../views/404Page';
import { userExists } from '../controllers/user.controller.js'; import { userExists } from '../controllers/user.controller';
const router = new Router();
// this is intended to be a temporary file // this is intended to be a temporary file
// until i figure out isomorphic rendering // until i figure out isomorphic rendering
@ -48,7 +49,7 @@ router.route('/about').get((req, res) => {
}); });
router.route('/:username/sketches').get((req, res) => { router.route('/:username/sketches').get((req, res) => {
userExists(req.params.username, (exists) => ( userExists(req.params.username, exists => (
exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html)) exists ? res.send(renderIndex()) : get404Sketch(html => res.send(html))
)); ));
}); });

View file

@ -1,5 +1,6 @@
import { Router } from 'express'; import { Router } from 'express';
import * as UserController from '../controllers/user.controller'; import * as UserController from '../controllers/user.controller';
const router = new Router(); const router = new Router();
router.route('/signup').post(UserController.createUser); router.route('/signup').post(UserController.createUser);

View file

@ -3,24 +3,15 @@ import mongoose from 'mongoose';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import session from 'express-session'; import session from 'express-session';
const MongoStore = require('connect-mongo')(session); import connectMongo from 'connect-mongo';
import passport from 'passport'; import passport from 'passport';
import path from 'path'; import path from 'path';
// Webpack Requirements // Webpack Requirements
import webpack from 'webpack'; import webpack from 'webpack';
import config from '../webpack.config.dev';
import webpackDevMiddleware from 'webpack-dev-middleware'; import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware'; import webpackHotMiddleware from 'webpack-hot-middleware';
import config from '../webpack.config.dev';
const app = new Express();
// Run Webpack dev server in development mode
if (process.env.NODE_ENV === 'development') {
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
app.use(webpackHotMiddleware(compiler));
}
// Import all required modules // Import all required modules
import serverConfig from './config'; import serverConfig from './config';
@ -35,6 +26,16 @@ import embedRoutes from './routes/embed.routes';
import { renderIndex } from './views/index'; import { renderIndex } from './views/index';
import { get404Sketch } from './views/404Page'; import { get404Sketch } from './views/404Page';
const app = new Express();
const MongoStore = connectMongo(session);
// Run Webpack dev server in development mode
if (process.env.NODE_ENV === 'development') {
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath }));
app.use(webpackHotMiddleware(compiler));
}
// Body parser, cookie parser, sessions, serve public assets // Body parser, cookie parser, sessions, serve public assets
app.use(Express.static(path.resolve(__dirname, '../static'))); app.use(Express.static(path.resolve(__dirname, '../static')));

View file

@ -15,7 +15,7 @@ export function resolvePathToFile(filePath, files) {
file._id.valueOf().toString() === childFileId.valueOf() file._id.valueOf().toString() === childFileId.valueOf()
) )
); );
childFiles.some(childFile => { childFiles.some((childFile) => {
if (childFile.name === filePathSegment) { if (childFile.name === filePathSegment) {
currentFile = childFile; currentFile = childFile;
foundChild = true; foundChild = true;
@ -30,3 +30,5 @@ export function resolvePathToFile(filePath, files) {
}); });
return resolvedFile; return resolvedFile;
} }
export default resolvePathToFile;

View file

@ -10,7 +10,7 @@ function resolveLinksInString(content, files, projectId) {
let fileStrings = content.match(STRING_REGEX); let fileStrings = content.match(STRING_REGEX);
const fileStringRegex = /^('|")(?!(http:\/\/|https:\/\/)).*('|")$/i; const fileStringRegex = /^('|")(?!(http:\/\/|https:\/\/)).*('|")$/i;
fileStrings = fileStrings || []; fileStrings = fileStrings || [];
fileStrings.forEach(fileString => { fileStrings.forEach((fileString) => {
// if string does not begin with http or https // if string does not begin with http or https
if (fileString.match(fileStringRegex)) { if (fileString.match(fileStringRegex)) {
const filePath = fileString.substr(1, fileString.length - 2); const filePath = fileString.substr(1, fileString.length - 2);
@ -35,7 +35,7 @@ function resolveLinksInString(content, files, projectId) {
} }
export function injectMediaUrls(filesToInject, allFiles, projectId) { export function injectMediaUrls(filesToInject, allFiles, projectId) {
filesToInject.forEach(file => { filesToInject.forEach((file) => {
file.content = resolveLinksInString(file.content, allFiles, projectId); file.content = resolveLinksInString(file.content, allFiles, projectId);
}); });
} }
@ -43,7 +43,7 @@ export function injectMediaUrls(filesToInject, allFiles, projectId) {
export function resolvePathsForElementsWithAttribute(attr, sketchDoc, files) { export function resolvePathsForElementsWithAttribute(attr, sketchDoc, files) {
const elements = sketchDoc.querySelectorAll(`[${attr}]`); const elements = sketchDoc.querySelectorAll(`[${attr}]`);
const elementsArray = Array.prototype.slice.call(elements); const elementsArray = Array.prototype.slice.call(elements);
elementsArray.forEach(element => { elementsArray.forEach((element) => {
if (element.getAttribute(attr).match(MEDIA_FILE_REGEX_NO_QUOTES)) { if (element.getAttribute(attr).match(MEDIA_FILE_REGEX_NO_QUOTES)) {
const resolvedFile = resolvePathToFile(element.getAttribute(attr), files); const resolvedFile = resolvePathToFile(element.getAttribute(attr), files);
if (resolvedFile) { if (resolvedFile) {
@ -56,7 +56,7 @@ export function resolvePathsForElementsWithAttribute(attr, sketchDoc, files) {
export function resolveScripts(sketchDoc, files, projectId) { export function resolveScripts(sketchDoc, files, projectId) {
const scriptsInHTML = sketchDoc.getElementsByTagName('script'); const scriptsInHTML = sketchDoc.getElementsByTagName('script');
const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML); const scriptsInHTMLArray = Array.prototype.slice.call(scriptsInHTML);
scriptsInHTMLArray.forEach(script => { scriptsInHTMLArray.forEach((script) => {
if (script.getAttribute('src') && script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null) { if (script.getAttribute('src') && script.getAttribute('src').match(NOT_EXTERNAL_LINK_REGEX) !== null) {
const resolvedFile = resolvePathToFile(script.getAttribute('src'), files); const resolvedFile = resolvePathToFile(script.getAttribute('src'), files);
if (resolvedFile) { if (resolvedFile) {
@ -76,13 +76,13 @@ export function resolveScripts(sketchDoc, files, projectId) {
export function resolveStyles(sketchDoc, files, projectId) { export function resolveStyles(sketchDoc, files, projectId) {
const inlineCSSInHTML = sketchDoc.getElementsByTagName('style'); const inlineCSSInHTML = sketchDoc.getElementsByTagName('style');
const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML); const inlineCSSInHTMLArray = Array.prototype.slice.call(inlineCSSInHTML);
inlineCSSInHTMLArray.forEach(style => { inlineCSSInHTMLArray.forEach((style) => {
style.innerHTML = resolveLinksInString(style.innerHTML, files, projectId); style.innerHTML = resolveLinksInString(style.innerHTML, files, projectId);
}); });
const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]'); const cssLinksInHTML = sketchDoc.querySelectorAll('link[rel="stylesheet"]');
const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML); const cssLinksInHTMLArray = Array.prototype.slice.call(cssLinksInHTML);
cssLinksInHTMLArray.forEach(css => { cssLinksInHTMLArray.forEach((css) => {
if (css.getAttribute('href') && css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null) { if (css.getAttribute('href') && css.getAttribute('href').match(NOT_EXTERNAL_LINK_REGEX) !== null) {
const resolvedFile = resolvePathToFile(css.getAttribute('href'), files); const resolvedFile = resolvePathToFile(css.getAttribute('href'), files);
if (resolvedFile) { if (resolvedFile) {

View file

@ -20,19 +20,19 @@ export function get404Sketch(callback) {
instanceMode = jsFiles.find(file => file.name === 'sketch.js').content.includes('Instance Mode'); instanceMode = jsFiles.find(file => file.name === 'sketch.js').content.includes('Instance Mode');
jsFiles.forEach(file => { // Add js files as script tags jsFiles.forEach((file) => { // Add js files as script tags
const html = htmlFile.split('</body>'); const html = htmlFile.split('</body>');
html[0] = `${html[0]}<script>${file.content}</script>`; html[0] = `${html[0]}<script>${file.content}</script>`;
htmlFile = html.join('</body>'); htmlFile = html.join('</body>');
}); });
cssFiles.forEach(file => { // Add css files as style tags cssFiles.forEach((file) => { // Add css files as style tags
const html = htmlFile.split('</head>'); const html = htmlFile.split('</head>');
html[0] = `${html[0]}<style>${file.content}</style>`; html[0] = `${html[0]}<style>${file.content}</style>`;
htmlFile = html.join('</head>'); htmlFile = html.join('</head>');
}); });
linkedFiles.forEach(file => { // Add linked files as link tags linkedFiles.forEach((file) => { // Add linked files as link tags
const html = htmlFile.split('<head>'); const html = htmlFile.split('<head>');
html[1] = `<link href=${file.url}>${html[1]}`; html[1] = `<link href=${file.url}>${html[1]}`;
htmlFile = html.join('<head>'); htmlFile = html.join('<head>');
@ -112,3 +112,6 @@ export function get404Sketch(callback) {
} }
}); });
} }
export default get404Sketch;

@ -1 +1 @@
Subproject commit a1c126721ac667f7750ec181c8ccf363f2658d8b Subproject commit 9fa934af4ca85853ba2fe911558d5f658621ea0b