Merge branch 'development' of https://github.com/catarak/p5.js-web-editor into console

This commit is contained in:
therewasaguy 2016-07-12 15:09:45 -04:00
commit fae5ea086e
22 changed files with 407 additions and 94 deletions

View file

@ -8,6 +8,12 @@ export const OPEN_PREFERENCES = 'OPEN_PREFERENCES';
export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES'; export const CLOSE_PREFERENCES = 'CLOSE_PREFERENCES';
export const INCREASE_FONTSIZE = 'INCREASE_FONTSIZE'; export const INCREASE_FONTSIZE = 'INCREASE_FONTSIZE';
export const DECREASE_FONTSIZE = 'DECREASE_FONTSIZE'; export const DECREASE_FONTSIZE = 'DECREASE_FONTSIZE';
export const UPDATE_FONTSIZE = 'UPDATE_FONTSIZE';
export const INCREASE_INDENTATION = 'INCREASE_INDENTATION';
export const DECREASE_INDENTATION = 'DECREASE_INDENTATION';
export const UPDATE_INDENTATION = 'UPDATE_INDENTATION';
export const INDENT_WITH_SPACE = 'INDENT_WITH_SPACE';
export const INDENT_WITH_TAB = 'INDENT_WITH_TAB';
export const AUTH_USER = 'AUTH_USER'; export const AUTH_USER = 'AUTH_USER';
export const UNAUTH_USER = 'UNAUTH_USER'; export const UNAUTH_USER = 'UNAUTH_USER';
@ -23,5 +29,7 @@ export const NEW_PROJECT = 'NEW_PROJECT';
export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECT = 'SET_PROJECT';
export const SET_PROJECTS = 'SET_PROJECTS'; export const SET_PROJECTS = 'SET_PROJECTS';
export const SET_SELECTED_FILE = 'SET_SELECTED_FILE';
// eventually, handle errors more specifically and better // eventually, handle errors more specifically and better
export const ERROR = 'ERROR'; export const ERROR = 'ERROR';

View file

@ -17,3 +17,10 @@ export function stopSketch() {
type: ActionTypes.STOP_SKETCH type: ActionTypes.STOP_SKETCH
}; };
} }
export function setSelectedFile(fileId) {
return {
type: ActionTypes.SET_SELECTED_FILE,
selectedFile: fileId
};
}

View file

@ -23,3 +23,43 @@ export function decreaseFont() {
type: ActionTypes.DECREASE_FONTSIZE type: ActionTypes.DECREASE_FONTSIZE
}; };
} }
export function updateFont(event) {
const value = event.target.value;
return {
type: ActionTypes.UPDATE_FONTSIZE,
value
};
}
export function increaseIndentation() {
return {
type: ActionTypes.INCREASE_INDENTATION
};
}
export function decreaseIndentation() {
return {
type: ActionTypes.DECREASE_INDENTATION
};
}
export function updateIndentation() {
const value = event.target.value;
return {
type: ActionTypes.UPDATE_INDENTATION,
value
};
}
export function indentWithTab() {
return {
type: ActionTypes.INDENT_WITH_TAB
};
}
export function indentWithSpace() {
return {
type: ActionTypes.INDENT_WITH_SPACE
};
}

View file

@ -12,7 +12,8 @@ export function getProject(id) {
dispatch({ dispatch({
type: ActionTypes.SET_PROJECT, type: ActionTypes.SET_PROJECT,
project: response.data, project: response.data,
files: response.data.files files: response.data.files,
selectedFile: response.data.selectedFile
}); });
}) })
.catch(response => dispatch({ .catch(response => dispatch({
@ -34,7 +35,7 @@ export function saveProject() {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const formParams = Object.assign({}, state.project); const formParams = Object.assign({}, state.project);
formParams.files = state.files; formParams.files = [...state.files];
if (state.project.id) { if (state.project.id) {
axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true }) axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
.then(() => { .then(() => {
@ -47,6 +48,12 @@ export function saveProject() {
error: response.data error: response.data
})); }));
} else { } else {
// this might be unnecessary, but to prevent collisions in mongodb
formParams.files.map(file => {
const newFile = Object.assign({}, file);
delete newFile.id;
return newFile;
});
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
.then(response => { .then(response => {
browserHistory.push(`/projects/${response.data.id}`); browserHistory.push(`/projects/${response.data.id}`);
@ -54,6 +61,7 @@ export function saveProject() {
type: ActionTypes.NEW_PROJECT, type: ActionTypes.NEW_PROJECT,
name: response.data.name, name: response.data.name,
id: response.data.id, id: response.data.id,
selectedFile: response.data.selectedFile,
files: response.data.files files: response.data.files
}); });
}) })
@ -75,6 +83,7 @@ export function createProject() {
type: ActionTypes.NEW_PROJECT, type: ActionTypes.NEW_PROJECT,
name: response.data.name, name: response.data.name,
id: response.data.id, id: response.data.id,
selectedFile: response.data.selectedFile,
files: response.data.files files: response.data.files
}); });
}) })

