import PropTypes from 'prop-types';
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { withTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet';
import SplitPane from 'react-split-pane';
import Editor from '../components/Editor';
import Sidebar from '../components/Sidebar';
import PreviewFrame from '../components/PreviewFrame';
import Toolbar from '../components/Toolbar';
import Preferences from '../components/Preferences/index';
import NewFileModal from '../components/NewFileModal';
import NewFolderModal from '../components/NewFolderModal';
import UploadFileModal from '../components/UploadFileModal';
import ShareModal from '../components/ShareModal';
import KeyboardShortcutModal from '../components/KeyboardShortcutModal';
import ErrorModal from '../components/ErrorModal';
import Nav from '../../../components/Nav';
import Console from '../components/Console';
import Toast from '../components/Toast';
import * as FileActions from '../actions/files';
import * as IDEActions from '../actions/ide';
import * as ProjectActions from '../actions/project';
import * as EditorAccessibilityActions from '../actions/editorAccessibility';
import * as PreferencesActions from '../actions/preferences';
import * as UserActions from '../../User/actions';
import * as ToastActions from '../actions/toast';
import * as ConsoleActions from '../actions/console';
import { getHTMLFile } from '../reducers/files';
import Overlay from '../../App/components/Overlay';
import About from '../components/About';
import AddToCollectionList from '../components/AddToCollectionList';
import Feedback from '../components/Feedback';
import { CollectionSearchbar } from '../components/Searchbar';
function getTitle(props) {
const { id } = props.project;
return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor';
}
function isUserOwner(props) {
return props.project.owner && props.project.owner.id === props.user.id;
}
function warnIfUnsavedChanges(props) {
// eslint-disable-line
const { route } = props.route;
if (
route &&
route.action === 'PUSH' &&
(route.pathname === '/login' || route.pathname === '/signup')
) {
// don't warn
props.persistState();
window.onbeforeunload = null;
} else if (
route &&
(props.location.pathname === '/login' ||
props.location.pathname === '/signup')
) {
// don't warn
props.persistState();
window.onbeforeunload = null;
} else if (props.ide.unsavedChanges) {
if (!window.confirm(props.t('Nav.WarningUnsavedChanges'))) {
return false;
}
props.setUnsavedChanges(false);
return true;
}
return false;
}
class IDEView extends React.Component {
constructor(props) {
super(props);
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
this.state = {
consoleSize: props.ide.consoleIsExpanded ? 150 : 29,
sidebarSize: props.ide.sidebarIsExpanded ? 160 : 20,
};
}
componentDidMount() {
// If page doesn't reload after Sign In then we need
// to force cleared state to be cleared
this.props.clearPersistedState();
this.props.stopSketch();
if (this.props.params.project_id) {
const { project_id: id, username } = this.props.params;
if (id !== this.props.project.id) {
this.props.getProject(id, username);
}
}
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
document.addEventListener('keydown', this.handleGlobalKeydown, false);
this.props.router.setRouteLeaveHook(
this.props.route,
this.handleUnsavedChanges
);
window.onbeforeunload = this.handleUnsavedChanges;
this.autosaveInterval = null;
}
componentWillReceiveProps(nextProps) {
if (nextProps.location !== this.props.location) {
this.props.setPreviousPath(this.props.location.pathname);
}
if (this.props.ide.consoleIsExpanded !== nextProps.ide.consoleIsExpanded) {
this.setState({
consoleSize: nextProps.ide.consoleIsExpanded ? 150 : 29,
});
}
if (this.props.ide.sidebarIsExpanded !== nextProps.ide.sidebarIsExpanded) {
this.setState({
sidebarSize: nextProps.ide.sidebarIsExpanded ? 160 : 20,
});
}
}
componentWillUpdate(nextProps) {
if (nextProps.params.project_id && !this.props.params.project_id) {
if (nextProps.params.project_id !== nextProps.project.id) {
this.props.getProject(nextProps.params.project_id);
}
}
}
componentDidUpdate(prevProps) {
if (isUserOwner(this.props) && this.props.project.id) {
if (
this.props.preferences.autosave &&
this.props.ide.unsavedChanges &&
!this.props.ide.justOpenedProject
) {
if (
this.props.selectedFile.name === prevProps.selectedFile.name &&
this.props.selectedFile.content !== prevProps.selectedFile.content
) {
if (this.autosaveInterval) {
clearTimeout(this.autosaveInterval);
}
console.log('will save project in 20 seconds');
this.autosaveInterval = setTimeout(this.props.autosaveProject, 20000);
}
} else if (this.autosaveInterval && !this.props.preferences.autosave) {
clearTimeout(this.autosaveInterval);
this.autosaveInterval = null;
}
} else if (this.autosaveInterval) {
clearTimeout(this.autosaveInterval);
this.autosaveInterval = null;
}
if (this.props.route.path !== prevProps.route.path) {
this.props.router.setRouteLeaveHook(this.props.route, () =>
warnIfUnsavedChanges(this.props));
}
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleGlobalKeydown, false);
clearTimeout(this.autosaveInterval);
this.autosaveInterval = null;
}
handleGlobalKeydown(e) {
// 83 === s
if (
e.keyCode === 83 &&
((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))
) {
e.preventDefault();
e.stopPropagation();
if (
isUserOwner(this.props) ||
(this.props.user.authenticated && !this.props.project.owner)
) {
this.props.saveProject(this.cmController.getContent());
} else if (this.props.user.authenticated) {
this.props.cloneProject();
} else {
this.props.showErrorModal('forceAuthentication');
}
// 13 === enter
} else if (
e.keyCode === 13 &&
e.shiftKey &&
((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))
) {
e.preventDefault();
e.stopPropagation();
this.props.stopSketch();
} else if (
e.keyCode === 13 &&
((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))
) {
e.preventDefault();
e.stopPropagation();
this.props.startSketch();
// 50 === 2
} else if (
e.keyCode === 50 &&
((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac)) &&
e.shiftKey
) {
e.preventDefault();
this.props.setAllAccessibleOutput(false);
// 49 === 1
} else if (
e.keyCode === 49 &&
((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac)) &&
e.shiftKey
) {
e.preventDefault();
this.props.setAllAccessibleOutput(true);
} else if (
e.keyCode === 66 &&
((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))
) {
e.preventDefault();
if (!this.props.ide.sidebarIsExpanded) {
this.props.expandSidebar();
} else {
this.props.collapseSidebar();
}
} else if (e.keyCode === 192 && e.ctrlKey) {
e.preventDefault();
if (this.props.ide.consoleIsExpanded) {
this.props.collapseConsole();
} else {
this.props.expandConsole();
}
} else if (e.keyCode === 27) {
if (this.props.ide.newFolderModalVisible) {
this.props.closeNewFolderModal();
} else if (this.props.ide.uploadFileModalVisible) {
this.props.closeUploadFileModal();
} else if (this.props.ide.modalIsVisible) {
this.props.closeNewFileModal();
}
}
}
handleUnsavedChanges = () => warnIfUnsavedChanges(this.props);
render() {
return (
{getTitle(this.props)}
{this.props.toast.isVisible &&
}
{this.props.ide.preferencesIsVisible && (
)}
this.setState({ sidebarSize: size })}
onDragFinished={this._handleSidebarPaneOnDragFinished}
allowResize={this.props.ide.sidebarIsExpanded}
minSize={20}
>
{
this.overlay.style.display = 'block';
}}
onDragFinished={() => {
this.overlay.style.display = 'none';
}}
resizerStyle={{
borderLeftWidth: '2px',
borderRightWidth: '2px',
width: '2px',
margin: '0px 0px',
}}
>
this.setState({ consoleSize: size })}
allowResize={this.props.ide.consoleIsExpanded}
className="editor-preview-subpanel"
>
{
this.cmController = ctl;
}}
/>
{this.props.t('Toolbar.Preview')}
{
this.overlay = element;
}}
>
{((this.props.preferences.textOutput ||
this.props.preferences.gridOutput ||
this.props.preferences.soundOutput) &&
this.props.ide.isPlaying) ||
this.props.ide.isAccessibleOutputPlaying}
{this.props.ide.modalIsVisible &&
}
{this.props.ide.newFolderModalVisible && (
)}
{this.props.ide.uploadFileModalVisible && (
)}
{this.props.location.pathname === '/about' && (
)}
{this.props.location.pathname === '/feedback' && (
)}
{this.props.location.pathname.match(/add-to-collection$/) && (
}
isFixedHeight
>
)}
{this.props.ide.shareModalVisible && (
)}
{this.props.ide.keyboardShortcutVisible && (
)}
{this.props.ide.errorType && (
)}
);
}
}
IDEView.propTypes = {
params: PropTypes.shape({
project_id: PropTypes.string,
username: PropTypes.string,
reset_password_token: PropTypes.string,
}).isRequired,
location: PropTypes.shape({
pathname: PropTypes.string,
}).isRequired,
getProject: PropTypes.func.isRequired,
user: PropTypes.shape({
authenticated: PropTypes.bool.isRequired,
id: PropTypes.string,
username: PropTypes.string,
}).isRequired,
saveProject: PropTypes.func.isRequired,
ide: PropTypes.shape({
isPlaying: PropTypes.bool.isRequired,
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
consoleEvent: PropTypes.array, // eslint-disable-line
modalIsVisible: PropTypes.bool.isRequired,
sidebarIsExpanded: PropTypes.bool.isRequired,
consoleIsExpanded: PropTypes.bool.isRequired,
preferencesIsVisible: PropTypes.bool.isRequired,
projectOptionsVisible: PropTypes.bool.isRequired,
newFolderModalVisible: PropTypes.bool.isRequired,
shareModalVisible: PropTypes.bool.isRequired,
shareModalProjectId: PropTypes.string.isRequired,
shareModalProjectName: PropTypes.string.isRequired,
shareModalProjectUsername: PropTypes.string.isRequired,
editorOptionsVisible: PropTypes.bool.isRequired,
keyboardShortcutVisible: PropTypes.bool.isRequired,
unsavedChanges: PropTypes.bool.isRequired,
infiniteLoop: PropTypes.bool.isRequired,
previewIsRefreshing: PropTypes.bool.isRequired,
infiniteLoopMessage: PropTypes.string.isRequired,
projectSavedTime: PropTypes.string,
previousPath: PropTypes.string.isRequired,
justOpenedProject: PropTypes.bool.isRequired,
errorType: PropTypes.string,
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
uploadFileModalVisible: PropTypes.bool.isRequired,
}).isRequired,
stopSketch: PropTypes.func.isRequired,
project: PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string.isRequired,
owner: PropTypes.shape({
username: PropTypes.string,
id: PropTypes.string,
}),
updatedAt: PropTypes.string,
}).isRequired,
editorAccessibility: PropTypes.shape({
lintMessages: PropTypes.array.isRequired, // eslint-disable-line
}).isRequired,
updateLintMessage: PropTypes.func.isRequired,
clearLintMessage: PropTypes.func.isRequired,
preferences: PropTypes.shape({
fontSize: PropTypes.number.isRequired,
autosave: PropTypes.bool.isRequired,
linewrap: PropTypes.bool.isRequired,
lineNumbers: PropTypes.bool.isRequired,
lintWarning: PropTypes.bool.isRequired,
textOutput: PropTypes.bool.isRequired,
gridOutput: PropTypes.bool.isRequired,
soundOutput: PropTypes.bool.isRequired,
theme: PropTypes.string.isRequired,
autorefresh: PropTypes.bool.isRequired,
}).isRequired,
closePreferences: PropTypes.func.isRequired,
setFontSize: PropTypes.func.isRequired,
setAutosave: PropTypes.func.isRequired,
setLineNumbers: PropTypes.func.isRequired,
setLinewrap: PropTypes.func.isRequired,
setLintWarning: PropTypes.func.isRequired,
setTextOutput: PropTypes.func.isRequired,
setGridOutput: PropTypes.func.isRequired,
setSoundOutput: PropTypes.func.isRequired,
setAllAccessibleOutput: PropTypes.func.isRequired,
files: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
})).isRequired,
updateFileContent: PropTypes.func.isRequired,
selectedFile: PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}).isRequired,
setSelectedFile: PropTypes.func.isRequired,
htmlFile: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
}).isRequired,
dispatchConsoleEvent: PropTypes.func.isRequired,
newFile: PropTypes.func.isRequired,
expandSidebar: PropTypes.func.isRequired,
collapseSidebar: PropTypes.func.isRequired,
cloneProject: PropTypes.func.isRequired,
expandConsole: PropTypes.func.isRequired,
collapseConsole: PropTypes.func.isRequired,
deleteFile: PropTypes.func.isRequired,
updateFileName: PropTypes.func.isRequired,
openProjectOptions: PropTypes.func.isRequired,
closeProjectOptions: PropTypes.func.isRequired,
newFolder: PropTypes.func.isRequired,
closeNewFolderModal: PropTypes.func.isRequired,
closeNewFileModal: PropTypes.func.isRequired,
createFolder: PropTypes.func.isRequired,
closeShareModal: PropTypes.func.isRequired,
showEditorOptions: PropTypes.func.isRequired,
closeEditorOptions: PropTypes.func.isRequired,
showKeyboardShortcutModal: PropTypes.func.isRequired,
closeKeyboardShortcutModal: PropTypes.func.isRequired,
toast: PropTypes.shape({
isVisible: PropTypes.bool.isRequired,
}).isRequired,
autosaveProject: PropTypes.func.isRequired,
router: PropTypes.shape({
setRouteLeaveHook: PropTypes.func,
}).isRequired,
route: PropTypes.oneOfType([PropTypes.object, PropTypes.element]).isRequired,
setUnsavedChanges: PropTypes.func.isRequired,
setTheme: PropTypes.func.isRequired,
endSketchRefresh: PropTypes.func.isRequired,
startRefreshSketch: PropTypes.func.isRequired,
setBlobUrl: PropTypes.func.isRequired,
setPreviousPath: PropTypes.func.isRequired,
console: PropTypes.arrayOf(PropTypes.shape({
method: PropTypes.string.isRequired,
args: PropTypes.arrayOf(PropTypes.string),
})).isRequired,
clearConsole: PropTypes.func.isRequired,
showErrorModal: PropTypes.func.isRequired,
hideErrorModal: PropTypes.func.isRequired,
clearPersistedState: PropTypes.func.isRequired,
showRuntimeErrorWarning: PropTypes.func.isRequired,
hideRuntimeErrorWarning: PropTypes.func.isRequired,
startSketch: PropTypes.func.isRequired,
openUploadFileModal: PropTypes.func.isRequired,
closeUploadFileModal: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
function mapStateToProps(state) {
return {
files: state.files,
selectedFile:
state.files.find(file => file.isSelectedFile) ||
state.files.find(file => file.name === 'sketch.js') ||
state.files.find(file => file.name !== 'root'),
htmlFile: getHTMLFile(state.files),
ide: state.ide,
preferences: state.preferences,
editorAccessibility: state.editorAccessibility,
user: state.user,
project: state.project,
toast: state.toast,
console: state.console,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(
Object.assign(
{},
EditorAccessibilityActions,
FileActions,
ProjectActions,
IDEActions,
PreferencesActions,
UserActions,
ToastActions,
ConsoleActions
),
dispatch
);
}
export default withTranslation()(withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView)));