diff --git a/app/models/Collection.js b/app/models/Collection.js index 91881f2e2..625bb87ba 100644 --- a/app/models/Collection.js +++ b/app/models/Collection.js @@ -11,7 +11,6 @@ import type { NavigationNode } from 'types'; class Collection extends BaseModel { isSaving: boolean = false; - hasPendingChanges: boolean = false; ui: UiStore; data: Object; @@ -109,7 +108,6 @@ class Collection extends BaseModel { runInAction('Collection#save', () => { invariant(res && res.data, 'Data should be available'); this.updateData(res.data); - this.hasPendingChanges = false; }); } catch (e) { this.ui.showToast('Collection failed saving'); diff --git a/app/models/Document.js b/app/models/Document.js index 883effcf2..d45601ccf 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -15,7 +15,6 @@ type SaveOptions = { publish?: boolean, done?: boolean, autosave?: boolean }; class Document extends BaseModel { isSaving: boolean = false; - hasPendingChanges: boolean = false; ui: *; store: *; @@ -209,7 +208,6 @@ class Document extends BaseModel { runInAction('Document#save', () => { invariant(res && res.data, 'Data should be available'); this.updateData(res.data); - this.hasPendingChanges = false; if (isCreating) { this.emit('documents.create', this); @@ -288,13 +286,12 @@ class Document extends BaseModel { a.click(); }; - updateData(data: Object = {}, dirty: boolean = false) { + updateData(data: Object = {}) { if (data.text) { const { title, emoji } = parseTitle(data.text); data.title = title; data.emoji = emoji; } - if (dirty) this.hasPendingChanges = true; extendObservable(this, data); } diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index 839595122..684f03105 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -36,7 +36,8 @@ import Search from 'scenes/Search'; import Error404 from 'scenes/Error404'; import ErrorOffline from 'scenes/ErrorOffline'; -const AUTOSAVE_INTERVAL = 3000; +const AUTOSAVE_DELAY = 3000; +const IS_DIRTY_DELAY = 500; const MARK_AS_VIEWED_AFTER = 3000; const DISCARD_CHANGES = ` You have unsaved changes. @@ -59,16 +60,16 @@ type Props = { @observer class DocumentScene extends React.Component { - savedTimeout: TimeoutID; viewTimeout: TimeoutID; + getEditorText: () => string; @observable editorComponent; - @observable editCache: ?string; @observable document: ?Document; @observable newDocument: ?Document; @observable isUploading = false; @observable isSaving = false; @observable isPublishing = false; + @observable isDirty = false; @observable notFound = false; @observable moveModalOpen: boolean = false; @@ -83,14 +84,13 @@ class DocumentScene extends React.Component { this.props.match.params.documentSlug ) { this.notFound = false; + clearTimeout(this.viewTimeout); this.loadDocument(nextProps); } } componentWillUnmount() { - clearTimeout(this.savedTimeout); clearTimeout(this.viewTimeout); - this.props.ui.clearActiveDocument(); } @@ -116,15 +116,13 @@ class DocumentScene extends React.Component { props.match.params.documentSlug, { shareId } ); + this.isDirty = false; const document = this.document; if (document) { this.props.ui.setActiveDocument(document); - // Cache data if user enters edit mode and cancels - this.editCache = document.text; - if (this.props.auth.user && !shareId) { if (!this.isEditing && document.publishedAt) { this.viewTimeout = setTimeout(document.view, MARK_AS_VIEWED_AFTER); @@ -163,13 +161,25 @@ class DocumentScene extends React.Component { options: { done?: boolean, publish?: boolean, autosave?: boolean } = {} ) => { let document = this.document; - if (!document || !document.allowSave) return; + if (!document) return; + + // get the latest version of the editor text value + const text = this.getEditorText ? this.getEditorText() : document.text; + + // prevent autosave if nothing has changed + if (options.autosave && document.text.trim() === text.trim()) return; + + document.updateData({ text }, true); + if (!document.allowSave) return; + + // prevent autosave before anything has been written + if (options.autosave && !document.title && !document.id) return; let isNew = !document.id; - this.editCache = null; this.isSaving = true; this.isPublishing = !!options.publish; document = await document.save(options); + this.isDirty = false; this.isSaving = false; this.isPublishing = false; @@ -182,9 +192,13 @@ class DocumentScene extends React.Component { } }; - autosave = debounce(async () => { + autosave = debounce(() => { this.onSave({ done: false, autosave: true }); - }, AUTOSAVE_INTERVAL); + }, AUTOSAVE_DELAY); + + updateIsDirty = debounce(() => { + this.isDirty = this.getEditorText().trim() !== this.document.text.trim(); + }, IS_DIRTY_DELAY); onImageUploadStart = () => { this.isUploading = true; @@ -194,14 +208,9 @@ class DocumentScene extends React.Component { this.isUploading = false; }; - onChange = text => { - let document = this.document; - if (!document) return; - if (document.text.trim() === text.trim()) return; - document.updateData({ text }, true); - - // prevent autosave before anything has been written - if (!document.title && !document.id) return; + onChange = getEditorText => { + this.getEditorText = getEditorText; + this.updateIsDirty(); this.autosave(); }; @@ -209,7 +218,6 @@ class DocumentScene extends React.Component { let url; if (this.document && this.document.url) { url = this.document.url; - if (this.editCache) this.document.updateData({ text: this.editCache }); } else { url = collectionUrl(this.props.match.params.id); } @@ -304,10 +312,7 @@ class DocumentScene extends React.Component { {this.isEditing && ( - + )} diff --git a/app/scenes/Document/components/Editor.js b/app/scenes/Document/components/Editor.js index c45fc6040..cb11a2218 100644 --- a/app/scenes/Document/components/Editor.js +++ b/app/scenes/Document/components/Editor.js @@ -41,10 +41,20 @@ schema.document.normalize = ( class Editor extends React.Component { editor: *; + componentDidMount() { + if (!this.props.defaultValue) { + this.focusAtStart(); + } + } + setEditorRef = (ref: RichMarkdownEditor) => { this.editor = ref; }; + focusAtStart = () => { + if (this.editor) this.editor.focusAtStart(); + }; + focusAtEnd = () => { if (this.editor) this.editor.focusAtEnd(); }; diff --git a/app/scenes/Document/components/Header.js b/app/scenes/Document/components/Header.js index 9ac29d043..4ae8202de 100644 --- a/app/scenes/Document/components/Header.js +++ b/app/scenes/Document/components/Header.js @@ -161,13 +161,6 @@ class Header extends React.Component { Edit )} - {isEditing && - !isSaving && - document.hasPendingChanges && ( - - Discard - - )} {!isEditing && ( diff --git a/package.json b/package.json index 3edd22a41..3d47ccc10 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "copy-to-clipboard": "^3.0.6", "css-loader": "^0.28.7", "date-fns": "1.29.0", - "debug": "2.2.0", + "debug": "2.6.9", "dotenv": "^4.0.0", "emoji-regex": "^6.5.1", "exports-loader": "^0.6.4", @@ -158,7 +158,7 @@ "react-waypoint": "^7.3.1", "redis": "^2.6.2", "redis-lock": "^0.1.0", - "rich-markdown-editor": "3.1.4", + "rich-markdown-editor": "5.0.0", "safestart": "1.1.0", "sequelize": "4.28.6", "sequelize-cli": "^2.7.0", diff --git a/yarn.lock b/yarn.lock index c50d32058..580eee2ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2526,19 +2526,13 @@ debug@*, debug@2.6.8, debug@^2.2.0, debug@^2.3.2, debug@^2.6.1, debug@^2.6.3, de dependencies: ms "2.0.0" -debug@2.2.0, debug@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" - dependencies: - ms "0.7.1" - debug@2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" dependencies: ms "2.0.0" -debug@^2.6.9: +debug@2.6.9, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" dependencies: @@ -2556,6 +2550,12 @@ debug@^3.0.1, debug@^3.1.0: dependencies: ms "2.0.0" +debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + debuglog@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -8858,9 +8858,9 @@ retry-axios@0.3.2, retry-axios@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-0.3.2.tgz#5757c80f585b4cc4c4986aa2ffd47a60c6d35e13" -rich-markdown-editor@3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-3.1.4.tgz#90e60f599e0ad57b52d893880c89d7ebdfdd97cd" +rich-markdown-editor@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/rich-markdown-editor/-/rich-markdown-editor-5.0.0.tgz#7c581f5ec3a52f749bc7633ea46b9f3d5b113935" dependencies: "@tommoor/slate-drop-or-paste-images" "^0.8.1" babel-plugin-transform-async-to-generator "^6.24.1"