From 6f33caca45788b9f2e2e60a75b74e50647824de9 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Wed, 6 Jul 2016 21:37:52 -0700 Subject: [PATCH] Fixes to editing --- .../MarkdownEditor/MarkdownEditor.js | 4 +- src/scenes/DocumentEdit/DocumentEdit.js | 95 ++++--- src/scenes/DocumentEdit/DocumentEditStore.js | 238 ++++++++++-------- src/utils/emojify.js | 2 +- 4 files changed, 189 insertions(+), 150 deletions(-) diff --git a/src/components/MarkdownEditor/MarkdownEditor.js b/src/components/MarkdownEditor/MarkdownEditor.js index f18f5ec98..ad2df39c0 100644 --- a/src/components/MarkdownEditor/MarkdownEditor.js +++ b/src/components/MarkdownEditor/MarkdownEditor.js @@ -17,13 +17,13 @@ import { client } from 'utils/ApiClient'; @observer class MarkdownEditor extends React.Component { static propTypes = { - text: React.PropTypes.string.isRequired, + text: React.PropTypes.string, onChange: React.PropTypes.func.isRequired, replaceText: React.PropTypes.func.isRequired, // This is actually not used but it triggers // re-render to help with CodeMirror focus issues - preview: React.PropTypes.bool.isRequired, + preview: React.PropTypes.bool, } getEditorInstance = () => { diff --git a/src/scenes/DocumentEdit/DocumentEdit.js b/src/scenes/DocumentEdit/DocumentEdit.js index ceacce94b..4a6b9559b 100644 --- a/src/scenes/DocumentEdit/DocumentEdit.js +++ b/src/scenes/DocumentEdit/DocumentEdit.js @@ -1,8 +1,10 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; -import { browserHistory } from 'react-router'; +import { browserHistory, withRouter } from 'react-router'; -import store from './DocumentEditStore'; +import DocumentEditStore, { + DOCUMENT_EDIT_SETTINGS, +} from './DocumentEditStore'; import Switch from 'components/Switch'; import Layout, { Title, HeaderAction } from 'components/Layout'; @@ -14,26 +16,43 @@ import DropdownMenu, { MenuItem } from 'components/DropdownMenu'; import EditorLoader from './components/EditorLoader'; import SaveAction from './components/SaveAction'; -import styles from './DocumentEdit.scss'; -import classNames from 'classnames/bind'; -const cx = classNames.bind(styles); +const DISREGARD_CHANGES = `You have unsaved changes. +Are you sure you want to disgard them?`; +@withRouter @observer class DocumentEdit extends Component { + static store; + + static propTypes = { + route: React.PropTypes.object.isRequired, + router: React.PropTypes.object.isRequired, + params: React.PropTypes.object, + } + + state = { + scrollTop: 0, + } + + constructor(props) { + super(props); + this.store = new DocumentEditStore( + JSON.parse(localStorage[DOCUMENT_EDIT_SETTINGS] || '{}') + ); + } + componentDidMount = () => { - // This is a bit hacky, should find a better way - store.reset(); if (this.props.route.newDocument) { - store.atlasId = this.props.params.id; - store.newDocument = true; + this.store.atlasId = this.props.params.id; + this.store.newDocument = true; } else if (this.props.route.newChildDocument) { - store.documentId = this.props.params.id; - store.newChildDocument = true; - store.fetchDocument(); + this.store.documentId = this.props.params.id; + this.store.newChildDocument = true; + this.store.fetchDocument(); } else { - store.documentId = this.props.params.id; - store.newDocument = false; - store.fetchDocument(); + this.store.documentId = this.props.params.id; + this.store.newDocument = false; + this.store.fetchDocument(); } // Load editor async @@ -41,6 +60,14 @@ class DocumentEdit extends Component { .then(({ Editor }) => { this.setState({ Editor }); }); + + // Set onLeave hook + this.props.router.setRouteLeaveHook(this.props.route, () => { + if (this.store.hasPendingChanges) { + return confirm(DISREGARD_CHANGES); + } + return; + }); } onSave = () => { @@ -48,10 +75,10 @@ class DocumentEdit extends Component { // alert("Please add a title before saving (hint: Write a markdown header)"); // return // } - if (store.newDocument || store.newChildDocument) { - store.saveDocument(); + if (this.store.newDocument || this.store.newChildDocument) { + this.store.saveDocument(); } else { - store.updateDocument(); + this.store.updateDocument(); } } @@ -59,43 +86,37 @@ class DocumentEdit extends Component { browserHistory.goBack(); } - state = { - scrollTop: 0, - } - onScroll = (scrollTop) => { this.setState({ - scrollTop: scrollTop, - }) - } - - onPreviewToggle = () => { - store.togglePreview(); + scrollTop, + }); } render() { + console.log("DocumentEdit#render", this.store.preview); + let title = ( - { store.title } + { this.store.title } ); - let titleText = store.title; + let titleText = this.store.title; const actions = ( - - Preview + + Preview Cancel @@ -109,16 +130,16 @@ class DocumentEdit extends Component { actions={ actions } title={ title } titleText={ titleText } - fixed={ true } - loading={ store.isSaving } + fixed + loading={ this.store.isSaving } > - { (store.isFetching || !('Editor' in this.state)) ? ( + { (this.store.isFetching || !('Editor' in this.state)) ? ( ) : ( diff --git a/src/scenes/DocumentEdit/DocumentEditStore.js b/src/scenes/DocumentEdit/DocumentEditStore.js index bc8718ee3..cc8218107 100644 --- a/src/scenes/DocumentEdit/DocumentEditStore.js +++ b/src/scenes/DocumentEdit/DocumentEditStore.js @@ -1,6 +1,5 @@ -import { observable, action, computed, autorun, toJS } from 'mobx'; +import { observable, action, toJS, autorun } from 'mobx'; import { client } from 'utils/ApiClient'; -import localforage from 'localforage'; import { browserHistory } from 'react-router'; import emojify from 'utils/emojify'; @@ -8,122 +7,141 @@ const DOCUMENT_EDIT_SETTINGS = 'DOCUMENT_EDIT_SETTINGS'; const parseHeader = (text) => { const firstLine = text.split(/\r?\n/)[0]; - const match = firstLine.match(/^#+ +(.*)$/); + if (firstLine) { + const match = firstLine.match(/^#+ +(.*)$/); - if (match) { - return emojify(match[1]); + if (match) { + return emojify(match[1]); + } else { + return ''; + } } -} + return ''; +}; -const documentEditStore = new class DocumentEditStore { - @observable documentId = null; - @observable atlasId = null; - @observable parentDocument; - @observable title; - @observable text; - @observable newDocument; - @observable newChildDocument; +class DocumentEditStore { + @observable documentId = null; + @observable atlasId = null; + @observable parentDocument; + @observable title; + @observable text; + @observable hasPendingChanges = false; + @observable newDocument; + @observable newChildDocument; - @observable preview; - @observable isFetching; - @observable isSaving; + @observable preview; + @observable isFetching; + @observable isSaving; - /* Actions */ + /* Actions */ - @action fetchDocument = async () => { - this.isFetching = true; + @action fetchDocument = async () => { + this.isFetching = true; - try { - const data = await client.post('/documents.info', { - id: this.documentId, - }) - if (this.newChildDocument) { - this.parentDocument = data.data; - } else { - const { title, text } = data.data; - this.title = title; - this.text = text; - } - } catch (e) { - console.error("Something went wrong"); - } - this.isFetching = false; - } - - @action saveDocument = async (nextPath) => { - if (this.isSaving) return; - - this.isSaving = true; - - try { - const data = await client.post('/documents.create', { - parentDocument: this.parentDocument && this.parentDocument.id, - atlas: this.atlasId || this.parentDocument.atlas.id, - title: this.title, - text: this.text, - }) - const { id } = data.data; - browserHistory.push(`/documents/${id}`); - } catch (e) { - console.error("Something went wrong"); - } - this.isSaving = false; - } - - @action updateDocument = async (nextPath) => { - if (this.isSaving) return; - - this.isSaving = true; - - try { - const data = await client.post('/documents.update', { - id: this.documentId, - title: this.title, - text: this.text, - }) - browserHistory.push(`/documents/${this.documentId}`); - } catch (e) { - console.error("Something went wrong"); - } - this.isSaving = false; - } - - @action updateText = (text) => { - this.text = text; - this.title = parseHeader(text); - } - - @action updateTitle = (title) => { - this.title = title; - } - - @action replaceText = (args) => { - this.text = this.text.replace(args.original, args.new); - } - - @action togglePreview = () => { - this.preview = !this.preview; - } - - @action reset = () => { - this.title = 'Lets start with a title'; - this.text = '# Lets start with a title\n\nAnd continue from there...'; - } - - constructor() { - // Rehydrate settings - localforage.getItem(DOCUMENT_EDIT_SETTINGS) - .then(data => { - this.preview = data.preview; + try { + const data = await client.post('/documents.info', { + id: this.documentId, }); + if (this.newChildDocument) { + this.parentDocument = data.data; + } else { + const { title, text } = data.data; + this.title = title; + this.text = text; + } + } catch (e) { + console.error('Something went wrong'); } -}(); + this.isFetching = false; + } -// Persist settings to localStorage -autorun(() => { - localforage.setItem(DOCUMENT_EDIT_SETTINGS, { - preview: documentEditStore.preview, - }); -}); + @action saveDocument = async () => { + if (this.isSaving) return; -export default documentEditStore; + this.isSaving = true; + + try { + const data = await client.post('/documents.create', { + parentDocument: this.parentDocument && this.parentDocument.id, + atlas: this.atlasId || this.parentDocument.atlas.id, + title: this.title, + text: this.text, + }); + const { id } = data.data; + + this.hasPendingChanges = false; + browserHistory.push(`/documents/${id}`); + } catch (e) { + console.error("Something went wrong"); + } + this.isSaving = false; + } + + @action updateDocument = async () => { + if (this.isSaving) return; + + this.isSaving = true; + + try { + await client.post('/documents.update', { + id: this.documentId, + title: this.title, + text: this.text, + }); + + this.hasPendingChanges = false; + browserHistory.push(`/documents/${this.documentId}`); + } catch (e) { + console.error("Something went wrong"); + } + this.isSaving = false; + } + + @action updateText = (text) => { + this.text = text; + this.title = parseHeader(text); + this.hasPendingChanges = true; + } + + @action updateTitle = (title) => { + this.title = title; + } + + @action replaceText = (args) => { + this.text = this.text.replace(args.original, args.new); + this.hasPendingChanges = true; + } + + @action togglePreview = () => { + this.preview = !this.preview; + } + + @action reset = () => { + this.title = 'Lets start with a title'; + this.text = '# Lets start with a title\n\nAnd continue from there...'; + } + + // Generic + + persistSettings = () => { + localStorage[DOCUMENT_EDIT_SETTINGS] = JSON.stringify({ + preview: toJS(this.preview), + }); + } + + constructor(settings) { + // Rehydrate settings + this.preview = settings.preview + + // Persist settings to localStorage + // TODO: This could be done more selectively + autorun(() => { + this.persistSettings(); + }); + } +}; + +export default DocumentEditStore; +export { + DOCUMENT_EDIT_SETTINGS +}; diff --git a/src/utils/emojify.js b/src/utils/emojify.js index daf77155d..7d2442f5d 100644 --- a/src/utils/emojify.js +++ b/src/utils/emojify.js @@ -2,7 +2,7 @@ import emojiMapping from './emoji-mapping.json'; const EMOJI_REGEX = /:([A-Za-z0-9_\-\+]+?):/gm; -const emojify = (text) => { +const emojify = (text='') => { const emojis = text.match(EMOJI_REGEX) || []; let emojifiedText = text;