0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Add {{navigation}} helper

closes #4541
creates a handlebars helper with behavior matching the spec in #4541 and
updates `frontend.js` to include the navigation data in the rendered page
context.
- checks for {{current}} against `relativeUrl`
- adds helper `getSiteNavigation()` which returns contents of
  `api.settings.read('navigation')`, or an empty list
- navigation helper is responsible for filtering and consistently formatting
  navigation data from settings.
- changes `frontend.js`'s `formatResponse` & `formatPageResponse` to return
  a promise with page data and updates frontend controllers to use it.
- `formatPageResponse` now includes a third parameter to allow values to be
  merged into the page response (rather than using `_.extend` in the
  render methods directly.
- {{navigation}} will render an empty `ul` if no navigation items exist
- incorporates {{url}}/urlFor behavior for nav contexts. (see #4862)
- uses {{url absolute="true"}} in default nav template
This commit is contained in:
Marcos Ojeda 2015-01-20 23:00:38 -08:00
parent a110b3741f
commit d28ffef3e9
5 changed files with 223 additions and 33 deletions

View file

@ -37,7 +37,27 @@ function getPostPage(options) {
});
}
function formatPageResponse(posts, page) {
/**
* returns a promise with an array of values used in {{navigation}}
* TODO(nsfmc): should this be in the 'prePostsRender' pipeline?
* @return {Promise} containing an array of navigation items
*/
function getSiteNavigation() {
return Promise.resolve(api.settings.read('navigation')).then(function (result) {
if (result && result.settings && result.settings.length) {
return JSON.parse(result.settings[0].value) || [];
}
return [];
});
}
/**
* formats variables for handlebars in multi-post contexts.
* If extraValues are available, they are merged in the final value
* TODO(nsfmc): should this be in the 'prePostsRender' pipeline?
* @return {Promise} containing page variables
*/
function formatPageResponse(posts, page, extraValues) {
// Delete email from author for frontend output
// TODO: do this on API level if no context is available
posts = _.each(posts, function (post) {
@ -46,19 +66,36 @@ function formatPageResponse(posts, page) {
}
return post;
});
return {
posts: posts,
pagination: page.meta.pagination
};
extraValues = extraValues || {};
return getSiteNavigation().then(function (navigation) {
var resp = {
posts: posts,
pagination: page.meta.pagination,
nav: navigation || {}
};
return _.extend(resp, extraValues);
});
}
/**
* similar to formatPageResponse, but for single post pages
* TODO(nsfmc): should this be in the 'prePostsRender' pipeline?
* @return {Promise} containing page variables
*/
function formatResponse(post) {
// Delete email from author for frontend output
// TODO: do this on API level if no context is available
if (post.author) {
delete post.author.email;
}
return {post: post};
return getSiteNavigation().then(function (navigation) {
return {
post: post,
nav: navigation
};
});
}
function handleError(next) {
@ -155,7 +192,9 @@ frontendControllers = {
}
setResponseContext(req, res);
res.render(view, formatPageResponse(posts, page));
formatPageResponse(posts, page).then(function (result) {
res.render(view, result);
});
});
});
}).catch(handleError(next));
@ -198,19 +237,19 @@ frontendControllers = {
// Render the page of posts
filters.doFilter('prePostsRender', page.posts).then(function (posts) {
getActiveThemePaths().then(function (paths) {
var view = template.getThemeViewForTag(paths, options.tag),
var view = template.getThemeViewForTag(paths, options.tag);
// Format data for template
result = _.extend(formatPageResponse(posts, page), {
tag: page.meta.filters.tags ? page.meta.filters.tags[0] : ''
});
// If the resulting tag is '' then 404.
if (!result.tag) {
return next();
}
setResponseContext(req, res);
res.render(view, result);
formatPageResponse(posts, page, {
tag: page.meta.filters.tags ? page.meta.filters.tags[0] : ''
}).then(function (result) {
// If the resulting tag is '' then 404.
if (!result.tag) {
return next();
}
setResponseContext(req, res);
res.render(view, result);
});
});
});
}).catch(handleError(next));
@ -253,20 +292,20 @@ frontendControllers = {
// Render the page of posts
filters.doFilter('prePostsRender', page.posts).then(function (posts) {
getActiveThemePaths().then(function (paths) {
var view = paths.hasOwnProperty('author.hbs') ? 'author' : 'index',
var view = paths.hasOwnProperty('author.hbs') ? 'author' : 'index';
// Format data for template
result = _.extend(formatPageResponse(posts, page), {
author: page.meta.filters.author ? page.meta.filters.author : ''
});
formatPageResponse(posts, page, {
author: page.meta.filters.author ? page.meta.filters.author : ''
}).then(function (result) {
// If the resulting author is '' then 404.
if (!result.author) {
return next();
}
// If the resulting author is '' then 404.
if (!result.author) {
return next();
}
setResponseContext(req, res);
res.render(view, result);
setResponseContext(req, res);
res.render(view, result);
});
});
});
}).catch(handleError(next));
@ -339,12 +378,13 @@ frontendControllers = {
filters.doFilter('prePostsRender', post).then(function (post) {
getActiveThemePaths().then(function (paths) {
var view = template.getThemeViewForPost(paths, post),
response = formatResponse(post);
var view = template.getThemeViewForPost(paths, post);
setResponseContext(req, res, response);
return formatResponse(post).then(function (response) {
setResponseContext(req, res, response);
res.render(view, response);
res.render(view, response);
});
});
});
}

View file