View file

@ -8,25 +8,34 @@ class Editor extends React.Component {
componentDidMount() { componentDidMount() {
this._cm = CodeMirror(this.refs.container, { // eslint-disable-line this._cm = CodeMirror(this.refs.container, { // eslint-disable-line
theme: 'p5-widget', theme: 'p5-widget',
value: this.props.content, value: this.props.file.content,
lineNumbers: true, lineNumbers: true,
styleActiveLine: true, styleActiveLine: true,
mode: 'javascript' mode: 'javascript'
}); });
this._cm.on('change', () => { // eslint-disable-line this._cm.on('change', () => { // eslint-disable-line
this.props.updateFileContent('sketch.js', this._cm.getValue()); // this.props.updateFileContent('sketch.js', this._cm.getValue());
this.props.updateFileContent(this.props.file.name, this._cm.getValue());
}); });
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
this._cm.setOption('indentWithTabs', this.props.isTabIndent);
this._cm.setOption('tabSize', this.props.indentationAmount);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.content !== prevProps.content && if (this.props.file.content !== prevProps.file.content &&
this.props.content !== this._cm.getValue()) { this.props.file.content !== this._cm.getValue()) {
this._cm.setValue(this.props.content); // eslint-disable-line no-underscore-dangle this._cm.setValue(this.props.file.content); // eslint-disable-line no-underscore-dangle
} }
if (this.props.fontSize !== prevProps.fontSize) { if (this.props.fontSize !== prevProps.fontSize) {
this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`;
} }
if (this.props.indentationAmount !== prevProps.indentationAmount) {
this._cm.setOption('tabSize', this.props.indentationAmount);
}
if (this.props.isTabIndent !== prevProps.isTabIndent) {
this._cm.setOption('indentWithTabs', this.props.isTabIndent);
}
} }
componentWillUnmount() { componentWillUnmount() {
@ -41,9 +50,14 @@ class Editor extends React.Component {
} }
Editor.propTypes = { Editor.propTypes = {
content: PropTypes.string.isRequired, indentationAmount: PropTypes.number.isRequired,
isTabIndent: PropTypes.bool.isRequired,
updateFileContent: PropTypes.func.isRequired, updateFileContent: PropTypes.func.isRequired,
fontSize: PropTypes.number.isRequired fontSize: PropTypes.number.isRequired,
file: PropTypes.shape({
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
})
}; };
export default Editor; export default Editor;

View file

@ -11,6 +11,14 @@ function Preferences(props) {
preferences: true, preferences: true,
'preferences--selected': props.isVisible 'preferences--selected': props.isVisible
}); });
let preferencesTabOptionClass = classNames({
preference__option: true,
'preference__option--selected': props.isTabIndent
});
let preferencesSpaceOptionClass = classNames({
preference__option: true,
'preference__option--selected': !props.isTabIndent
});
return ( return (
<div className={preferencesContainerClass} tabIndex="0"> <div className={preferencesContainerClass} tabIndex="0">
<div className="preferences__heading"> <div className="preferences__heading">
@ -19,16 +27,37 @@ function Preferences(props) {
<Isvg src={exitUrl} alt="Exit Preferences" /> <Isvg src={exitUrl} alt="Exit Preferences" />
</button> </button>
</div> </div>
<div className="preference"> <div className="preference">
<h3 className="preference__title">Text Size</h3> <h4 className="preference__title">Text Size</h4>
<button className="preference__plus-button" onClick={props.decreaseFont}> <button className="preference__plus-button" onClick={props.decreaseFont}>
<Isvg src={minusUrl} alt="Decrease Font Size" /> <Isvg src={minusUrl} alt="Decrease Font Size" />
<h6 className="preference__label">Decrease</h6>
</button> </button>
<p className="preference__value">{props.fontSize}</p>
<input className="preference__value" value={props.fontSize} onChange={props.updateFont}></input>
<button className="preference__minus-button" onClick={props.increaseFont}> <button className="preference__minus-button" onClick={props.increaseFont}>
<Isvg src={plusUrl} alt="Increase Font Size" /> <Isvg src={plusUrl} alt="Increase Font Size" />
<h6 className="preference__label">Increase</h6>
</button> </button>
</div> </div>
<div className="preference">
<h4 className="preference__title">Indentation Amount</h4>
<button className="preference__plus-button" onClick={props.decreaseIndentation}>
<Isvg src={minusUrl} alt="DecreaseIndentation Amount" />
<h6 className="preference__label">Decrease</h6>
</button>
<input className="preference__value" value={props.indentationAmount} onChange={props.updateIndentation}></input>
<button className="preference__minus-button" onClick={props.increaseIndentation}>
<Isvg src={plusUrl} alt="IncreaseIndentation Amount" />
<h6 className="preference__label">Increase</h6>
</button>
<div className="preference__vertical-list">
<button className={preferencesSpaceOptionClass} onClick={props.indentWithSpace}>Spaces</button>
<button className={preferencesTabOptionClass} onClick={props.indentWithTab}>Tabs</button>
</div>
</div>
</div> </div>
); );
} }
@ -37,8 +66,16 @@ Preferences.propTypes = {
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
closePreferences: PropTypes.func.isRequired, closePreferences: PropTypes.func.isRequired,
decreaseFont: PropTypes.func.isRequired, decreaseFont: PropTypes.func.isRequired,
updateFont: PropTypes.func.isRequired,
fontSize: PropTypes.number.isRequired, fontSize: PropTypes.number.isRequired,
increaseFont: PropTypes.func.isRequired increaseFont: PropTypes.func.isRequired,
indentationAmount: PropTypes.number.isRequired,
decreaseIndentation: PropTypes.func.isRequired,
increaseIndentation: PropTypes.func.isRequired,
updateIndentation: PropTypes.func.isRequired,
indentWithSpace: PropTypes.func.isRequired,
indentWithTab: PropTypes.func.isRequired,
isTabIndent: PropTypes.bool.isRequired
}; };
export default Preferences; export default Preferences;

View file

@ -1,5 +1,7 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import escapeStringRegexp from 'escape-string-regexp';
import srcDoc from 'srcdoc-polyfill';
class PreviewFrame extends React.Component { class PreviewFrame extends React.Component {
@ -13,11 +15,7 @@ class PreviewFrame extends React.Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.isPlaying !== prevProps.isPlaying) { if (this.props.isPlaying !== prevProps.isPlaying) {
if (this.props.isPlaying) { this.renderSketch();
this.renderSketch();
} else {
this.clearPreview();
}
} }
if (this.props.isPlaying && this.props.content !== prevProps.content) { if (this.props.isPlaying && this.props.content !== prevProps.content) {
@ -30,9 +28,33 @@ class PreviewFrame extends React.Component {
} }
clearPreview() { clearPreview() {
const doc = ReactDOM.findDOMNode(this).contentDocument; const doc = ReactDOM.findDOMNode(this);
doc.write(''); doc.srcDoc = '';
doc.close(); }
injectLocalFiles() {
let htmlFile = this.props.htmlFile.content;
this.props.jsFiles.forEach(jsFile => {
const fileName = escapeStringRegexp(jsFile.name);
const fileRegex = new RegExp(`<script.*?src=('|")((\.\/)|\/)?${fileName}('|").*?>([\s\S]*?)<\/script>`, 'gmi');
htmlFile = htmlFile.replace(fileRegex, `<script>\n${jsFile.content}\n</script>`);
});
this.props.cssFiles.forEach(cssFile => {
const fileName = escapeStringRegexp(cssFile.name);
const fileRegex = new RegExp(`<link.*?href=('|")((\.\/)|\/)?${fileName}('|").*?>`, 'gmi');
htmlFile = htmlFile.replace(fileRegex, `<style>\n${cssFile.content}\n</style>`);
});
// const htmlHead = htmlFile.match(/(?:<head.*?>)([\s\S]*?)(?:<\/head>)/gmi);
// const headRegex = new RegExp('head', 'i');
// let htmlHeadContents = htmlHead[0].split(headRegex)[1];
// htmlHeadContents = htmlHeadContents.slice(1, htmlHeadContents.length - 2);
// htmlHeadContents += '<link rel="stylesheet" type="text/css" href="/preview-styles.css" />\n';
// htmlFile = htmlFile.replace(/(?:<head.*?>)([\s\S]*?)(?:<\/head>)/gmi, `<head>\n${htmlHeadContents}\n</head>`);
return htmlFile;
} }
hijackConsole() { hijackConsole() {
@ -55,16 +77,18 @@ class PreviewFrame extends React.Component {
} }
renderSketch() { renderSketch() {
const doc = ReactDOM.findDOMNode(this).contentDocument; const doc = ReactDOM.findDOMNode(this);
this.clearPreview(); if (this.props.isPlaying) {
ReactDOM.render(this.props.head, doc.head); // TODO add polyfill for this
const p5Script = doc.createElement('script'); // doc.srcdoc = this.injectLocalFiles();
p5Script.setAttribute('src', 'https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.0/p5.min.js'); srcDoc.set(doc, this.injectLocalFiles());
doc.body.appendChild(p5Script); } else {
// doc.srcdoc = '';
const sketchScript = doc.createElement('script'); srcDoc.set(doc, '');
sketchScript.textContent = this.props.content; doc.contentWindow.document.open();
doc.body.appendChild(sketchScript); doc.contentWindow.document.write('');
doc.contentWindow.document.close();
}
} }
renderFrameContents() { renderFrameContents() {
@ -81,8 +105,8 @@ class PreviewFrame extends React.Component {
<iframe <iframe
className="preview-frame" className="preview-frame"
frameBorder="0" frameBorder="0"
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-modals allow-forms"
title="sketch output" title="sketch output"
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-modals allow-forms"
></iframe> ></iframe>
); );
} }
@ -91,7 +115,12 @@ class PreviewFrame extends React.Component {
PreviewFrame.propTypes = { PreviewFrame.propTypes = {
isPlaying: PropTypes.bool.isRequired, isPlaying: PropTypes.bool.isRequired,
head: PropTypes.object.isRequired, head: PropTypes.object.isRequired,
content: PropTypes.string.isRequired content: PropTypes.string.isRequired,
htmlFile: PropTypes.shape({
content: PropTypes.string.isRequired
}),
jsFiles: PropTypes.array.isRequired,
cssFiles: PropTypes.array.isRequired
}; };
export default PreviewFrame; export default PreviewFrame;

View file

@ -1,22 +1,34 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import classNames from 'classnames';
function Sidebar(props) { function Sidebar(props) {
return ( return (
<section className="sidebar"> <section className="sidebar">
<ul className="sidebar__file-list"> <ul className="sidebar__file-list">
{props.files.map(file => {props.files.map(file => {
<li let itemClass = classNames({
className="sidebar__file-item" 'sidebar__file-item': true,
key={file.id} 'sidebar__file-item--selected': file.id === props.selectedFile.id
>{file.name}</li> });
)} return (
<li
className={itemClass}
key={file.id}
onClick={() => props.setSelectedFile(file.id)}
>{file.name}</li>
);
})}
</ul> </ul>
</section> </section>
); );
} }
Sidebar.propTypes = { Sidebar.propTypes = {
files: PropTypes.array.isRequired files: PropTypes.array.isRequired,
selectedFile: PropTypes.shape({
id: PropTypes.string.isRequired
}),
setSelectedFile: PropTypes.func.isRequired
}; };
export default Sidebar; export default Sidebar;

View file

@ -11,6 +11,7 @@ import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide'; import * as IDEActions from '../actions/ide';
import * as PreferencesActions from '../actions/preferences'; import * as PreferencesActions from '../actions/preferences';
import * as ProjectActions from '../actions/project'; import * as ProjectActions from '../actions/project';
import { getFile, getHTMLFile, getJSFiles, getCSSFiles } from '../reducers/files';
class IDEView extends React.Component { class IDEView extends React.Component {
componentDidMount() { componentDidMount() {
@ -43,16 +44,35 @@ class IDEView extends React.Component {
closePreferences={this.props.closePreferences} closePreferences={this.props.closePreferences}
increaseFont={this.props.increaseFont} increaseFont={this.props.increaseFont}
decreaseFont={this.props.decreaseFont} decreaseFont={this.props.decreaseFont}
updateFont={this.props.updateFont}
fontSize={this.props.preferences.fontSize} fontSize={this.props.preferences.fontSize}
increaseIndentation={this.props.increaseIndentation}
decreaseIndentation={this.props.decreaseIndentation}
updateIndentation={this.props.updateIndentation}
indentationAmount={this.props.preferences.indentationAmount}
isTabIndent={this.props.preferences.isTabIndent}
indentWithSpace={this.props.indentWithSpace}
indentWithTab={this.props.indentWithTab}
/>
<Sidebar
files={this.props.files}
selectedFile={this.props.selectedFile}
setSelectedFile={this.props.setSelectedFile}
/> />
<Sidebar files={this.props.files} />
<Editor <Editor
content={this.props.files[0].content} file={this.props.selectedFile}
updateFileContent={this.props.updateFileContent} updateFileContent={this.props.updateFileContent}
fontSize={this.props.preferences.fontSize} fontSize={this.props.preferences.fontSize}
indentationAmount={this.props.preferences.indentationAmount}
isTabIndent={this.props.preferences.isTabIndent}
files={this.props.files}
/> />
<PreviewFrame <PreviewFrame
content={this.props.files[0].content} htmlFile={this.props.htmlFile}
jsFiles={this.props.jsFiles}
cssFiles={this.props.cssFiles}
files={this.props.files}
content={this.props.selectedFile.content}
head={ head={
<link type="text/css" rel="stylesheet" href="/preview-styles.css" /> <link type="text/css" rel="stylesheet" href="/preview-styles.css" />
} }
@ -83,18 +103,38 @@ IDEView.propTypes = {
openPreferences: PropTypes.func.isRequired, openPreferences: PropTypes.func.isRequired,
preferences: PropTypes.shape({ preferences: PropTypes.shape({
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
fontSize: PropTypes.number.isRequired fontSize: PropTypes.number.isRequired,
indentationAmount: PropTypes.number.isRequired,
isTabIndent: PropTypes.bool.isRequired
}).isRequired, }).isRequired,
closePreferences: PropTypes.func.isRequired, closePreferences: PropTypes.func.isRequired,
increaseFont: PropTypes.func.isRequired, increaseFont: PropTypes.func.isRequired,
decreaseFont: PropTypes.func.isRequired, decreaseFont: PropTypes.func.isRequired,
updateFont: PropTypes.func.isRequired,
increaseIndentation: PropTypes.func.isRequired,
decreaseIndentation: PropTypes.func.isRequired,
updateIndentation: PropTypes.func.isRequired,
indentWithSpace: PropTypes.func.isRequired,
indentWithTab: PropTypes.func.isRequired,
files: PropTypes.array.isRequired, files: PropTypes.array.isRequired,
updateFileContent: PropTypes.func.isRequired updateFileContent: PropTypes.func.isRequired,
selectedFile: PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired
}),
setSelectedFile: PropTypes.func.isRequired,
htmlFile: PropTypes.object.isRequired,
jsFiles: PropTypes.array.isRequired,
cssFiles: PropTypes.array.isRequired
}; };
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
files: state.files, files: state.files,
selectedFile: getFile(state.files, state.ide.selectedFile),
htmlFile: getHTMLFile(state.files),
jsFiles: getJSFiles(state.files),
cssFiles: getCSSFiles(state.files),
ide: state.ide, ide: state.ide,
preferences: state.preferences, preferences: state.preferences,
user: state.user, user: state.user,

View file

@ -9,10 +9,13 @@ function draw() {
}`; }`;
const defaultHTML = const defaultHTML =
` `<!DOCTYPE html>
<!DOCTYPE html>
<html> <html>
<head> <head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.2/p5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.2/addons/p5.dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.2/addons/p5.sound.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head> </head>
<body> <body>
<script src="sketch.js"></script> <script src="sketch.js"></script>
@ -20,14 +23,30 @@ const defaultHTML =
</html> </html>
`; `;
const defaultCSS =
`html, body {
overflow: hidden;
margin: 0;
padding: 0;
}
`;
// if the project has never been saved,
const initialState = [ const initialState = [
{ {
name: 'sketch.js', name: 'sketch.js',
content: defaultSketch content: defaultSketch,
id: '1'
}, },
{ {
name: 'index.html', name: 'index.html',
content: defaultHTML content: defaultHTML,
id: '2'
},
{
name: 'style.css',
content: defaultCSS,
id: '3'
}]; }];
@ -50,4 +69,9 @@ const files = (state = initialState, action) => {
} }
}; };
export const getFile = (state, id) => state.filter(file => file.id === id)[0];
export const getHTMLFile = (state) => state.filter(file => file.name.match(/.*\.html$/))[0];
export const getJSFiles = (state) => state.filter(file => file.name.match(/.*\.js$/));
export const getCSSFiles = (state) => state.filter(file => file.name.match(/.*\.css$/));
export default files; export default files;

