🎨 Focus editor content area by default (#768)

closes TryGhost/Ghost#8525

- always give focus to the editor content area by default when loading the editor
- allow content autosave to work for new posts (it was previously turned off for new posts)
- move transition-on-save behaviour from editor/new controller into the controller mixin's save routine
- cancel background autosave when "are you sure you want to leave?" modal is shown as it can cause the "leave" option to fail because it attempts to delete the post record that can be in flight (plus if we're saving anyway it doesn't make much sense to ask the user  🙈) - this is quite an edge-case as it will only happen if the user makes a content change to a draft post then tries to leave the screen within 3 seconds
- change the editor placeholder text
- wait for any save task to finish before exiting the new post route (fixes infinite loop and popup of "are you sure you want to leave?" modal that is then closed automatically straight away
- add a guard to the `gh-post-settings-menu` component so that if the authors query takes a while we don't end up trying to set a value when the component has already been removed
This commit is contained in:
Kevin Ansfield 2017-07-10 16:09:50 +01:00 committed by Katharina Irrgang
parent 1596721468
commit a0af248df4
8 changed files with 33 additions and 53 deletions

View File

@ -40,7 +40,9 @@ export default Component.extend(SettingsMenuMixin, {
this._super(...arguments); this._super(...arguments);
this.get('store').query('user', {limit: 'all'}).then((users) => { this.get('store').query('user', {limit: 'all'}).then((users) => {
this.set('authors', users.sortBy('name')); if (!this.get('isDestroyed')) {
this.set('authors', users.sortBy('name'));
}
}); });
this.get('model.author').then((author) => { this.get('model.author').then((author) => {

View File

@ -48,8 +48,10 @@ export default OneWayTextarea.extend(TextInputMixin, {
// collapse the element first so that we can shrink as well as expand // collapse the element first so that we can shrink as well as expand
// then set the height to match the text height // then set the height to match the text height
el.style.height = 0; if (el) {
el.style.height = `${el.scrollHeight}px`; el.style.height = 0;
el.style.height = `${el.scrollHeight}px`;
}
}, },
_setupAutoExpand() { _setupAutoExpand() {

View File

@ -1,23 +1,6 @@
import Controller from 'ember-controller'; import Controller from 'ember-controller';
import EditorControllerMixin from 'ghost-admin/mixins/editor-base-controller'; import EditorControllerMixin from 'ghost-admin/mixins/editor-base-controller';
function K() {
return this;
}
export default Controller.extend(EditorControllerMixin, { export default Controller.extend(EditorControllerMixin, {
// Overriding autoSave on the base controller, as the new controller shouldn't be autosaving
autoSave: K,
actions: {
/**
* Redirect to editor after the first save
*/
save(options) {
return this._super(options).then((model) => {
if (model.get('id')) {
this.replaceRoute('editor.edit', model);
}
});
}
}
}); });

View File

@ -3,7 +3,7 @@ import Mixin from 'ember-metal/mixin';
import PostModel from 'ghost-admin/models/post'; import PostModel from 'ghost-admin/models/post';
import RSVP from 'rsvp'; import RSVP from 'rsvp';
import boundOneWay from 'ghost-admin/utils/bound-one-way'; import boundOneWay from 'ghost-admin/utils/bound-one-way';
import computed, {alias, mapBy, reads} from 'ember-computed'; import computed, {mapBy, reads} from 'ember-computed';
import ghostPaths from 'ghost-admin/utils/ghost-paths'; import ghostPaths from 'ghost-admin/utils/ghost-paths';
import injectController from 'ember-controller/inject'; import injectController from 'ember-controller/inject';
import injectService from 'ember-service/inject'; import injectService from 'ember-service/inject';
@ -28,6 +28,11 @@ const watchedProps = ['model.scratch', 'model.titleScratch', 'model.hasDirtyAttr
const DEFAULT_TITLE = '(Untitled)'; const DEFAULT_TITLE = '(Untitled)';
const TITLE_DEBOUNCE = testing ? 10 : 700; const TITLE_DEBOUNCE = testing ? 10 : 700;
// time in ms to save after last content edit
const AUTOSAVE_TIMEOUT = 3000;
// time in ms to force a save if the user is continuously typing
const TIMEDSAVE_TIMEOUT = 60000;
PostModel.eachAttribute(function (name) { PostModel.eachAttribute(function (name) {
watchedProps.push(`model.${name}`); watchedProps.push(`model.${name}`);
}); });
@ -52,9 +57,6 @@ export default Mixin.create({
editor: null, editor: null,
editorMenuIsOpen: false, editorMenuIsOpen: false,
shouldFocusTitle: alias('model.isNew'),
shouldFocusEditor: false,
navIsClosed: reads('application.autoNav'), navIsClosed: reads('application.autoNav'),
init() { init() {
@ -65,12 +67,12 @@ export default Mixin.create({
}, },
_canAutosave: computed('model.{isDraft,isNew}', function () { _canAutosave: computed('model.{isDraft,isNew}', function () {
return !testing && this.get('model.isDraft') && !this.get('model.isNew'); return !testing && this.get('model.isDraft');
}), }),
// save 3 seconds after the last edit // save 3 seconds after the last edit
_autosave: task(function* () { _autosave: task(function* () {
yield timeout(3000); yield timeout(AUTOSAVE_TIMEOUT);
if (this.get('_canAutosave')) { if (this.get('_canAutosave')) {
yield this.get('autosave').perform(); yield this.get('autosave').perform();
@ -81,7 +83,7 @@ export default Mixin.create({
_timedSave: task(function* () { _timedSave: task(function* () {
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (!testing && true) { while (!testing && true) {
yield timeout(60000); yield timeout(TIMEDSAVE_TIMEOUT);
if (this.get('_canAutosave')) { if (this.get('_canAutosave')) {
yield this.get('autosave').perform(); yield this.get('autosave').perform();
@ -170,6 +172,12 @@ export default Mixin.create({
this.get('model').set('statusScratch', null); this.get('model').set('statusScratch', null);
// redirect to edit route if saving a new record
if (isNew && model.get('id')) {
this.replaceRoute('editor.edit', model);
return;
}
return model; return model;
}); });
@ -570,6 +578,12 @@ export default Mixin.create({
}, },
toggleLeaveEditorModal(transition) { toggleLeaveEditorModal(transition) {
// cancel autosave when showing the modal to prevent the "leave"
// action failing due to deletion of in-flight records
if (!this.get('showLeaveEditorModal')) {
this.send('cancelAutosave');
}
this.set('leaveEditorTransition', transition); this.set('leaveEditorTransition', transition);
this.toggleProperty('showLeaveEditorModal'); this.toggleProperty('showLeaveEditorModal');
}, },

View File

@ -50,9 +50,10 @@ export default Mixin.create(styleBody, ShortcutsRoute, {
// so we abort the transition and retry after the save has completed. // so we abort the transition and retry after the save has completed.
if (state.isSaving) { if (state.isSaving) {
transition.abort(); transition.abort();
controller.get('generateSlug.last').then(() => { controller.get('saveTasks.last').then(() => {
transition.retry(); transition.retry();
}); });
return;
} }
fromNewToEdit = this.get('routeName') === 'editor.new' fromNewToEdit = this.get('routeName') === 'editor.new'

View File

@ -5,12 +5,6 @@ import base from 'ghost-admin/mixins/editor-base-route';
export default AuthenticatedRoute.extend(base, { export default AuthenticatedRoute.extend(base, {
titleToken: 'Editor', titleToken: 'Editor',
beforeModel(transition) {
this.set('_transitionedFromNew', transition.data.fromNew);
this._super(...arguments);
},
model(params) { model(params) {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
let query = { let query = {
@ -42,11 +36,6 @@ export default AuthenticatedRoute.extend(base, {
}); });
}, },
setupController(controller) {
this._super(...arguments);
controller.set('shouldFocusEditor', this.get('_transitionedFromNew'));
},
actions: { actions: {
authorizationFailed() { authorizationFailed() {
this.get('controller').send('toggleReAuthenticateModal'); this.get('controller').send('toggleReAuthenticateModal');

View File

@ -17,15 +17,5 @@ export default AuthenticatedRoute.extend(base, {
controller, controller,
model model
}); });
},
actions: {
willTransition(transition) {
// decorate the transition object so the editor.edit route
// knows this was the previous active route
transition.data.fromNew = true;
this._super(...arguments);
}
} }
}); });

View File

@ -35,8 +35,8 @@
--}} --}}
{{#gh-markdown-editor {{#gh-markdown-editor
tabindex="2" tabindex="2"
placeholder="Click here to start..." placeholder="Now begin writing your story..."
autofocus=shouldFocusEditor autofocus=true
uploadedImageUrls=editor.uploadedImageUrls uploadedImageUrls=editor.uploadedImageUrls
mobiledoc=(readonly model.scratch) mobiledoc=(readonly model.scratch)
isFullScreen=editor.isFullScreen isFullScreen=editor.isFullScreen
@ -54,7 +54,6 @@
class="gh-editor-title" class="gh-editor-title"
placeholder="Your Post Title" placeholder="Your Post Title"
tabindex="1" tabindex="1"
shouldFocus=shouldFocusTitle
autoExpand=".gh-markdown-editor-pane" autoExpand=".gh-markdown-editor-pane"
focusOut=(action "saveTitle") focusOut=(action "saveTitle")
update=(action (perform updateTitle)) update=(action (perform updateTitle))