diff --git a/core/client/controllers/settings/tags.js b/core/client/controllers/settings/tags.js index 92041fd6a2..5b83e9bfd3 100644 --- a/core/client/controllers/settings/tags.js +++ b/core/client/controllers/settings/tags.js @@ -4,13 +4,14 @@ import boundOneWay from 'ghost/utils/bound-one-way'; var TagsController = Ember.ArrayController.extend(PaginationMixin, { tags: Ember.computed.alias('model'), + needs: 'application', + activeTag: null, activeTagNameScratch: boundOneWay('activeTag.name'), activeTagSlugScratch: boundOneWay('activeTag.slug'), activeTagDescriptionScratch: boundOneWay('activeTag.description'), - - // Tag properties that should not be set to the empty string - requiredTagProperties: ['name', 'slug'], + activeTagMetaTitleScratch: boundOneWay('activeTag.meta_title'), + activeTagMetaDescriptionScratch: boundOneWay('activeTag.meta_description'), init: function (options) { options = options || {}; @@ -18,18 +19,30 @@ var TagsController = Ember.ArrayController.extend(PaginationMixin, { this._super(options); }, + isViewingSubview: Ember.computed('controllers.application.showSettingsMenu', function (key, value) { + // Not viewing a subview if we can't even see the PSM + if (!this.get('controllers.application.showSettingsMenu')) { + return false; + } + if (arguments.length > 1) { + return value; + } + + return false; + }), + + showErrors: function (errors) { + errors = Ember.isArray(errors) ? errors : [errors]; + this.notifications.showErrors(errors); + }, + saveActiveTagProperty: function (propKey, newValue) { var activeTag = this.get('activeTag'), currentValue = activeTag.get(propKey), - requiredTagProps = this.get('requiredTagProperties'), - self = this, - tagName; + self = this; newValue = newValue.trim(); - // Quit if value is empty for a required property - if (!newValue && requiredTagProps.contains(propKey)) { - return; - } + // Quit if there was no change if (newValue === currentValue) { return; @@ -37,19 +50,59 @@ var TagsController = Ember.ArrayController.extend(PaginationMixin, { activeTag.set(propKey, newValue); - tagName = activeTag.get('name'); - // don't save a new tag until it has a name - if (!tagName) { - return; - } + this.notifications.closePassive(); - activeTag.save().then(function () { - self.notifications.showSuccess('Saved ' + tagName); - }).catch(function (error) { - self.notifications.showAPIError(error); + activeTag.save().catch(function (errors) { + self.showErrors(errors); }); }, + seoTitle: Ember.computed('scratch', 'activeTagNameScratch', 'activeTagMetaTitleScratch', function () { + var metaTitle = this.get('activeTagMetaTitleScratch') || ''; + + metaTitle = metaTitle.length > 0 ? metaTitle : this.get('activeTagNameScratch'); + + if (metaTitle && metaTitle.length > 70) { + metaTitle = metaTitle.substring(0, 70).trim(); + metaTitle = Ember.Handlebars.Utils.escapeExpression(metaTitle); + metaTitle = new Ember.Handlebars.SafeString(metaTitle + '…'); + } + + return metaTitle; + }), + + seoURL: Ember.computed('activeTagSlugScratch', function () { + var blogUrl = this.get('config').blogUrl, + seoSlug = this.get('activeTagSlugScratch') ? this.get('activeTagSlugScratch') : '', + seoURL = blogUrl + '/tag/' + seoSlug; + + // only append a slash to the URL if the slug exists + if (seoSlug) { + seoURL += '/'; + } + + if (seoURL.length > 70) { + seoURL = seoURL.substring(0, 70).trim(); + seoURL = new Ember.Handlebars.SafeString(seoURL + '…'); + } + + return seoURL; + }), + + seoDescription: Ember.computed('scratch', 'activeTagDescriptionScratch', 'activeTagMetaDescriptionScratch', function () { + var metaDescription = this.get('activeTagMetaDescriptionScratch') || ''; + + metaDescription = metaDescription.length > 0 ? metaDescription : this.get('activeTagDescriptionScratch'); + + if (metaDescription && metaDescription.length > 156) { + metaDescription = metaDescription.substring(0, 156).trim(); + metaDescription = Ember.Handlebars.Utils.escapeExpression(metaDescription); + metaDescription = new Ember.Handlebars.SafeString(metaDescription + '…'); + } + + return metaDescription; + }), + actions: { newTag: function () { this.set('activeTag', this.store.createRecord('tag')); @@ -84,6 +137,22 @@ var TagsController = Ember.ArrayController.extend(PaginationMixin, { saveActiveTagDescription: function (description) { this.saveActiveTagProperty('description', description); + }, + + saveActiveTagMetaTitle: function (metaTitle) { + this.saveActiveTagProperty('meta_title', metaTitle); + }, + + saveActiveTagMetaDescription: function (metaDescription) { + this.saveActiveTagProperty('meta_description', metaDescription); + }, + + showSubview: function () { + this.set('isViewingSubview', true); + }, + + closeSubview: function () { + this.set('isViewingSubview', false); } } }); diff --git a/core/client/mixins/validation-engine.js b/core/client/mixins/validation-engine.js index 34b951855c..089f1ac06f 100644 --- a/core/client/mixins/validation-engine.js +++ b/core/client/mixins/validation-engine.js @@ -9,6 +9,7 @@ import ForgotValidator from 'ghost/validators/forgotten'; import SettingValidator from 'ghost/validators/setting'; import ResetValidator from 'ghost/validators/reset'; import UserValidator from 'ghost/validators/user'; +import TagSettingsValidator from 'ghost/validators/tag-settings'; // our extensions to the validator library ValidatorExtensions.init(); @@ -71,7 +72,8 @@ var ValidationEngine = Ember.Mixin.create({ forgotten: ForgotValidator, setting: SettingValidator, reset: ResetValidator, - user: UserValidator + user: UserValidator, + tag: TagSettingsValidator }, /** diff --git a/core/client/models/tag.js b/core/client/models/tag.js index 2ee7407b5c..bc16f8291d 100644 --- a/core/client/models/tag.js +++ b/core/client/models/tag.js @@ -1,6 +1,9 @@ +import ValidationEngine from 'ghost/mixins/validation-engine'; import NProgressSaveMixin from 'ghost/mixins/nprogress-save'; -var Tag = DS.Model.extend(NProgressSaveMixin, { +var Tag = DS.Model.extend(NProgressSaveMixin, ValidationEngine, { + validationType: 'tag', + uuid: DS.attr('string'), name: DS.attr('string'), slug: DS.attr('string'), diff --git a/core/client/templates/settings/tags/settings-menu.hbs b/core/client/templates/settings/tags/settings-menu.hbs index 4c530b3110..0ae8c811bd 100644 --- a/core/client/templates/settings/tags/settings-menu.hbs +++ b/core/client/templates/settings/tags/settings-menu.hbs @@ -1,6 +1,6 @@ <div class="content-cover" {{action "closeSettingsMenu"}}></div> -<div class="settings-menu-container"> - <div class="settings-menu settings-menu-pane settings-menu-pane-in"> +{{#gh-tabs-manager selected="showSubview" class="settings-menu-container"}} + <div {{bind-attr class="isViewingSubview:settings-menu-pane-out-left:settings-menu-pane-in :settings-menu :settings-menu-pane"}}> <div class="settings-menu-header"> <h4>Tag Settings</h4> <button class="close icon-x settings-menu-header-action" {{action "closeSettingsMenu"}}> @@ -11,12 +11,12 @@ <form> <div class="form-group"> <label>Tag Name</label> - {{gh-input type="text" placeholder=activeTag.name value=activeTagNameScratch focus-out="saveActiveTagName"}} + {{gh-input type="text" value=activeTagNameScratch focus-out="saveActiveTagName"}} </div> <div class="form-group"> <label>Slug</label>{{!--@TODO show full url preview, not just slug--}} - {{gh-input type="text" placeholder=activeTag.slug value=activeTagSlugScratch focus-out="saveActiveTagSlug"}} + {{gh-input type="text" value=activeTagSlugScratch focus-out="saveActiveTagSlug"}} </div> <div class="form-group"> @@ -24,10 +24,52 @@ {{gh-textarea value=activeTagDescriptionScratch focus-out="saveActiveTagDescription"}} </div> + <ul class="nav-list nav-list-block"> + {{#gh-tab tagName="li" classNames="nav-list-item"}} + <button type="button"> + <b>Meta Data</b> + <span>Extra content for SEO and social media.</span> + </button> + {{/gh-tab}} + </ul> + {{#unless activeTag.isNew}} - <button class="btn btn-red icon-trash" {{action "deleteTag" activeTag}}>Delete Tag</button> + <button type="button" class="btn btn-red icon-trash" {{action "deleteTag" activeTag}}>Delete Tag</button> {{/unless}} </form> </div> - </div> -</div> + </div>{{! .settings-menu-pane }} + + <div {{bind-attr class="isViewingSubview:settings-menu-pane-in:settings-menu-pane-out-right :settings-menu :settings-menu-pane"}}> + {{#gh-tab-pane}} + <div class="settings-menu-header subview"> + <button {{action "closeSubview"}} class="back icon-chevron-left settings-menu-header-action"><span class="hidden">Back</span></button> + <h4>Meta Data</h4> + </div> + + <div class="settings-menu-content"> + <form> + <div class="form-group"> + <label for="meta-title">Meta Title</label> + {{gh-input type="text" value=activeTagMetaTitleScratch focus-out="saveActiveTagMetaTitle"}} + <p>Recommended: <b>70</b> characters. You’ve used {{gh-count-down-characters activeTagMetaTitleScratch 70}}</p> + </div> + + <div class="form-group"> + <label for="meta-description">Meta Description</label> + {{gh-textarea value=activeTagMetaDescriptionScratch focus-out="saveActiveTagMetaDescription"}} + <p>Recommended: <b>156</b> characters. You’ve used {{gh-count-down-characters activeTagMetaDescriptionScratch 156}}</p> + </div> + + <div class="form-group"> + <label>Search Engine Result Preview</label> + <div class="seo-preview"> + <div class="seo-preview-title">{{seoTitle}}</div> + <div class="seo-preview-link">{{seoURL}}</div> + <div class="seo-preview-description">{{seoDescription}}</div> + </div> + </form> + </div>{{! .settings-menu-content }} + {{/gh-tab-pane}} + </div>{{! .settings-menu-pane }} +{{/gh-tabs-manager}} \ No newline at end of file diff --git a/core/client/validators/tag-settings.js b/core/client/validators/tag-settings.js new file mode 100644 index 0000000000..883b40bdfe --- /dev/null +++ b/core/client/validators/tag-settings.js @@ -0,0 +1,28 @@ +var TagSettingsValidator = Ember.Object.create({ + check: function (model) { + var validationErrors = [], + data = model.getProperties('name', 'meta_title', 'meta_description'); + + if (validator.empty(data.name)) { + validationErrors.push({ + message: 'You must specify a name for the tag.' + }); + } + + if (!validator.isLength(data.meta_title, 0, 150)) { + validationErrors.push({ + message: 'Meta Title cannot be longer than 150 characters.' + }); + } + + if (!validator.isLength(data.meta_description, 0, 200)) { + validationErrors.push({ + message: 'Meta Description cannot be longer than 200 characters.' + }); + } + + return validationErrors; + } +}); + +export default TagSettingsValidator;