mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Autosave Draft posts
Ref #1413 - If the user stops typing for 5 seconds, a "typingPause" action is sent from the GhCodemirrorComponent - Editor-base-controller ties the "typingPause" action to its "autosave" - If a post is being saved and does not have a title, the title is set to "(Untitled)" - Cleanup editor base controller property dependencies and code
This commit is contained in:
parent
58ec6e0ac9
commit
a9fb2af7ea
5 changed files with 55 additions and 45 deletions
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -16,7 +16,10 @@
|
|||
<a class="markdown-help" href="" {{action "openModal" "markdown"}}><span class="hidden">What is Markdown?</span></a>
|
||||
</header>
|
||||
<section id="entry-markdown-content" class="entry-markdown-content">
|
||||
{{gh-codemirror value=scratch scrollInfo=view.markdownScrollInfo setCodeMirror="setCodeMirror" openModal="openModal"}}
|
||||
{{gh-codemirror value=scratch scrollInfo=view.markdownScrollInfo
|
||||
setCodeMirror="setCodeMirror"
|
||||
openModal="openModal"
|
||||
typingPause="autoSave"}}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Add table
Reference in a new issue