Merge pull request #2618 from novaugust/post-settings

[Ember.js] Create Post Settings Menu and its functionality on the Post controller.
This commit is contained in:
Hannah Wolfe 2014-05-07 20:22:46 +01:00
commit 843a8fd51f
15 changed files with 343 additions and 89 deletions

View File

@ -3,10 +3,6 @@
The contents should be solved properly or moved into ghost-ui package.
*/
.post-settings-menu {
display: none !important;
}
#entry-markdown,
.entry-preview,
.CodeMirror.cm-s-default {
@ -75,4 +71,4 @@
to {
opacity: 1;
}
}
}

View File

@ -0,0 +1,13 @@
var BlurTextField = Ember.TextField.extend({
selectOnClick: false,
click: function (event) {
if (this.get('selectOnClick')) {
event.currentTarget.select();
}
},
focusOut: function () {
this.sendAction('action', this.get('value'));
}
});
export default BlurTextField;

View File

@ -1,8 +1,176 @@
import {parseDateString, formatDate} from 'ghost/utils/date-formatting';
var equal = Ember.computed.equal;
var PostController = Ember.ObjectController.extend({
isPublished: equal('status', 'published'),
isDraft: equal('status', 'draft')
isDraft: equal('status', 'draft'),
isEditingSettings: false,
isStaticPage: function (key, val) {
if (arguments.length > 1) {
this.set('model.page', val ? 1 : 0);
this.get('model').save('page').then(function () {
this.notifications.showSuccess('Succesfully converted ' + (val ? 'to static page' : 'to post'));
}, this.notifications.showErrors);
}
return !!this.get('model.page');
}.property('model.page'),
isOnServer: function () {
return this.get('model.id') !== undefined;
}.property('model.id'),
newSlugBinding: Ember.Binding.oneWay('model.slug'),
slugPlaceholder: null,
// Requests a new slug when the title was changed
updateSlugPlaceholder: function () {
var model,
self = this,
title = this.get('title');
// If there's a title present we want to
// validate it against existing slugs in the db
// and then update the placeholder value.
if (title) {
model = self.get('model');
model.generateSlug().then(function (slug) {
self.set('slugPlaceholder', slug);
}, function () {
self.notifications.showWarn('Unable to generate a slug for "' + title + '"');
});
} else {
// If there's no title set placeholder to blank
// and don't make an ajax request to server
// for a proper slug (as there won't be any).
self.set('slugPlaceholder', '');
}
}.observes('model.title'),
publishedAt: null,
publishedAtChanged: function () {
this.set('publishedAt', formatDate(this.get('model.published_at')));
}.observes('model.published_at'),
actions: {
editSettings: function () {
this.toggleProperty('isEditingSettings');
if (this.get('isEditingSettings')) {
//Stop editing if the user clicks outside the settings view
Ember.run.next(this, function () {
var self = this;
// @TODO has a race condition with click on the editSettings action
$(document).one('click', function () {
self.toggleProperty('isEditingSettings');
});
});
}
},
updateSlug: function () {
var newSlug = this.get('newSlug'),
slug = this.get('model.slug'),
placeholder = this.get('slugPlaceholder'),
self = this;
newSlug = (!newSlug && placeholder) ? placeholder : newSlug;
// Ignore unchanged slugs
if (slug === newSlug) {
return;
}
//reset to model's slug on empty string
if (!newSlug) {
this.set('newSlug', slug);
return;
}
//Validation complete
this.set('model.slug', newSlug);
// If the model doesn't currently
// exist on the server
// then just update the model's value
if (!this.get('isOnServer')) {
return;
}
this.get('model').save('slug').then(function () {
self.notifications.showSuccess('Permalink successfully changed to <strong>' + this.get('model.slug') + '</strong>.');
}, this.notifications.showErrors);
},
updatePublishedAt: function (userInput) {
var errMessage = '',
newPubDate = formatDate(parseDateString(userInput)),
pubDate = this.get('publishedAt'),
newPubDateMoment,
pubDateMoment;
// if there is no new pub date, mark that until the post is published,
// when we'll fill in with the current time.
if (!newPubDate) {
this.set('publishedAt', '');
return;
}
// Check for missing time stamp on new data
// If no time specified, add a 12:00
if (newPubDate && !newPubDate.slice(-5).match(/\d+:\d\d/)) {
newPubDate += " 12:00";
}
newPubDateMoment = parseDateString(newPubDate);
// If there was a published date already set
if (pubDate) {
// Check for missing time stamp on current model
// If no time specified, add a 12:00
if (!pubDate.slice(-5).match(/\d+:\d\d/)) {
pubDate += " 12:00";
}
pubDateMoment = parseDateString(pubDate);
// Quit if the new date is the same
if (pubDateMoment.isSame(newPubDateMoment)) {
return;
}
}
// Validate new Published date
if (!newPubDateMoment.isValid() || newPubDate.substr(0, 12) === "Invalid date") {
errMessage = 'Published Date must be a valid date with format: DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)';
}
if (newPubDateMoment.diff(new Date(), 'h') > 0) {
errMessage = 'Published Date cannot currently be in the future.';
}
if (errMessage) {
// Show error message
this.notifications.showError(errMessage);
//Hack to push a "change" when it's actually staying
// the same.
//This alerts the listener on post-settings-menu
this.notifyPropertyChange('publishedAt');
return;
}
//Validation complete
this.set('model.published_at', newPubDateMoment.toDate());
// If the model doesn't currently
// exist on the server
// then just update the model's value
if (!this.get('isOnServer')) {
return;
}
this.get('model').save('published_at').then(function () {
this.notifications.showSuccess('Publish date successfully changed to <strong>' + this.get('publishedAt') + '</strong>.');
}, this.notifications.showErrors);
}
}
});
export default PostController;
export default PostController;