@ -27,6 +27,7 @@ coreHelpers.is = require('./is');
coreHelpers.has = require('./has');
coreHelpers.meta_description = require('./meta_description');
coreHelpers.meta_title = require('./meta_title');
coreHelpers.navigation = require('./navigation');
coreHelpers.page_url = require('./page_url');
coreHelpers.pageUrl = require('./page_url').deprecated;
coreHelpers.pagination = require('./pagination');
@ -89,6 +90,7 @@ registerHelpers = function (adminHbs) {
registerThemeHelper('foreach', coreHelpers.foreach);
registerThemeHelper('is', coreHelpers.is);
registerThemeHelper('has', coreHelpers.has);
registerThemeHelper('navigation', coreHelpers.navigation);
registerThemeHelper('page_url', coreHelpers.page_url);
registerThemeHelper('pageUrl', coreHelpers.pageUrl);
registerThemeHelper('pagination', coreHelpers.pagination);

View file

@ -0,0 +1,58 @@
// ### Navigation Helper
// `{{navigation}}`
// Outputs navigation menu of static urls
var _ = require('lodash'),
hbs = require('express-hbs'),
errors = require('../errors'),
template = require('./template'),
navigation;
navigation = function (options) {
/*jshint unused:false*/
var nav,
context,
currentUrl = this.relativeUrl;
if (!_.isObject(this.nav) || _.isFunction(this.nav)) {
return errors.logAndThrowError('navigation data is not an object or is a function');
}
if (this.nav.filter(function (e) {
return (_.isUndefined(e.label) || _.isUndefined(e.url));
}).length > 0) {
return errors.logAndThrowError('All values must be defined for label, url and current');
}
// check for non-null string values
if (this.nav.filter(function (e) {
return ((!_.isNull(e.label) && !_.isString(e.label)) ||
(!_.isNull(e.url) && !_.isString(e.url)));
}).length > 0) {
return errors.logAndThrowError('Invalid value, Url and Label must be strings');
}
function _slugify(label) {
return label.toLowerCase().replace(/[^\w ]+/g, '').replace(/ +/g, '-');
}
// {{navigation}} should no-op if no data passed in
if (this.nav.length === 0) {
return new hbs.SafeString('');
}
nav = this.nav.map(function (e) {
var out = {};
out.current = e.url === currentUrl;
out.label = e.label;
out.slug = _slugify(e.label);
out.url = hbs.handlebars.Utils.escapeExpression(e.url);
return out;
});
context = _.merge({}, {nav: nav});
return template.execute('navigation', context);
};
module.exports = navigation;

View file

@ -0,0 +1,5 @@
<ul class="nav">
{{#foreach nav}}
<li class="nav-{{slug}} {{#if current}}nav-current{{/if}}"><a href="{{url absolute="true"}}">{{label}}</a></li>
{{/foreach}}
</ul>

View file

@ -0,0 +1,85 @@
/*globals describe, before, it*/
/*jshint expr:true*/
var should = require('should'),
hbs = require('express-hbs'),
utils = require('./utils'),
// Stuff we are testing
handlebars = hbs.handlebars,
helpers = require('../../../server/helpers');
describe('{{navigation}} helper', function () {
before(function (done) {
utils.loadHelpers();
hbs.express3({partialsDir: [utils.config.paths.helperTemplates]});
hbs.cachePartials(function () {
done();
});
});
it('has loaded navigation helper', function () {
should.exist(handlebars.helpers.navigation);
});
it('should throw errors on invalid data', function () {
var runHelper = function (data) {
return function () {
helpers.navigation.call(data);
};
};
runHelper('not an object').should.throwError('navigation data is not an object or is a function');
runHelper(function () {}).should.throwError('navigation data is not an object or is a function');
runHelper({nav: [{label: 1, url: 'bar'}]}).should.throwError('Invalid value, Url and Label must be strings');
runHelper({nav: [{label: 'foo', url: 1}]}).should.throwError('Invalid value, Url and Label must be strings');
});
it('can render empty nav', function () {
var navigation = {nav:[]},
rendered = helpers.navigation.call(navigation);
should.exist(rendered);
rendered.string.should.be.equal('');
});
it('can render one item', function () {
var singleItem = {label: 'Foo', url: '/foo'},
navigation = {nav: [singleItem]},
rendered = helpers.navigation.call(navigation);
should.exist(rendered);
rendered.string.should.containEql('li');
rendered.string.should.containEql('nav-foo');
rendered.string.should.containEql('href="/foo"');
});
it('can render multiple items', function () {
var firstItem = {label: 'Foo', url: '/foo'},
secondItem = {label: 'Bar Baz Qux', url: '/qux'},
navigation = {nav: [firstItem, secondItem]},
rendered = helpers.navigation.call(navigation);
should.exist(rendered);
rendered.string.should.containEql('nav-foo');
rendered.string.should.containEql('nav-bar-baz-qux');
rendered.string.should.containEql('href="/foo"');
rendered.string.should.containEql('href="/qux"');
});
it('can annotate the current url', function () {
var firstItem = {label: 'Foo', url: '/foo'},
secondItem = {label: 'Bar', url: '/qux'},
navigation = {
relativeUrl: '/foo',
nav: [firstItem, secondItem]
},
rendered = helpers.navigation.call(navigation);
should.exist(rendered);
rendered.string.should.containEql('nav-foo');
rendered.string.should.containEql('nav-current');
rendered.string.should.containEql('nav-foo nav-current');
rendered.string.should.containEql('nav-bar "');
});
});