diff --git a/client/common/icons.jsx b/client/common/icons.jsx index c92fa2e7..d4a458bc 100644 --- a/client/common/icons.jsx +++ b/client/common/icons.jsx @@ -14,6 +14,7 @@ import Preferences from '../images/preferences.svg'; import Play from '../images/triangle-arrow-right.svg'; import More from '../images/more.svg'; import Code from '../images/code.svg'; +import Save from '../images/save.svg'; import Terminal from '../images/terminal.svg'; import Folder from '../images/folder-padded.svg'; @@ -87,6 +88,7 @@ export const PlayIcon = withLabel(Play); export const MoreIcon = withLabel(More); export const TerminalIcon = withLabel(Terminal); export const CodeIcon = withLabel(Code); +export const SaveIcon = withLabel(Save); export const FolderIcon = withLabel(Folder); diff --git a/client/components/mobile/ActionStrip.jsx b/client/components/mobile/ActionStrip.jsx index 6f72b34b..4446d89f 100644 --- a/client/components/mobile/ActionStrip.jsx +++ b/client/components/mobile/ActionStrip.jsx @@ -1,20 +1,16 @@ import React from 'react'; -import styled from 'styled-components'; import PropTypes from 'prop-types'; -import { bindActionCreators } from 'redux'; -import { useDispatch, useSelector } from 'react-redux'; +import styled from 'styled-components'; import { remSize, prop } from '../../theme'; import IconButton from './IconButton'; -import { TerminalIcon, FolderIcon } from '../../common/icons'; -import * as IDEActions from '../../modules/IDE/actions/ide'; const BottomBarContent = styled.div` padding: ${remSize(8)}; - display: flex; + display: grid; + grid-template-columns: repeat(8,1fr); svg { max-height: ${remSize(32)}; - } path { fill: ${prop('primaryTextColor')} !important } @@ -25,42 +21,28 @@ const BottomBarContent = styled.div` } `; -// Maybe this component shouldn't be connected, and instead just receive the `actions` prop -const ActionStrip = ({ toggleExplorer }) => { - const { expandConsole, collapseConsole } = bindActionCreators(IDEActions, useDispatch()); - const { consoleIsExpanded } = useSelector(state => state.ide); - - const actions = [ - { - icon: TerminalIcon, inverted: true, aria: 'Open terminal console', action: consoleIsExpanded ? collapseConsole : expandConsole - }, - { icon: FolderIcon, aria: 'Open files explorer', action: toggleExplorer } - ]; - - return ( - - {actions.map(({ - icon, aria, action, inverted - }) => - ( - action()} - />))} - - ); -}; +const ActionStrip = ({ actions }) => ( + + {actions.map(({ + icon, aria, action, inverted + }) => + ())} + ); ActionStrip.propTypes = { - toggleExplorer: PropTypes.func -}; - -ActionStrip.defaultProps = { - toggleExplorer: () => {} + actions: PropTypes.arrayOf(PropTypes.shape({ + icon: PropTypes.any, + aria: PropTypes.string.isRequired, + action: PropTypes.func.isRequired, + inverted: PropTypes.bool + })).isRequired }; export default ActionStrip; diff --git a/client/components/mobile/Header.jsx b/client/components/mobile/Header.jsx index 7b3fbae1..6492a44d 100644 --- a/client/components/mobile/Header.jsx +++ b/client/components/mobile/Header.jsx @@ -35,6 +35,12 @@ const HeaderDiv = styled.div` } & svg path { fill: ${textColor} !important; } + + .editor__unsaved-changes svg { + width: ${remSize(16)}; + padding: 0; + vertical-align: top + } `; const IconContainer = styled.div` @@ -71,8 +77,9 @@ const Header = ({ ); + Header.propTypes = { - title: PropTypes.string, + title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), subtitle: PropTypes.string, leftButton: PropTypes.element, children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]), diff --git a/client/images/save.svg b/client/images/save.svg new file mode 100644 index 00000000..ed9113fd --- /dev/null +++ b/client/images/save.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index b57cacd2..05215579 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -126,7 +126,7 @@ function getSynchedProject(currentState, responseProject) { }; } -export function saveProject(selectedFile = null, autosave = false) { +export function saveProject(selectedFile = null, autosave = false, mobile = false) { return (dispatch, getState) => { const state = getState(); if (state.project.isSaving) { @@ -185,16 +185,15 @@ export function saveProject(selectedFile = null, autosave = false) { .then((response) => { dispatch(endSavingProject()); const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data); + + dispatch(setNewProject(synchedProject)); + dispatch(setUnsavedChanges(false)); + browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); + if (hasChanges) { - dispatch(setNewProject(synchedProject)); - dispatch(setUnsavedChanges(false)); - browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); dispatch(setUnsavedChanges(true)); - } else { - dispatch(setNewProject(synchedProject)); - dispatch(setUnsavedChanges(false)); - browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`); } + dispatch(projectSaveSuccess()); if (!autosave) { if (state.preferences.autosave) { @@ -222,9 +221,9 @@ export function saveProject(selectedFile = null, autosave = false) { }; } -export function autosaveProject() { +export function autosaveProject(mobile = false) { return (dispatch, getState) => { - saveProject(null, true)(dispatch, getState); + saveProject(null, true, mobile)(dispatch, getState); }; } diff --git a/client/modules/IDE/components/Editor.jsx b/client/modules/IDE/components/Editor.jsx index f8269ed0..773898d4 100644 --- a/client/modules/IDE/components/Editor.jsx +++ b/client/modules/IDE/components/Editor.jsx @@ -27,6 +27,8 @@ import { CSSLint } from 'csslint'; import { HTMLHint } from 'htmlhint'; import classNames from 'classnames'; import { debounce } from 'lodash'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; import '../../../utils/htmlmixed'; import '../../../utils/p5-javascript'; import '../../../utils/webGL-clike'; @@ -40,6 +42,16 @@ import beepUrl from '../../../sounds/audioAlert.mp3'; import UnsavedChangesDotIcon from '../../../images/unsaved-changes-dot.svg'; import RightArrowIcon from '../../../images/right-arrow.svg'; import LeftArrowIcon from '../../../images/left-arrow.svg'; +import { getHTMLFile } from '../reducers/files'; + +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'; search(CodeMirror); @@ -413,4 +425,47 @@ Editor.defaultProps = { consoleEvents: [], }; -export default withTranslation()(Editor); + +function mapStateToProps(state) { + return { + files: state.files, + file: + 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, + + ...state.preferences, + ...state.ide, + ...state.project, + ...state.editorAccessibility, + isExpanded: state.ide.consoleIsExpanded, + projectSavedTime: state.project.updatedAt + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators( + Object.assign( + {}, + EditorAccessibilityActions, + FileActions, + ProjectActions, + IDEActions, + PreferencesActions, + UserActions, + ToastActions, + ConsoleActions + ), + dispatch + ); +} + +export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Editor)); diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 90c2c4b4..c5b358f1 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -343,46 +343,7 @@ class IDEView extends React.Component { allowResize={this.props.ide.consoleIsExpanded} className="editor-preview-subpanel" > - { - this.cmController = ctl; - }} - /> + { this.cmController = ctl; }} />
@@ -533,31 +494,25 @@ IDEView.propTypes = { }).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, + errorType: PropTypes.string, + keyboardShortcutVisible: 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, + previewIsRefreshing: PropTypes.bool.isRequired, + isPlaying: PropTypes.bool.isRequired, + isAccessibleOutputPlaying: PropTypes.bool.isRequired, + projectOptionsVisible: PropTypes.bool.isRequired, + preferencesIsVisible: PropTypes.bool.isRequired, + modalIsVisible: PropTypes.bool.isRequired, uploadFileModalVisible: PropTypes.bool.isRequired, + newFolderModalVisible: PropTypes.bool.isRequired, + justOpenedProject: PropTypes.bool.isRequired, + sidebarIsExpanded: PropTypes.bool.isRequired, + consoleIsExpanded: PropTypes.bool.isRequired, + unsavedChanges: PropTypes.bool.isRequired, }).isRequired, stopSketch: PropTypes.func.isRequired, project: PropTypes.shape({ @@ -572,11 +527,9 @@ IDEView.propTypes = { 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, + fontSize: PropTypes.number.isRequired, linewrap: PropTypes.bool.isRequired, lineNumbers: PropTypes.bool.isRequired, lintWarning: PropTypes.bool.isRequired, @@ -602,7 +555,6 @@ IDEView.propTypes = { name: PropTypes.string.isRequired, content: PropTypes.string.isRequired, })).isRequired, - updateFileContent: PropTypes.func.isRequired, selectedFile: PropTypes.shape({ id: PropTypes.string.isRequired, content: PropTypes.string.isRequired, @@ -630,9 +582,6 @@ IDEView.propTypes = { 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, @@ -642,22 +591,14 @@ IDEView.propTypes = { 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, diff --git a/client/modules/IDE/pages/MobileIDEView.jsx b/client/modules/IDE/pages/MobileIDEView.jsx index 842cd8c7..504cac89 100644 --- a/client/modules/IDE/pages/MobileIDEView.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -8,19 +8,17 @@ import styled from 'styled-components'; // Imports to be Refactored import { bindActionCreators } from 'redux'; -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 * as PreferencesActions from '../actions/preferences'; +import * as EditorAccessibilityActions from '../actions/editorAccessibility'; // Local Imports import Editor from '../components/Editor'; -import { PlayIcon, MoreIcon } from '../../../common/icons'; + +import { PlayIcon, MoreIcon, FolderIcon, PreferencesIcon, TerminalIcon, SaveIcon } from '../../../common/icons'; +import UnsavedChangesDotIcon from '../../../images/unsaved-changes-dot.svg'; import IconButton from '../../../components/mobile/IconButton'; import Header from '../../../components/mobile/Header'; @@ -33,28 +31,25 @@ import { remSize } from '../../../theme'; import ActionStrip from '../../../components/mobile/ActionStrip'; import useAsModal from '../../../components/useAsModal'; -import { PreferencesIcon } from '../../../common/icons'; import Dropdown from '../../../components/Dropdown'; + +import { useEffectWithComparison, useEventListener } from '../../../utils/custom-hooks'; + +import * as device from '../../../utils/device'; + +const withChangeDot = (title, unsavedChanges = false) => ( + + {title} + + {unsavedChanges && + } + + +); const getRootFile = files => files && files.filter(file => file.name === 'root')[0]; const getRootFileID = files => (root => root && root.id)(getRootFile(files)); -const isUserOwner = ({ project, user }) => - project.owner && project.owner.id === user.id; - - -// const userCanEditProject = (props) => { -// let canEdit; -// if (!props.owner) { -// canEdit = true; -// } else if (props.user.authenticated && props.owner.id === props.user.id) { -// canEdit = true; -// } else { -// canEdit = false; -// } -// return canEdit; -// }; - const Expander = styled.div` height: ${props => (props.expanded ? remSize(160) : remSize(27))}; `; @@ -80,24 +75,135 @@ const getNavOptions = (username = undefined, logoutUser = () => {}, toggleForceD ] ); -const MobileIDEView = (props) => { + +const isUserOwner = ({ project, user }) => + project && project.owner && project.owner.id === user.id; + +const canSaveProject = (project, user) => + isUserOwner({ project, user }) || (user.authenticated && !project.owner); + +// TODO: This could go into +const handleGlobalKeydown = (props, cmController) => (e) => { const { - preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage, - selectedFile, updateFileContent, files, user, params, - closeEditorOptions, showEditorOptions, logoutUser, - startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console, - showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch, getProject, clearPersistedState, setUnsavedChanges, - toggleForceDesktop + user, project, ide, + setAllAccessibleOutput, + saveProject, cloneProject, showErrorModal, startSketch, stopSketch, + expandSidebar, collapseSidebar, expandConsole, collapseConsole, + closeNewFolderModal, closeUploadFileModal, closeNewFileModal } = props; - const [tmController, setTmController] = useState(null); // eslint-disable-line + + const isMac = device.isMac(); + + // const ctrlDown = (e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac); + const ctrlDown = (isMac ? e.metaKey : e.ctrlKey); + + if (ctrlDown) { + if (e.shiftKey) { + if (e.keyCode === 13) { + e.preventDefault(); + e.stopPropagation(); + stopSketch(); + } else if (e.keyCode === 13) { + e.preventDefault(); + e.stopPropagation(); + startSketch(); + // 50 === 2 + } else if (e.keyCode === 50 + ) { + e.preventDefault(); + setAllAccessibleOutput(false); + // 49 === 1 + } else if (e.keyCode === 49) { + e.preventDefault(); + setAllAccessibleOutput(true); + } + } else if (e.keyCode === 83) { + // 83 === s + e.preventDefault(); + e.stopPropagation(); + if (canSaveProject(project, user)) saveProject(cmController.getContent(), false, true); + else if (user.authenticated) cloneProject(); + else showErrorModal('forceAuthentication'); + + // 13 === enter + } else if (e.keyCode === 66) { + e.preventDefault(); + if (!ide.sidebarIsExpanded) expandSidebar(); + else collapseSidebar(); + } + } else if (e.keyCode === 192 && e.ctrlKey) { + e.preventDefault(); + if (ide.consoleIsExpanded) collapseConsole(); + else expandConsole(); + } else if (e.keyCode === 27) { + if (ide.newFolderModalVisible) closeNewFolderModal(); + else if (ide.uploadFileModalVisible) closeUploadFileModal(); + else if (ide.modalIsVisible) closeNewFileModal(); + } +}; + + +const autosave = (autosaveInterval, setAutosaveInterval) => (props, prevProps) => { + const { + autosaveProject, preferences, ide, selectedFile: file, project + } = props; + + const { selectedFile: oldFile } = prevProps; + + const doAutosave = () => autosaveProject(true); + + if (isUserOwner(props) && project.id) { + if (preferences.autosave && ide.unsavedChanges && !ide.justOpenedProject) { + if (file.name === oldFile.name && file.content !== oldFile.content) { + if (autosaveInterval) { + clearTimeout(autosaveInterval); + } + console.log('will save project in 20 seconds'); + setAutosaveInterval(setTimeout(doAutosave, 20000)); + } + } else if (autosaveInterval && !preferences.autosave) { + clearTimeout(autosaveInterval); + setAutosaveInterval(null); + } + } else if (autosaveInterval) { + clearTimeout(autosaveInterval); + setAutosaveInterval(null); + } +}; + +// ide, preferences, project, selectedFile, user, params, unsavedChanges, expandConsole, collapseConsole, +// stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject, files + +const MobileIDEView = (props) => { + // const { + // preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage, + // selectedFile, updateFileContent, files, user, params, + // closeEditorOptions, showEditorOptions, logoutUser, + // startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console, + // showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch, getProject, clearPersistedState, setUnsavedChanges, + // toggleForceDesktop + // } = props; + + const { + ide, preferences, project, selectedFile, user, params, unsavedChanges, expandConsole, collapseConsole, + stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject, files, + toggleForceDesktop, logoutUser + } = props; + + + const [cmController, setCmController] = useState(null); // eslint-disable-line const { username } = user; - + const { consoleIsExpanded } = ide; + const { name: filename } = selectedFile; // Force state reset useEffect(clearPersistedState, []); - useEffect(stopSketch, []); + useEffect(() => { + stopSketch(); + collapseConsole(); + }, []); // Load Project const [currentProjectID, setCurrentProjectID] = useState(null); @@ -124,12 +230,28 @@ const MobileIDEView = (props) => { onPressClose={toggle} />), true); + // TODO: This behavior could move to + const [autosaveInterval, setAutosaveInterval] = useState(null); + useEffectWithComparison(autosave(autosaveInterval, setAutosaveInterval), { + autosaveProject, preferences, ide, selectedFile, project, user + }); + + useEventListener('keydown', handleGlobalKeydown(props, cmController), false, [props]); + + const projectActions = + [{ + icon: TerminalIcon, aria: 'Toggle console open/closed', action: consoleIsExpanded ? collapseConsole : expandConsole, inverted: true + }, + { icon: SaveIcon, aria: 'Save project', action: () => saveProject(cmController.getContent(), false, true) }, + { icon: FolderIcon, aria: 'Open files explorer', action: toggleExplorer } + ]; + return (
@@ -146,97 +268,43 @@ const MobileIDEView = (props) => {
- +
); }; +const handleGlobalKeydownProps = { + expandConsole: PropTypes.func.isRequired, + collapseConsole: PropTypes.func.isRequired, + expandSidebar: PropTypes.func.isRequired, + collapseSidebar: PropTypes.func.isRequired, + + setAllAccessibleOutput: PropTypes.func.isRequired, + saveProject: PropTypes.func.isRequired, + cloneProject: PropTypes.func.isRequired, + showErrorModal: PropTypes.func.isRequired, + + closeNewFolderModal: PropTypes.func.isRequired, + closeUploadFileModal: PropTypes.func.isRequired, + closeNewFileModal: PropTypes.func.isRequired, +}; + MobileIDEView.propTypes = { - 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, - 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, - editorAccessibility: PropTypes.shape({ - lintMessages: PropTypes.array.isRequired, + preferences: PropTypes.shape({ }).isRequired, project: PropTypes.shape({ @@ -246,14 +314,8 @@ MobileIDEView.propTypes = { username: PropTypes.string, id: PropTypes.string, }), - updatedAt: PropTypes.string, }).isRequired, - startSketch: PropTypes.func.isRequired, - - updateLintMessage: PropTypes.func.isRequired, - - clearLintMessage: PropTypes.func.isRequired, selectedFile: PropTypes.shape({ id: PropTypes.string.isRequired, @@ -261,36 +323,12 @@ MobileIDEView.propTypes = { name: PropTypes.string.isRequired, }).isRequired, - updateFileContent: PropTypes.func.isRequired, - files: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, content: PropTypes.string.isRequired, })).isRequired, - closeEditorOptions: PropTypes.func.isRequired, - - showEditorOptions: PropTypes.func.isRequired, - - startRefreshSketch: PropTypes.func.isRequired, - - stopSketch: PropTypes.func.isRequired, - - expandSidebar: PropTypes.func.isRequired, - - collapseSidebar: PropTypes.func.isRequired, - - clearConsole: PropTypes.func.isRequired, - - console: PropTypes.arrayOf(PropTypes.shape({ - method: PropTypes.string.isRequired, - args: PropTypes.arrayOf(PropTypes.string), - })).isRequired, - - showRuntimeErrorWarning: PropTypes.func.isRequired, - - hideRuntimeErrorWarning: PropTypes.func.isRequired, toggleForceDesktop: PropTypes.func.isRequired, user: PropTypes.shape({ @@ -301,26 +339,32 @@ MobileIDEView.propTypes = { logoutUser: PropTypes.func.isRequired, - setUnsavedChanges: PropTypes.func.isRequired, getProject: PropTypes.func.isRequired, clearPersistedState: PropTypes.func.isRequired, params: PropTypes.shape({ project_id: PropTypes.string, username: PropTypes.string }).isRequired, + + startSketch: PropTypes.func.isRequired, + + unsavedChanges: PropTypes.bool.isRequired, + autosaveProject: PropTypes.func.isRequired, + + + ...handleGlobalKeydownProps }; 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, + files: state.files, + unsavedChanges: state.ide.unsavedChanges, preferences: state.preferences, - editorAccessibility: state.editorAccessibility, user: state.user, project: state.project, toast: state.toast, @@ -328,21 +372,12 @@ function mapStateToProps(state) { }; } -function mapDispatchToProps(dispatch) { - return bindActionCreators( - Object.assign( - {}, - EditorAccessibilityActions, - FileActions, - ProjectActions, - IDEActions, - PreferencesActions, - UserActions, - ToastActions, - ConsoleActions - ), - dispatch - ); -} +const mapDispatchToProps = dispatch => bindActionCreators({ + ...ProjectActions, + ...IDEActions, + ...ConsoleActions, + ...PreferencesActions, + ...EditorAccessibilityActions +}, dispatch); export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView)); diff --git a/client/utils/custom-hooks.js b/client/utils/custom-hooks.js index a58bda41..9271287e 100644 --- a/client/utils/custom-hooks.js +++ b/client/utils/custom-hooks.js @@ -40,3 +40,20 @@ export const useModalBehavior = (hideOverlay) => { return [visible, trigger, setRef]; }; + +// Usage: useEffectWithComparison((props, prevProps) => { ... }, { prop1, prop2 }) +// This hook basically applies useEffect but keeping track of the last value of relevant props +// So you can passa a 2-param function to capture new and old values and do whatever with them. +export const useEffectWithComparison = (fn, props) => { + const [prevProps, update] = useState({}); + + return useEffect(() => { + fn(props, prevProps); + update(props); + }, Object.values(props)); +}; + +export const useEventListener = (event, callback, useCapture = false, list = []) => useEffect(() => { + document.addEventListener(event, callback, useCapture); + return () => document.removeEventListener(event, callback, useCapture); +}, list); diff --git a/client/utils/device.js b/client/utils/device.js new file mode 100644 index 00000000..040b16b7 --- /dev/null +++ b/client/utils/device.js @@ -0,0 +1 @@ +export const isMac = () => navigator.userAgent.toLowerCase().indexOf('mac') !== -1; // eslint-disable-line