diff --git a/core/client/components/gh-codemirror.js b/core/client/components/gh-codemirror.js index cfc2204fe5..93ffed4c1d 100644 --- a/core/client/components/gh-codemirror.js +++ b/core/client/components/gh-codemirror.js @@ -9,19 +9,20 @@ codeMirrorShortcuts.init(); var onChangeHandler = function (cm, changeObj) { var line, - component = cm.component, - checkLine = _.bind(component.checkLine, component), - checkMarkers = _.bind(component.checkMarkers, component); + component = cm.component; // fill array with a range of numbers for (line = changeObj.from.line; line < changeObj.from.line + changeObj.text.length; line += 1) { - checkLine(line, changeObj.origin); + component.checkLine(line, changeObj.origin); } // Is this a line which may have had a marker on it? - checkMarkers(); + component.checkMarkers(); cm.component.set('value', cm.getValue()); + + // Send an action notifying a 5 second pause in typing/changes. + Ember.run.debounce(component, 'sendAction', 'typingPause', 5000); }; var onScrollHandler = function (cm) { diff --git a/core/client/controllers/editor/new.js b/core/client/controllers/editor/new.js index 73ba0daf4a..3a1e0735df 100644 --- a/core/client/controllers/editor/new.js +++ b/core/client/controllers/editor/new.js @@ -5,9 +5,9 @@ var EditorNewController = Ember.ObjectController.extend(EditorControllerMixin, { /** * Redirect to editor after the first save */ - save: function () { + save: function (options) { var self = this; - this._super().then(function (model) { + return this._super(options).then(function (model) { if (model.get('id')) { self.transitionToRoute('editor.edit', model); } diff --git a/core/client/mixins/editor-base-controller.js b/core/client/mixins/editor-base-controller.js index 11ae6049f9..71ecfde164 100644 --- a/core/client/mixins/editor-base-controller.js +++ b/core/client/mixins/editor-base-controller.js @@ -5,17 +5,13 @@ import boundOneWay from 'ghost/utils/bound-one-way'; // this array will hold properties we need to watch // to know if the model has been changed (`controller.isDirty`) -var watchedProps = ['scratch', 'titleScratch', 'model.isDirty']; +var watchedProps = ['scratch', 'titleScratch', 'model.isDirty', 'tags.[]']; Ember.get(PostModel, 'attributes').forEach(function (name) { watchedProps.push('model.' + name); }); -// watch if number of tags changes on the model -watchedProps.push('tags.[]'); - var EditorControllerMixin = Ember.Mixin.create(MarkerManager, { - needs: ['post-tags-input'], init: function () { @@ -34,11 +30,14 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, { */ willPublish: boundOneWay('isPublished'), + // Make sure editor starts with markdown shown + isPreview: false, + // set by the editor route and `isDirty`. useful when checking // whether the number of tags has changed for `isDirty`. previousTagNames: null, - tagNames: Ember.computed('tags.[]', function () { + tagNames: Ember.computed('tags.@each.name', function () { return this.get('tags').mapBy('name'); }), @@ -130,11 +129,7 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, { // which does *not* change the model's `isDirty` property, // `isDirty` will tell us if the other props have changed, // as long as the model is not new (model.isNew === false). - if (model.get('isDirty')) { - return true; - } - - return false; + return model.get('isDirty'); })), // used on window.onbeforeunload @@ -152,12 +147,12 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, { errors: { post: { published: { - 'published': 'Update failed.', - 'draft': 'Saving failed.' + published: 'Update failed.', + draft: 'Saving failed.' }, draft: { - 'published': 'Publish failed.', - 'draft': 'Saving failed.' + published: 'Publish failed.', + draft: 'Saving failed.' } } @@ -166,12 +161,12 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, { success: { post: { published: { - 'published': 'Updated.', - 'draft': 'Saved.' + published: 'Updated.', + draft: 'Saved.' }, draft: { - 'published': 'Published!', - 'draft': 'Saved.' + published: 'Published!', + draft: 'Saved.' } } } @@ -191,16 +186,15 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, { this.notifications.showError(message, { delayed: delay }); }, - shouldFocusTitle: Ember.computed('model', function () { - return !!this.get('model.isNew'); - }), + shouldFocusTitle: Ember.computed.alias('model.isNew'), actions: { - save: function () { + save: function (options) { var status = this.get('willPublish') ? 'published' : 'draft', prevStatus = this.get('status'), isNew = this.get('isNew'), self = this; + options = options || {}; self.notifications.closePassive(); @@ -210,15 +204,23 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, { // Set the properties that are indirected // set markdown equal to what's in the editor, minus the image markers. this.set('markdown', this.getMarkdown().withoutMarkers); - this.set('title', this.get('titleScratch')); this.set('status', status); + // Set a default title + if (!this.get('titleScratch')) { + this.set('titleScratch', '(Untitled)'); + } + this.set('title', this.get('titleScratch')); + return this.get('model').save().then(function (model) { - self.showSaveNotification(prevStatus, model.get('status'), isNew ? true : false); + if (!options.silent) { + self.showSaveNotification(prevStatus, model.get('status'), isNew ? true : false); + } return model; }).catch(function (errors) { - self.showErrorNotification(prevStatus, self.get('status'), errors); - + if (!options.silent) { + self.showErrorNotification(prevStatus, self.get('status'), errors); + } self.set('status', prevStatus); return Ember.RSVP.reject(errors); @@ -286,11 +288,14 @@ var EditorControllerMixin = Ember.Mixin.create(MarkerManager, { editor.replaceSelection(result_src); }, - // Make sure editor starts with markdown shown - isPreview: false, - togglePreview: function (preview) { this.set('isPreview', preview); + }, + + autoSave: function () { + if (this.get('model.isDraft')) { + this.send('save', {silent: true, disableNProgress: true}); + } } } }); diff --git a/core/client/templates/editor/edit.hbs b/core/client/templates/editor/edit.hbs index 075eda1cdd..4946cea1f9 100644 --- a/core/client/templates/editor/edit.hbs +++ b/core/client/templates/editor/edit.hbs @@ -16,7 +16,10 @@
- {{gh-codemirror value=scratch scrollInfo=view.markdownScrollInfo setCodeMirror="setCodeMirror" openModal="openModal"}} + {{gh-codemirror value=scratch scrollInfo=view.markdownScrollInfo + setCodeMirror="setCodeMirror" + openModal="openModal" + typingPause="autoSave"}}
diff --git a/core/test/functional/client/editor_test.js b/core/test/functional/client/editor_test.js index 2821506f22..ec8d487640 100644 --- a/core/test/functional/client/editor_test.js +++ b/core/test/functional/client/editor_test.js @@ -2,7 +2,7 @@ // Test the editor screen works as expected /*globals CasperTest, casper, testPost, $ */ -CasperTest.begin('Ghost editor functions correctly', 21, function suite(test) { +CasperTest.begin('Ghost editor functions correctly', 20, function suite(test) { test.assertHTMLEquals = function (equals, message) { test.assertEvalEquals(function () { return document.querySelector('.entry-preview .rendered-markdown').innerHTML @@ -17,7 +17,7 @@ CasperTest.begin('Ghost editor functions correctly', 21, function suite(test) { test.assertExists('.entry-preview', 'Ghost preview is present'); }); - // Part 1: Test saving with no data - title is required + // Part 1: Test saving with no data - title should default casper.waitForSelector('#entry-title', function then() { test.assertEvalEquals(function () { return document.getElementById('entry-title').value; @@ -26,12 +26,13 @@ CasperTest.begin('Ghost editor functions correctly', 21, function suite(test) { casper.thenClick('.js-publish-button'); - casper.waitForSelector('.notification-error', function onSuccess() { - test.assert(true, 'Save without title results in error notification as expected'); - test.assertSelectorHasText('.notification-error', 'must specify a title', 'notification text is correct'); - test.assertSelectorDoesntHaveText('.notification-error', '[object Object]'); + casper.waitForSelector('.notification-success', function onSuccess() { + test.assert(true, 'Can save with no title.'); + test.assertEvalEquals(function () { + return document.getElementById('entry-title').value; + }, '(Untitled)', 'Title is "(Untitled)"'); }, function onTimeout() { - test.assert(false, 'Save without title did not result in an error notification'); + test.assert(false, 'Failed to save without a title.'); }); this.thenClick('.js-bb-notification .close');