View File

@ -39,6 +39,10 @@ var defineFixtures = function (status) {
ic.ajax.defineFixture('/ghost/api/v0.1/posts', posts(status));
ic.ajax.defineFixture('/ghost/api/v0.1/posts/1', post(1, status));
ic.ajax.defineFixture('/ghost/api/v0.1/posts/2', post(2, status));
ic.ajax.defineFixture('/ghost/api/v0.1/posts/3', post(3, status));
ic.ajax.defineFixture('/ghost/api/v0.1/posts/4', post(4, status));
ic.ajax.defineFixture('/ghost/api/v0.1/posts/slug/test%20title/', response('generated-slug', status));
ic.ajax.defineFixture('/ghost/api/v0.1/signin', user(status));
ic.ajax.defineFixture('/ghost/api/v0.1/users/me/', user(status));
ic.ajax.defineFixture('/ghost/changepw/', response({
@ -53,4 +57,4 @@ var defineFixtures = function (status) {
ic.ajax.defineFixture('/ghost/api/v0.1/settings/?type=blog,theme,app', settings(status));
};
export default defineFixtures;
export default defineFixtures;

View File

@ -1,5 +1,56 @@
var PostModel = Ember.Object.extend({
import BaseModel from 'ghost/models/base';
var PostModel = BaseModel.extend({
url: BaseModel.apiRoot + '/posts/',
generateSlug: function () {
// @TODO Make this request use this.get('title') once we're an actual user
var url = this.get('url') + 'slug/' + encodeURIComponent('test title') + '/';
return ic.ajax.request(url, {
type: 'GET'
});
},
save: function (properties) {
var url = this.url,
self = this,
type,
validationErrors = this.validate();
if (validationErrors.length) {
return Ember.RSVP.Promise(function (resolve, reject) {
return reject(validationErrors);
});
}
//If specific properties are being saved,
//this is an edit. Otherwise, it's an add.
if (properties && properties.length > 0) {
type = 'PUT';
url += this.get('id');
} else {
type = 'POST';
properties = Ember.keys(this);
}
return ic.ajax.request(url, {
type: type,
data: this.getProperties(properties)
}).then(function (model) {
return self.setProperties(model);
});
},
validate: function () {
var validationErrors = [];
if (!(this.get('title') && this.get('title').length)) {
validationErrors.push({
message: "You must specify a title for the post."
});
}
return validationErrors;
}
});
export default PostModel;
export default PostModel;

View File

@ -1,13 +1,15 @@
import ajax from 'ghost/utils/ajax';
import styleBody from 'ghost/mixins/style-body';
import AuthenticatedRoute from 'ghost/routes/authenticated';
import Post from 'ghost/models/post';
var EditorRoute = AuthenticatedRoute.extend(styleBody, {
classNames: ['editor'],
controllerName: 'posts.post',
model: function (params) {
return ajax('/ghost/api/v0.1/posts/' + params.post_id);
return ajax('/ghost/api/v0.1/posts/' + params.post_id).then(function (post) {
return Post.create(post);
});
}
});
export default EditorRoute;
export default EditorRoute;

View File

@ -1,13 +1,16 @@
import ajax from 'ghost/utils/ajax';
import styleBody from 'ghost/mixins/style-body';
import AuthenticatedRoute from 'ghost/routes/authenticated';
import Post from 'ghost/models/post';
var PostsRoute = AuthenticatedRoute.extend(styleBody, {
classNames: ['manage'],
model: function () {
return ajax('/ghost/api/v0.1/posts').then(function (response) {
return response.posts;
return response.posts.map(function (post) {
return Post.create(post);
});
});
},
@ -18,4 +21,4 @@ var PostsRoute = AuthenticatedRoute.extend(styleBody, {
}
});
export default PostsRoute;
export default PostsRoute;

View File

@ -1,7 +1,10 @@
/*global ajax */
import Post from 'ghost/models/post';
var PostsPostRoute = Ember.Route.extend({
model: function (params) {
return ajax('/ghost/api/v0.1/posts/' + params.post_id);
return ajax('/ghost/api/v0.1/posts/' + params.post_id).then(function (post) {
return Post.create(post);
});
}
});

View File

@ -14,40 +14,8 @@
{{#link-to "editor" this class="post-edit" title="Edit Post"}}
<span class="hidden">Edit Post</span>
{{/link-to}}
<a class="post-settings" href="#" data-toggle=".post-settings-menu" title="Post Settings"><span class="hidden">Post Settings</span></a>
<div class="post-settings-menu menu-drop-right overlay" style="display: none;">
<form>
<table class="plain">
<tbody>
<tr class="post-setting">
<td class="post-setting-label">
<label for="url">URL</label>
</td>
<td class="post-setting-field">
<input id="url" class="post-setting-slug" type="text" value="">
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<label for="pub-date">Pub Date</label>
</td>
<td class="post-setting-field">
<input id="pub-date" class="post-setting-date" type="text" value="" placeholder="17 Feb 14 @ 02:35"><!--<span class="post-setting-calendar"></span>-->
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<span class="label">Static Page</span>
</td>
<td class="post-setting-item">
<input id="static-page" class="post-setting-static-page" type="checkbox" value="">
<label class="checkbox" for="static-page"></label>
</td>
</tr>
</tbody>
</table>
</form>
<a class="delete" href="#">Delete This Post</a>
</div>
<a class="post-settings" title="Post Settings" {{action 'editSettings'}}><span class="hidden">Post Settings</span></a>
<!-- @TODO use Ghost Popover (#2565) --->
{{view "post-settings-menu-view"}}
</section>
</header>
</header>

View File

@ -10,39 +10,9 @@
<div class="right">
<section id="entry-controls">
<a class="post-settings" href="#" data-toggle=".post-settings-menu" title="Post Settings"><span class="hidden">Post Settings</span></a>
<div class="post-settings-menu menu-right overlay">
<form>
<table class="plain">
<tr class="post-setting">
<td class="post-setting-label">
<label for="url">URL</label>
</td>
<td class="post-setting-field">
<input id="url" class="post-setting-slug" type="text" placeholder="" value="" />
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<label for="pub-date">Pub Date</label>
</td>
<td class="post-setting-field">
<input id="pub-date" class="post-setting-date" type="text" placeholder="Now" value=""><!--<span class="post-setting-calendar"></span>-->
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<span class="label">Static Page</span>
</td>
<td class="post-setting-item">
<input id="static-page" class="post-setting-static-page" type="checkbox" value="">
<label class="checkbox" for="static-page"></label>
</td>
</tr>
</table>
</form>
<a class="delete" href="#">Delete This Post</a>
</div>
<a class="post-settings" title="Post Settings" {{action 'editSettings'}}><span class="hidden">Post Settings</span></a>
<!-- @TODO Use Ghost Popover (#2565) and style arrow down -->
{{view "post-settings-menu-view"}}
</section>
<section id="entry-actions" class="js-publish-splitbutton splitbutton-save">
@ -56,4 +26,4 @@
</section>
</div>
</nav>
</footer>
</footer>

View File

@ -0,0 +1,32 @@
<form>
<table class="plain">
<tbody>
<tr class="post-setting">
<td class="post-setting-label">
<label for="url">URL</label>
</td>
<td class="post-setting-field">
{{blur-text-field class="post-setting-slug" id="url" value=newSlug action="updateSlug" placeholder=slugPlaceholder selectOnClick="true"}}
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<label for="pub-date">Pub Date</label>
</td>
<td class="post-setting-field">
{{blur-text-field class="post-setting-date" value=view.publishedAt action="updatePublishedAt" placeholder=view.datePlaceholder}}
</td>
</tr>
<tr class="post-setting">
<td class="post-setting-label">
<label class="label" for="static-page">Static Page</label>
</td>
<td class="post-setting-item">
{{input type="checkbox" name="static-page" id="static-page" class="post-setting-static-page" checked=isStaticPage}}
<label class="checkbox" for="static-page"></label>
</td>
</tr>
</tbody>
</table>
</form>
<a class="delete" {{action 'openModal' 'delete-post' post}}>Delete This Post</a>

View File

@ -0,0 +1,21 @@
/* global moment */
var parseDateFormats = ["DD MMM YY HH:mm",
"DD MMM YYYY HH:mm",
"DD/MM/YY HH:mm",
"DD/MM/YYYY HH:mm",
"DD-MM-YY HH:mm",
"DD-MM-YYYY HH:mm",
"YYYY-MM-DD HH:mm"],
displayDateFormat = 'DD MMM YY @ HH:mm';
//Parses a string to a Moment
var parseDateString = function (value) {
return value ? moment(value, parseDateFormats) : '';
};
//Formats a Date or Moment
var formatDate = function (value) {
return value ? moment(value).format(displayDateFormat) : '';
};
export {parseDateString, formatDate};

View File

@ -15,6 +15,11 @@ var Notifications = Ember.ArrayProxy.extend({
message: message
});
},
showErrors: function (errors) {
for (var i = 0; i < errors.length; i += 1) {
this.showError(errors[i].message || errors[i]);
}
},
showInfo: function (message) {
this.pushObject({
type: 'info',
@ -35,4 +40,4 @@ var Notifications = Ember.ArrayProxy.extend({
}
});
export default Notifications;
export default Notifications;

View File

@ -2,7 +2,7 @@ import itemView from 'ghost/views/item-view';
var PostItemView = itemView.extend({
openEditor: function () {
this.get('controller').send('openEditor', this.get('context')); // send action to handle transition to editor route
this.get('controller').send('openEditor', this.get('controller.model')); // send action to handle transition to editor route
}.on("doubleClick")
});

View File

@ -0,0 +1,18 @@
/* global moment */
import {formatDate} from 'ghost/utils/date-formatting';
var PostSettingsMenuView = Ember.View.extend({
templateName: 'post-settings-menu',
classNames: ['post-settings-menu', 'menu-drop-right', 'overlay'],
classNameBindings: ['controller.isEditingSettings::hidden'],
publishedAtBinding: Ember.Binding.oneWay('controller.publishedAt'),
click: function (event) {
//Stop click propagation to prevent window closing
event.stopPropagation();
},
datePlaceholder: function () {
return formatDate(moment());
}.property('controller.publishedAt')
});
export default PostSettingsMenuView;