diff --git a/client/constants.js b/client/constants.js index c2e6c5be..a4235717 100644 --- a/client/constants.js +++ b/client/constants.js @@ -82,5 +82,7 @@ export const SHOW_TOAST = 'SHOW_TOAST'; export const HIDE_TOAST = 'HIDE_TOAST'; export const SET_TOAST_TEXT = 'SET_TOAST_TEXT'; +export const SET_UNSAVED_CHANGES = 'SET_UNSAVED_CHANGES'; + // eventually, handle errors more specifically and better export const ERROR = 'ERROR'; diff --git a/client/modules/IDE/actions/ide.js b/client/modules/IDE/actions/ide.js index d56ae836..05725fca 100644 --- a/client/modules/IDE/actions/ide.js +++ b/client/modules/IDE/actions/ide.js @@ -163,3 +163,10 @@ export function closeKeyboardShortcutModal() { }; } +export function setUnsavedChanges(value) { + return { + type: ActionTypes.SET_UNSAVED_CHANGES, + value + }; +} + diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index 681829b9..d47f93c5 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -6,6 +6,7 @@ import JSZipUtils from 'jszip-utils'; import { saveAs } from 'file-saver'; import { getBlobUrl } from './files'; import { showToast, setToastText } from './toast'; +import { setUnsavedChanges } from './ide'; const ROOT_URL = location.href.indexOf('localhost') > 0 ? 'http://localhost:8000/api' : '/api'; @@ -32,6 +33,7 @@ export function getProject(id) { owner: response.data.user }); getProjectBlobUrls()(dispatch, getState); + dispatch(setUnsavedChanges(false)); }) .catch(response => dispatch({ type: ActionTypes.ERROR, @@ -58,6 +60,7 @@ export function saveProject(autosave) { if (state.project.id) { axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true }) .then(() => { + dispatch(setUnsavedChanges(false)); dispatch({ type: ActionTypes.PROJECT_SAVE_SUCCESS }); @@ -73,6 +76,7 @@ export function saveProject(autosave) { } else { axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true }) .then(response => { + dispatch(setUnsavedChanges(false)); browserHistory.push(`/projects/${response.data.id}`); dispatch({ type: ActionTypes.NEW_PROJECT, @@ -112,6 +116,7 @@ export function createProject() { owner: response.data.user, files: response.data.files }); + dispatch(setUnsavedChanges(false)); }) .catch(response => dispatch({ type: ActionTypes.PROJECT_SAVE_FAIL, diff --git a/client/modules/IDE/components/Editor.js b/client/modules/IDE/components/Editor.js index 43379a4d..d035a104 100644 --- a/client/modules/IDE/components/Editor.js +++ b/client/modules/IDE/components/Editor.js @@ -60,33 +60,33 @@ class Editor extends React.Component { }) } }); + this._cm.on('change', debounce(200, () => { + this.props.setUnsavedChanges(true); this.props.updateFileContent(this.props.file.name, this._cm.getValue()); })); + this._cm.on('keyup', () => { const temp = `line ${parseInt((this._cm.getCursor().line) + 1, 10)}`; document.getElementById('current-line').innerHTML = temp; }); - // this._cm.on('change', () => { // eslint-disable-line - // // this.props.updateFileContent('sketch.js', this._cm.getValue()); - // throttle(1000, () => console.log('debounce is working!')); - // this.props.updateFileContent(this.props.file.name, this._cm.getValue()); - // }); - this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; - this._cm.setOption('indentWithTabs', this.props.isTabIndent); - this._cm.setOption('tabSize', this.props.indentationAmount); this._cm.on('keydown', (_cm, e) => { if (e.key === 'Tab' && e.shiftKey) { this.tidyCode(); } }); + + 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) { if (this.props.file.content !== prevProps.file.content && this.props.file.content !== this._cm.getValue()) { this._cm.setValue(this.props.file.content); // eslint-disable-line no-underscore-dangle + setTimeout(() => this.props.setUnsavedChanges(false), 500); } if (this.props.fontSize !== prevProps.fontSize) { this._cm.getWrapperElement().style['font-size'] = `${this.props.fontSize}px`; @@ -190,7 +190,8 @@ Editor.propTypes = { editorOptionsVisible: PropTypes.bool.isRequired, showEditorOptions: PropTypes.func.isRequired, closeEditorOptions: PropTypes.func.isRequired, - showKeyboardShortcutModal: PropTypes.func.isRequired + showKeyboardShortcutModal: PropTypes.func.isRequired, + setUnsavedChanges: PropTypes.func.isRequired }; export default Editor; diff --git a/client/modules/IDE/pages/IDEView.js b/client/modules/IDE/pages/IDEView.js index 5c1ffc1d..a95249fd 100644 --- a/client/modules/IDE/pages/IDEView.js +++ b/client/modules/IDE/pages/IDEView.js @@ -14,6 +14,7 @@ import Console from '../components/Console'; 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 IDEActions from '../actions/ide'; import * as ProjectActions from '../actions/project'; @@ -33,6 +34,7 @@ class IDEView extends React.Component { this._handleConsolePaneOnDragFinished = this._handleConsolePaneOnDragFinished.bind(this); this._handleSidebarPaneOnDragFinished = this._handleSidebarPaneOnDragFinished.bind(this); this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this); + this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind(this); } componentDidMount() { @@ -55,6 +57,10 @@ class IDEView extends React.Component { this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1; document.addEventListener('keydown', this.handleGlobalKeydown, false); + + this.props.router.setRouteLeaveHook(this.props.route, () => this.warnIfUnsavedChanges()); + + window.onbeforeunload = () => this.warnIfUnsavedChanges(); } componentWillUpdate(nextProps) { @@ -91,6 +97,10 @@ class IDEView extends React.Component { clearInterval(this.autosaveInterval); this.autosaveInterval = null; } + + if (this.props.route.path !== prevProps.route.path) { + this.props.router.setRouteLeaveHook(this.props.route, () => this.warnIfUnsavedChanges()); + } } componentWillUnmount() { @@ -134,6 +144,17 @@ class IDEView extends React.Component { } } + warnIfUnsavedChanges() { // eslint-disable-line + if (this.props.ide.unsavedChanges + && ((this.props.project.owner && this.props.project.owner.id === this.props.user.id) + || (!this.props.project.owner))) { + if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) { + return false; + } + this.props.setUnsavedChanges(false); + } + } + render() { return (
@@ -243,6 +264,7 @@ class IDEView extends React.Component { showEditorOptions={this.props.showEditorOptions} closeEditorOptions={this.props.closeEditorOptions} showKeyboardShortcutModal={this.props.showKeyboardShortcutModal} + setUnsavedChanges={this.props.setUnsavedChanges} /> { @@ -70,6 +71,8 @@ const ide = (state = initialState, action) => { return Object.assign({}, state, { keyboardShortcutVisible: true }); case ActionTypes.CLOSE_KEYBOARD_SHORTCUT_MODAL: return Object.assign({}, state, { keyboardShortcutVisible: false }); + case ActionTypes.SET_UNSAVED_CHANGES: + return Object.assign({}, state, { unsavedChanges: action.value }); default: return state; }