View file

@ -1,23 +1,22 @@
import * as ActionTypes from '../../../constants'; import * as ActionTypes from '../../../constants';
const initialState = { const initialState = {
isPlaying: false isPlaying: false,
selectedFile: '1'
}; };
const ide = (state = initialState, action) => { const ide = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case ActionTypes.TOGGLE_SKETCH: case ActionTypes.TOGGLE_SKETCH:
return { return Object.assign({}, state, { isPlaying: !state.isPlaying });
isPlaying: !state.isPlaying
};
case ActionTypes.START_SKETCH: case ActionTypes.START_SKETCH:
return { return Object.assign({}, state, { isPlaying: true });
isPlaying: true
};
case ActionTypes.STOP_SKETCH: case ActionTypes.STOP_SKETCH:
return { return Object.assign({}, state, { isPlaying: false });
isPlaying: false case ActionTypes.SET_SELECTED_FILE:
}; case ActionTypes.SET_PROJECT:
case ActionTypes.NEW_PROJECT:
return Object.assign({}, state, { selectedFile: action.selectedFile });
default: default:
return state; return state;
} }

View file

@ -2,31 +2,53 @@ import * as ActionTypes from '../../../constants';
const initialState = { const initialState = {
isVisible: false, isVisible: false,
fontSize: 18 fontSize: 18,
indentationAmount: 2,
isTabIndent: true
}; };
const preferences = (state = initialState, action) => { const preferences = (state = initialState, action) => {
switch (action.type) { switch (action.type) {
case ActionTypes.OPEN_PREFERENCES: case ActionTypes.OPEN_PREFERENCES:
return { return Object.assign({}, state, {
isVisible: true, isVisible: true
fontSize: state.fontSize });
};
case ActionTypes.CLOSE_PREFERENCES: case ActionTypes.CLOSE_PREFERENCES:
return { return Object.assign({}, state, {
isVisible: false, isVisible: false
fontSize: state.fontSize });
};
case ActionTypes.INCREASE_FONTSIZE: case ActionTypes.INCREASE_FONTSIZE:
return { return Object.assign({}, state, {
isVisible: state.isVisible,
fontSize: state.fontSize + 2 fontSize: state.fontSize + 2
}; });
case ActionTypes.DECREASE_FONTSIZE: case ActionTypes.DECREASE_FONTSIZE:
return { return Object.assign({}, state, {
isVisible: state.isVisible,
fontSize: state.fontSize - 2 fontSize: state.fontSize - 2
}; });
case ActionTypes.UPDATE_FONTSIZE:
return Object.assign({}, state, {
fontSize: action.value
});
case ActionTypes.INCREASE_INDENTATION:
return Object.assign({}, state, {
indentationAmount: state.indentationAmount + 2
});
case ActionTypes.DECREASE_INDENTATION:
return Object.assign({}, state, {
indentationAmount: state.indentationAmount - 2
});
case ActionTypes.UPDATE_INDENTATION:
return Object.assign({}, state, {
indentationAmount: action.value
});
case ActionTypes.INDENT_WITH_TAB:
return Object.assign({}, state, {
isTabIndent: true
});
case ActionTypes.INDENT_WITH_SPACE:
return Object.assign({}, state, {
isTabIndent: false
});
default: default:
return state; return state;
} }

View file

@ -69,6 +69,8 @@
@extend %toolbar-button; @extend %toolbar-button;
color: $light-primary-text-color; color: $light-primary-text-color;
background-color: $light-modal-button-background-color; background-color: $light-modal-button-background-color;
padding: 0;
line-height: #{50 / $base-font-size}rem;
& g { & g {
fill: $light-primary-text-color; fill: $light-primary-text-color;
} }
@ -82,9 +84,25 @@
} }
%fake-link { %fake-link {
color: $light-secondary-text-color; color: $light-inactive-text-color;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: $light-primary-text-color; color: $light-primary-text-color;
} }
} }
%preference-option {
background-color: $light-button-background-color;
color: $light-inactive-text-color;
font-size: #{14 / $base-font-size}rem;
cursor: pointer;
text-align: left;
margin-bottom: #{5 / $base-font-size}rem;
border: 0px;
&:hover {
color: $light-primary-text-color;
}
&--selected {
color: $light-primary-text-color;
}
}

