diff --git a/core/client/assets/sass/layouts/editor.scss b/core/client/assets/sass/layouts/editor.scss index 88f3125f93..94a024b59f 100644 --- a/core/client/assets/sass/layouts/editor.scss +++ b/core/client/assets/sass/layouts/editor.scss @@ -634,6 +634,20 @@ body.zen { position: relative; padding: 0; z-index: 1; + + &.unsaved { + .post-settings-menu { + padding-bottom: 0; + + .post-setting:nth-child(3) td { + border-bottom: none; + } + + .delete { + display: none; + } + } + } } #entry-actions { diff --git a/core/client/views/editor.js b/core/client/views/editor.js index 1a32618d90..d6ce674ff6 100644 --- a/core/client/views/editor.js +++ b/core/client/views/editor.js @@ -396,6 +396,9 @@ if (rawTitle !== trimmedTitle) { $title.val(trimmedTitle); } + + // Trigger title change for post-settings.js + this.model.set('title', trimmedTitle); }, renderTitle: function () { diff --git a/core/client/views/post-settings.js b/core/client/views/post-settings.js index 5fcf43c5f0..364bd42ccb 100644 --- a/core/client/views/post-settings.js +++ b/core/client/views/post-settings.js @@ -21,15 +21,17 @@ this.listenTo(this.model, 'change:status', this.render); this.listenTo(this.model, 'change:published_at', this.render); this.listenTo(this.model, 'change:page', this.render); + this.listenTo(this.model, 'change:title', this.updateSlugPlaceholder); } }, render: function () { var slug = this.model ? this.model.get('slug') : '', pubDate = this.model ? this.model.get('published_at') : 'Not Published', - $pubDateEl = this.$('.post-setting-date'); + $pubDateEl = this.$('.post-setting-date'), + $postSettingSlugEl = this.$('.post-setting-slug'); - $('.post-setting-slug').val(slug); + $postSettingSlugEl.val(slug); // Update page status test if already a page. if (this.model && this.model.get('page')) { @@ -46,9 +48,42 @@ this.$('.delete').removeClass('hidden'); } + // Apply different style for model's that aren't + // yet persisted to the server. + // Mostly we're hiding the delete post UI + if (this.model.id === undefined) { + this.$el.addClass('unsaved'); + } else { + this.$el.removeClass('unsaved'); + } + $pubDateEl.val(pubDate); }, + // Requests a new slug when the title was changed + updateSlugPlaceholder: function () { + var title = this.model.get('title'), + $postSettingSlugEl = this.$('.post-setting-slug'); + + // 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) { + $.ajax({ + url: Ghost.paths.apiRoot + '/posts/getSlug/' + encodeURIComponent(title) + '/', + success: function (result) { + $postSettingSlugEl.attr('placeholder', result); + } + }); + } 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). + $postSettingSlugEl.attr('placeholder', ''); + return; + } + }, + selectSlug: function (e) { e.currentTarget.select(); }, @@ -60,8 +95,18 @@ slugEl = e.currentTarget, newSlug = slugEl.value; - // Ignore empty or unchanged slugs - if (newSlug.length === 0 || slug === newSlug) { + // If the model doesn't currently + // exist on the server (aka has no id) + // then just update the model's value + if (self.model.id === undefined) { + this.model.set({ + slug: newSlug + }); + return; + } + + // Ignore unchanged slugs + if (slug === newSlug) { slugEl.value = slug === undefined ? '' : slug; return; } @@ -102,7 +147,7 @@ pubDateMoment, newPubDateMoment; - // Ignore empty or unchanged dates + // if there is no new pub date do nothing if (!newPubDate) { return; } @@ -155,6 +200,16 @@ return; } + // If the model doesn't currently + // exist on the server (aka has no id) + // then just update the model's value + if (self.model.id === undefined) { + this.model.set({ + published_at: newPubDateMoment.toDate() + }); + return; + } + // Save new 'Published' date this.model.save({ published_at: newPubDateMoment.toDate() @@ -183,6 +238,16 @@ var pageEl = $(e.currentTarget), page = pageEl.prop('checked'); + // Don't try to save + // if the model doesn't currently + // exist on the server + if (this.model.id === undefined) { + this.model.set({ + page: page + }); + return; + } + this.model.save({ page: page }, { @@ -209,6 +274,11 @@ deletePost: function (e) { e.preventDefault(); var self = this; + // You can't delete a post + // that hasn't yet been saved + if (this.model.id === undefined) { + return; + } this.addSubview(new Ghost.Views.Modal({ model: { options: { diff --git a/core/server/api/posts.js b/core/server/api/posts.js index 010f44d502..9879d8118a 100644 --- a/core/server/api/posts.js +++ b/core/server/api/posts.js @@ -48,6 +48,15 @@ posts = { }); }, + getSlug: function getSlug(args) { + return dataProvider.Base.Model.generateSlug(dataProvider.Post, args.title, {status: 'all'}).then(function (slug) { + if (slug) { + return slug; + } + return when.reject({errorCode: 500, message: 'Could not generate slug'}); + }); + }, + // #### Edit // **takes:** a json object with all the properties which should be updated diff --git a/core/server/models/base.js b/core/server/models/base.js index 3029504aaf..284a70692d 100644 --- a/core/server/models/base.js +++ b/core/server/models/base.js @@ -93,67 +93,6 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ sanitize: function (attr) { return sanitize(this.get(attr)).xss(); - }, - - // #### generateSlug - // Create a string act as the permalink for an object. - generateSlug: function (Model, base, readOptions) { - var slug, - slugTryCount = 1, - // Look for a post with a matching slug, append an incrementing number if so - checkIfSlugExists = function (slugToFind) { - var args = {slug: slugToFind}; - //status is needed for posts - if (readOptions && readOptions.status) { - args.status = readOptions.status; - } - return Model.findOne(args, readOptions).then(function (found) { - var trimSpace; - - if (!found) { - return when.resolve(slugToFind); - } - - slugTryCount += 1; - - // If this is the first time through, add the hyphen - if (slugTryCount === 2) { - slugToFind += '-'; - } else { - // Otherwise, trim the number off the end - trimSpace = -(String(slugTryCount - 1).length); - slugToFind = slugToFind.slice(0, trimSpace); - } - - slugToFind += slugTryCount; - - return checkIfSlugExists(slugToFind); - }); - }; - - // Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"` - slug = base.trim().replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '') - // Replace dots and spaces with a dash - .replace(/(\s|\.)/g, '-') - // Convert 2 or more dashes into a single dash - .replace(/-+/g, '-') - // Make the whole thing lowercase - .toLowerCase(); - - // Remove trailing hyphen - slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug; - // Remove non ascii characters - slug = unidecode(slug); - // Check the filtered slug doesn't match any of the reserved keywords - slug = /^(ghost|ghost\-admin|admin|wp\-admin|wp\-login|dashboard|logout|login|signin|signup|signout|register|archive|archives|category|categories|tag|tags|page|pages|post|posts|user|users)$/g - .test(slug) ? slug + '-post' : slug; - - //if slug is empty after trimming use "post" - if (!slug) { - slug = 'post'; - } - // Test for duplicate slugs. - return checkIfSlugExists(slug); } }, { @@ -236,6 +175,67 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({ 'delete': function () { return this.destroy.apply(this, arguments); + }, + + // #### generateSlug + // Create a string act as the permalink for an object. + generateSlug: function (Model, base, readOptions) { + var slug, + slugTryCount = 1, + // Look for a post with a matching slug, append an incrementing number if so + checkIfSlugExists = function (slugToFind) { + var args = {slug: slugToFind}; + //status is needed for posts + if (readOptions && readOptions.status) { + args.status = readOptions.status; + } + return Model.findOne(args, readOptions).then(function (found) { + var trimSpace; + + if (!found) { + return when.resolve(slugToFind); + } + + slugTryCount += 1; + + // If this is the first time through, add the hyphen + if (slugTryCount === 2) { + slugToFind += '-'; + } else { + // Otherwise, trim the number off the end + trimSpace = -(String(slugTryCount - 1).length); + slugToFind = slugToFind.slice(0, trimSpace); + } + + slugToFind += slugTryCount; + + return checkIfSlugExists(slugToFind); + }); + }; + + // Remove URL reserved chars: `:/?#[]@!$&'()*+,;=` as well as `\%<>|^~£"` + slug = base.trim().replace(/[:\/\?#\[\]@!$&'()*+,;=\\%<>\|\^~£"]/g, '') + // Replace dots and spaces with a dash + .replace(/(\s|\.)/g, '-') + // Convert 2 or more dashes into a single dash + .replace(/-+/g, '-') + // Make the whole thing lowercase + .toLowerCase(); + + // Remove trailing hyphen + slug = slug.charAt(slug.length - 1) === '-' ? slug.substr(0, slug.length - 1) : slug; + // Remove non ascii characters + slug = unidecode(slug); + // Check the filtered slug doesn't match any of the reserved keywords + slug = /^(ghost|ghost\-admin|admin|wp\-admin|wp\-login|dashboard|logout|login|signin|signup|signout|register|archive|archives|category|categories|tag|tags|page|pages|post|posts|user|users)$/g + .test(slug) ? slug + '-post' : slug; + + //if slug is empty after trimming use "post" + if (!slug) { + slug = 'post'; + } + // Test for duplicate slugs. + return checkIfSlugExists(slug); } }); diff --git a/core/server/models/post.js b/core/server/models/post.js index 394e8fc807..fb24db0d04 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -67,7 +67,7 @@ Post = ghostBookshelf.Model.extend({ if (this.hasChanged('slug')) { // Pass the new slug through the generator to strip illegal characters, detect duplicates - return this.generateSlug(Post, this.get('slug'), {status: 'all', transacting: options.transacting}) + return ghostBookshelf.Model.generateSlug(Post, this.get('slug'), {status: 'all', transacting: options.transacting}) .then(function (slug) { self.set({slug: slug}); }); @@ -85,9 +85,11 @@ Post = ghostBookshelf.Model.extend({ ghostBookshelf.Model.prototype.creating.call(this); + // We require a slug be set when creating a new post + // as the database doesn't allow null slug values. if (!this.get('slug')) { // Generating a slug requires a db call to look for conflicting slugs - return this.generateSlug(Post, this.get('title'), {status: 'all', transacting: options.transacting}) + return ghostBookshelf.Model.generateSlug(Post, this.get('title'), {status: 'all', transacting: options.transacting}) .then(function (slug) { self.set({slug: slug}); }); @@ -398,7 +400,6 @@ Post = ghostBookshelf.Model.extend({ return post.destroy(options); }); } - }); Posts = ghostBookshelf.Collection.extend({ diff --git a/core/server/models/tag.js b/core/server/models/tag.js index 962b89be4a..b8d66bec85 100644 --- a/core/server/models/tag.js +++ b/core/server/models/tag.js @@ -24,7 +24,7 @@ Tag = ghostBookshelf.Model.extend({ if (!this.get('slug')) { // Generating a slug requires a db call to look for conflicting slugs - return this.generateSlug(Tag, this.get('name')) + return ghostBookshelf.Model.generateSlug(Tag, this.get('name')) .then(function (slug) { self.set({slug: slug}); }); diff --git a/core/server/models/user.js b/core/server/models/user.js index 8c45de05d6..04ff8db270 100644 --- a/core/server/models/user.js +++ b/core/server/models/user.js @@ -58,7 +58,7 @@ User = ghostBookshelf.Model.extend({ if (!this.get('slug')) { // Generating a slug requires a db call to look for conflicting slugs - return this.generateSlug(User, this.get('name')) + return ghostBookshelf.Model.generateSlug(User, this.get('name')) .then(function (slug) { self.set({slug: slug}); }); diff --git a/core/server/routes/api.js b/core/server/routes/api.js index ffb0be2bc3..c642d959bb 100644 --- a/core/server/routes/api.js +++ b/core/server/routes/api.js @@ -10,6 +10,7 @@ module.exports = function (server) { server.get('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.read)); server.put('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.edit)); server.del('/ghost/api/v0.1/posts/:id', middleware.authAPI, api.requestHandler(api.posts.destroy)); + server.get('/ghost/api/v0.1/posts/getSlug/:title', middleware.authAPI, api.requestHandler(api.posts.getSlug)); // #### Settings server.get('/ghost/api/v0.1/settings/', middleware.authAPI, api.requestHandler(api.settings.browse)); server.get('/ghost/api/v0.1/settings/:key/', middleware.authAPI, api.requestHandler(api.settings.read)); diff --git a/core/server/views/editor.hbs b/core/server/views/editor.hbs index f95b4f92f7..4c26829cc3 100644 --- a/core/server/views/editor.hbs +++ b/core/server/views/editor.hbs @@ -40,7 +40,7 @@
- +
@@ -51,7 +51,7 @@ - + @@ -59,7 +59,7 @@ - +