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 @@
What is Markdown?
- {{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');