View file

@ -18,7 +18,7 @@ body, input, button {
a { a {
text-decoration: none; text-decoration: none;
color: $light-secondary-text-color; color: $light-inactive-text-color;
&:hover { &:hover {
text-decoration: none; text-decoration: none;
color: $light-primary-text-color; color: $light-primary-text-color;
@ -46,7 +46,12 @@ h2 {
h3 { h3 {
font-weight: normal; font-weight: normal;
} }
h4 {
font-weight: normal;
}
h6 {
font-weight: normal;
}
thead { thead {
text-align: left; text-align: left;
} }

View file

@ -2,7 +2,7 @@
position: absolute; position: absolute;
top: #{66 / $base-font-size}rem; top: #{66 / $base-font-size}rem;
right: #{40 / $base-font-size}rem; right: #{40 / $base-font-size}rem;
width: #{276 / $base-font-size}rem; width: #{332 / $base-font-size}rem;
background-color: $light-button-background-color; background-color: $light-button-background-color;
display: none; display: none;
padding: #{16 / $base-font-size}rem #{26 / $base-font-size}rem; padding: #{16 / $base-font-size}rem #{26 / $base-font-size}rem;
@ -35,7 +35,10 @@
.preference { .preference {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; padding-bottom: #{40 / $base-font-size}rem;
& + & {
border-top: 2px dashed $light-button-border-color;
}
} }
.preference__title { .preference__title {
@ -44,8 +47,35 @@
} }
.preference__value { .preference__value {
border: 1px solid $light-button-border-color; border: 2px solid $light-button-border-color;
text-align: center; text-align: center;
line-height: #{48 / $base-font-size}rem; border-radius: 0%;
width: #{48 / $base-font-size}rem; width: #{48 / $base-font-size}rem;
height: #{44 / $base-font-size}rem;
margin: 0 #{28 / $base-font-size}rem;
padding: 0;
background-color: $light-button-background-color;
}
.preference__label {
margin: 0;
line-height: #{20 / $base-font-size}rem;
color: $light-inactive-text-color;
&:hover {
color: $light-inactive-text-color;
}
}
.preference__vertical-list {
display: flex;
flex-direction: column;
}
.preference__option {
@extend %preference-option;
list-style-type: none;
padding-left: #{28 / $base-font-size}rem;
&--selected {
@extend %preference-option--selected;
}
} }

View file

@ -1,10 +1,9 @@
.sidebar__file-list { .sidebar__file-list {
padding: #{4 / $base-font-size}rem #{20 / $base-font-size}rem;
border-top: 1px solid $ide-border-color; border-top: 1px solid $ide-border-color;
} }
.sidebar__file-item { .sidebar__file-item {
padding: #{4 / $base-font-size}rem 0; padding: #{8 / $base-font-size}rem #{20 / $base-font-size}rem;
color: $light-inactive-text-color; color: $light-inactive-text-color;
} }

View file

@ -45,12 +45,12 @@
} }
.toolbar__project-name { .toolbar__project-name {
color: $light-secondary-text-color; color: $light-inactive-text-color;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: $light-primary-text-color; color: $light-primary-text-color;
} }
&:focus { &:focus {
color: $light-secondary-text-color; color: $light-inactive-text-color;
} }
} }

View file

@ -60,11 +60,13 @@
"babel-core": "^6.8.0", "babel-core": "^6.8.0",
"bcrypt-nodejs": "0.0.3", "bcrypt-nodejs": "0.0.3",
"body-parser": "^1.15.1", "body-parser": "^1.15.1",
"bson-objectid": "^1.1.4",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"codemirror": "^5.14.2", "codemirror": "^5.14.2",
"connect-mongo": "^1.2.0", "connect-mongo": "^1.2.0",
"cookie-parser": "^1.4.1", "cookie-parser": "^1.4.1",
"dotenv": "^2.0.0", "dotenv": "^2.0.0",
"escape-string-regexp": "^1.0.5",
"eslint-loader": "^1.3.0", "eslint-loader": "^1.3.0",
"express": "^4.13.4", "express": "^4.13.4",
"express-session": "^1.13.0", "express-session": "^1.13.0",
@ -81,6 +83,7 @@
"redux": "^3.5.2", "redux": "^3.5.2",
"redux-form": "^5.2.5", "redux-form": "^5.2.5",
"redux-thunk": "^2.1.0", "redux-thunk": "^2.1.0",
"shortid": "^2.2.6" "shortid": "^2.2.6",
"srcdoc-polyfill": "^0.2.0"
} }
} }

