1
0
Fork 0
mirror of https://github.com/TryGhost/Ghost-Admin.git synced 2023-12-14 02:33:04 +01:00
Ghost-Admin/app/mixins/editor-base-controller.js
Jason Williams 3ef1167815 Use Ember.inject instead of needs and initializers
No Issue
- Switches to the newer style of dependency injection.
- Instead of injection Controllers via "needs," use
  Ember.inject.controller().
- Get rid of initializers that were only injecting objects
  into various factories. Converts these objects into Ember.Service
  objects and declaratively inject them where needed via
  Ember.inject.service().  The added benefit to this is that it's no
  longer a mystery where these properties/methods come from and it's
  straightforward to inject them where needed.
2015-05-27 07:41:42 -05:00

375 lines
13 KiB
JavaScript

/* global console */
import Ember from 'ember';
import PostModel from 'ghost/models/post';
import boundOneWay from 'ghost/utils/bound-one-way';
import imageManager from 'ghost/utils/ed-image-manager';
// this array will hold properties we need to watch
// to know if the model has been changed (`controller.isDirty`)
var watchedProps = ['model.scratch', 'model.titleScratch', 'model.isDirty', 'model.tags.[]'];
PostModel.eachAttribute(function (name) {
watchedProps.push('model.' + name);
});
export default Ember.Mixin.create({
postTagsInputController: Ember.inject.controller('post-tags-input'),
postSettingsMenuController: Ember.inject.controller('post-settings-menu'),
autoSaveId: null,
timedSaveId: null,
editor: null,
notifications: Ember.inject.service(),
init: function () {
var self = this;
this._super();
window.onbeforeunload = function () {
return self.get('isDirty') ? self.unloadDirtyMessage() : null;
};
},
autoSave: function () {
// Don't save just because we swapped out models
if (this.get('model.isDraft') && !this.get('model.isNew')) {
var autoSaveId,
timedSaveId,
saveOptions;
saveOptions = {
silent: true,
backgroundSave: true
};
timedSaveId = Ember.run.throttle(this, 'send', 'save', saveOptions, 60000, false);
this.set('timedSaveId', timedSaveId);
autoSaveId = Ember.run.debounce(this, 'send', 'save', saveOptions, 3000);
this.set('autoSaveId', autoSaveId);
}
}.observes('model.scratch'),
/**
* By default, a post will not change its publish state.
* Only with a user-set value (via setSaveType action)
* can the post's status change.
*/
willPublish: boundOneWay('model.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('model.tags.@each.name', function () {
return this.get('model.tags').mapBy('name');
}),
postOrPage: Ember.computed('model.page', function () {
return this.get('model.page') ? 'Page' : 'Post';
}),
// compares previousTagNames to tagNames
tagNamesEqual: function () {
var tagNames = this.get('tagNames'),
previousTagNames = this.get('previousTagNames'),
hashCurrent,
hashPrevious;
// beware! even if they have the same length,
// that doesn't mean they're the same.
if (tagNames.length !== previousTagNames.length) {
return false;
}
// instead of comparing with slow, nested for loops,
// perform join on each array and compare the strings
hashCurrent = tagNames.join('');
hashPrevious = previousTagNames.join('');
return hashCurrent === hashPrevious;
},
// a hook created in editor-base-route's setupController
modelSaved: function () {
var model = this.get('model');
// safer to updateTags on save in one place
// rather than in all other places save is called
model.updateTags();
// set previousTagNames to current tagNames for isDirty check
this.set('previousTagNames', this.get('tagNames'));
// `updateTags` triggers `isDirty => true`.
// for a saved model it would otherwise be false.
// if the two "scratch" properties (title and content) match the model, then
// it's ok to set isDirty to false
if (model.get('titleScratch') === model.get('title') &&
model.get('scratch') === model.get('markdown')) {
this.set('isDirty', false);
}
},
// an ugly hack, but necessary to watch all the model's properties
// and more, without having to be explicit and do it manually
isDirty: Ember.computed.apply(Ember, watchedProps.concat(function (key, value) {
if (arguments.length > 1) {
return value;
}
var model = this.get('model'),
markdown = model.get('markdown'),
title = model.get('title'),
titleScratch = model.get('titleScratch'),
scratch = this.get('editor').getValue(),
changedAttributes;
if (!this.tagNamesEqual()) {
return true;
}
if (titleScratch !== title) {
return true;
}
// since `scratch` is not model property, we need to check
// it explicitly against the model's markdown attribute
if (markdown !== scratch) {
return true;
}
// if the Adapter failed to save the model isError will be true
// and we should consider the model still dirty.
if (model.get('isError')) {
return true;
}
// models created on the client always return `isDirty: true`,
// so we need to see which properties have actually changed.
if (model.get('isNew')) {
changedAttributes = Ember.keys(model.changedAttributes());
if (changedAttributes.length) {
return true;
}
return false;
}
// even though we use the `scratch` prop to show edits,
// 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).
return model.get('isDirty');
})),
// used on window.onbeforeunload
unloadDirtyMessage: function () {
return '==============================\n\n' +
'Hey there! It looks like you\'re in the middle of writing' +
' something and you haven\'t saved all of your content.' +
'\n\nSave before you go!\n\n' +
'==============================';
},
// TODO: This has to be moved to the I18n localization file.
// This structure is supposed to be close to the i18n-localization which will be used soon.
messageMap: {
errors: {
post: {
published: {
published: 'Update failed.',
draft: 'Saving failed.'
},
draft: {
published: 'Publish failed.',
draft: 'Saving failed.'
}
}
},
success: {
post: {
published: {
published: 'Updated.',
draft: 'Saved.'
},
draft: {
published: 'Published!',
draft: 'Saved.'
}
}
}
},
showSaveNotification: function (prevStatus, status, delay) {
var message = this.messageMap.success.post[prevStatus][status],
path = this.get('model.absoluteUrl'),
type = this.get('postOrPage'),
notifications = this.get('notifications');
if (status === 'published') {
message += `&nbsp;<a href="${path}">View ${type}</a>`;
}
notifications.showSuccess(message.htmlSafe(), {delayed: delay});
},
showErrorNotification: function (prevStatus, status, errors, delay) {
var message = this.messageMap.errors.post[prevStatus][status],
error = (errors && errors[0] && errors[0].message) || 'Unknown Error',
notifications = this.get('notifications');
message += '<br />' + error;
notifications.showError(message.htmlSafe(), {delayed: delay});
},
shouldFocusTitle: Ember.computed.alias('model.isNew'),
shouldFocusEditor: Ember.computed.not('model.isNew'),
actions: {
save: function (options) {
var status,
prevStatus = this.get('model.status'),
isNew = this.get('model.isNew'),
autoSaveId = this.get('autoSaveId'),
timedSaveId = this.get('timedSaveId'),
self = this,
psmController = this.get('postSettingsMenuController'),
promise,
notifications = this.get('notifications');
options = options || {};
if (options.backgroundSave) {
// do not allow a post's status to be set to published by a background save
status = 'draft';
} else {
status = this.get('willPublish') ? 'published' : 'draft';
}
if (autoSaveId) {
Ember.run.cancel(autoSaveId);
this.set('autoSaveId', null);
}
if (timedSaveId) {
Ember.run.cancel(timedSaveId);
this.set('timedSaveId', null);
}
notifications.closePassive();
// ensure an incomplete tag is finalised before save
this.get('postTagsInputController').send('addNewTag');
// Set the properties that are indirected
// set markdown equal to what's in the editor, minus the image markers.
this.set('model.markdown', this.get('editor').getValue());
this.set('model.status', status);
// Set a default title
if (!this.get('model.titleScratch').trim()) {
this.set('model.titleScratch', '(Untitled)');
}
this.set('model.title', this.get('model.titleScratch'));
this.set('model.meta_title', psmController.get('metaTitleScratch'));
this.set('model.meta_description', psmController.get('metaDescriptionScratch'));
if (!this.get('model.slug')) {
// Cancel any pending slug generation that may still be queued in the
// run loop because we need to run it before the post is saved.
Ember.run.cancel(psmController.get('debounceId'));
psmController.generateAndSetSlug('model.slug');
}
promise = Ember.RSVP.resolve(psmController.get('lastPromise')).then(function () {
return self.get('model').save(options).then(function (model) {
if (!options.silent) {
self.showSaveNotification(prevStatus, model.get('status'), isNew ? true : false);
}
return model;
});
}).catch(function (errors) {
if (!options.silent) {
self.showErrorNotification(prevStatus, self.get('model.status'), errors);
}
self.set('model.status', prevStatus);
return self.get('model');
});
psmController.set('lastPromise', promise);
return promise;
},
setSaveType: function (newType) {
if (newType === 'publish') {
this.set('willPublish', true);
} else if (newType === 'draft') {
this.set('willPublish', false);
} else {
console.warn('Received invalid save type; ignoring.');
}
},
// set from a `sendAction` on the gh-ed-editor component,
// so that we get a reference for handling uploads.
setEditor: function (editor) {
this.set('editor', editor);
},
// fired from the gh-ed-preview component when an image upload starts
disableEditor: function () {
this.get('editor').disable();
},
// fired from the gh-ed-preview component when an image upload finishes
enableEditor: function () {
this.get('editor').enable();
},
// Match the uploaded file to a line in the editor, and update that line with a path reference
// ensuring that everything ends up in the correct place and format.
handleImgUpload: function (e, resultSrc) {
var editor = this.get('editor'),
editorValue = editor.getValue(),
replacement = imageManager.getSrcRange(editorValue, e.target),
cursorPosition;
if (replacement) {
cursorPosition = replacement.start + resultSrc.length + 1;
if (replacement.needsParens) {
resultSrc = '(' + resultSrc + ')';
}
editor.replaceSelection(resultSrc, replacement.start, replacement.end, cursorPosition);
}
},
togglePreview: function (preview) {
this.set('isPreview', preview);
},
autoSaveNew: function () {
if (this.get('model.isNew')) {
this.send('save', {silent: true, backgroundSave: true});
}
}
}
});