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..efdcd55a87 100644 --- a/core/client/views/post-settings.js +++ b/core/client/views/post-settings.js @@ -21,6 +21,7 @@ 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); } }, @@ -49,6 +50,19 @@ $pubDateEl.val(pubDate); }, + // Requests a new slug when the title was changed + updateSlugPlaceholder: function () { + var title = this.model.get('title'); + + $.ajax({ + url: Ghost.paths.apiRoot + '/posts/getSlug/' + encodeURIComponent(title), + success: function (result){ + // ToDo: Find better selector + $('.post-setting-slug')[0].placeholder = result; + } + }); + }, + selectSlug: function (e) { e.currentTarget.select(); }, @@ -60,8 +74,8 @@ slugEl = e.currentTarget, newSlug = slugEl.value; - // Ignore empty or unchanged slugs - if (newSlug.length === 0 || slug === newSlug) { + // Ignore unchanged slugs + if (slug === newSlug) { slugEl.value = slug === undefined ? '' : slug; return; } @@ -102,11 +116,6 @@ pubDateMoment, newPubDateMoment; - // Ignore empty or unchanged dates - if (!newPubDate) { - 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/)) { diff --git a/core/server/api/posts.js b/core/server/api/posts.js index 22fd61f31b..af8c08135e 100644 --- a/core/server/api/posts.js +++ b/core/server/api/posts.js @@ -47,6 +47,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..03a6a7cfce 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..f5c518aedd 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}); }); @@ -84,14 +84,6 @@ Post = ghostBookshelf.Model.extend({ } ghostBookshelf.Model.prototype.creating.call(this); - - 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}) - .then(function (slug) { - self.set({slug: slug}); - }); - } }, updateTags: function (newTags, attr, options) { @@ -397,6 +389,9 @@ Post = ghostBookshelf.Model.extend({ return post.destroy(options); }); + }, + getSlug: function(options) { + } }); 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..b16dce0c79 100644 --- a/core/server/views/editor.hbs +++ b/core/server/views/editor.hbs @@ -51,7 +51,7 @@