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'; 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('WarningUnsavedChanges'))) { return false; } props.setUnsavedChanges(false); return true; } } 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 && }
); } } 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, 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, }).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('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView)));