View file

@ -2,8 +2,7 @@ import Project from '../models/project';
export function createProject(req, res) { export function createProject(req, res) {
const projectValues = { const projectValues = {
user: req.user ? req.user._id : undefined, // eslint-disable-line no-underscore-dangle user: req.user ? req.user._id : undefined // eslint-disable-line no-underscore-dangle
file: {}
}; };
Object.assign(projectValues, req.body); Object.assign(projectValues, req.body);

View file

@ -12,16 +12,24 @@ function draw() {
}` }`
const defaultHTML = const defaultHTML =
` `<!DOCTYPE html>
<!DOCTYPE html>
<html> <html>
<head> <head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.0/p5.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
</head> </head>
<body> <body>
<script src="sketch.js"></script> <script src="sketch.js"></script>
</body> </body>
</html> </html>
` `
const defaultCSS =
`html, body {
overflow: hidden;
margin: 0;
padding: 0;
}
`;
const fileSchema = new Schema({ const fileSchema = new Schema({
name: { type: String, default: 'sketch.js' }, name: { type: String, default: 'sketch.js' },
@ -39,8 +47,11 @@ fileSchema.set('toJSON', {
const projectSchema = new Schema({ const projectSchema = new Schema({
name: { type: String, default: "Hello p5.js, it's the server" }, name: { type: String, default: "Hello p5.js, it's the server" },
user: { type: Schema.Types.ObjectId, ref: 'User' }, user: { type: Schema.Types.ObjectId, ref: 'User' },
files: {type: [ fileSchema ], default: [{ name: 'sketch.js', content: defaultSketch, _id: new ObjectId() }, { name: 'index.html', content: defaultHTML, _id: new ObjectId() }]}, files: { type: [ fileSchema ], default: [{ name: 'sketch.js', content: defaultSketch, _id: new ObjectId() },
_id: { type: String, default: shortid.generate } { name: 'index.html', content: defaultHTML, _id: new ObjectId() },
{ name: 'style.css', content: defaultCSS, _id: new ObjectId() }]},
_id: { type: String, default: shortid.generate },
selectedFile: Schema.Types.ObjectId
}, { timestamps: true }); }, { timestamps: true });
projectSchema.virtual('id').get(function(){ projectSchema.virtual('id').get(function(){
@ -51,4 +62,12 @@ projectSchema.set('toJSON', {
virtuals: true virtuals: true
}); });
projectSchema.pre('save', function createSelectedFile(next) {
const project = this;
if (!project.selectedFile) {
project.selectedFile = project.files[0]._id; // eslint-disable-line no-underscore-dangle
return next();
}
});
export default mongoose.model('Project', projectSchema); export default mongoose.model('Project', projectSchema);

View file

@ -83,4 +83,3 @@ app.listen(serverConfig.port, (error) => {
}); });
export default app; export default app;