mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-06 22:40:14 -05:00
Merge pull request #4919 from ErisDS/pr/4852
Update to PR 4852 ({{navigation}} helper)
This commit is contained in:
commit
8b4979c7d1
6 changed files with 226 additions and 40 deletions
|
@ -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,
|
||||
navigation: 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,
|
||||
navigation: 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
58
core/server/helpers/navigation.js
Normal file
58
core/server/helpers/navigation.js
Normal 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 navigation,
|
||||
context,
|
||||
currentUrl = this.relativeUrl;
|
||||
|
||||
if (!_.isObject(this.navigation) || _.isFunction(this.navigation)) {
|
||||
return errors.logAndThrowError('navigation data is not an object or is a function');
|
||||
}
|
||||
|
||||
if (this.navigation.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.navigation.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.navigation.length === 0) {
|
||||
return new hbs.SafeString('');
|
||||
}
|
||||
|
||||
navigation = this.navigation.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({}, {navigation: navigation});
|
||||
|
||||
return template.execute('navigation', context);
|
||||
};
|
||||
|
||||
module.exports = navigation;
|
|
@ -1,7 +0,0 @@
|
|||
<nav id="site-navigation" role="navigation">
|
||||
<ul>
|
||||
{{#links}}
|
||||
<li class="{{#active}}current-menu-item{{/active}}"><a title="{{{title}}}" href="{{url}}">{{{title}}}</a></li>
|
||||
{{/links}}
|
||||
</ul>
|
||||
</nav>
|
5
core/server/helpers/tpl/navigation.hbs
Normal file
5
core/server/helpers/tpl/navigation.hbs
Normal file
|
@ -0,0 +1,5 @@
|
|||
<ul class="nav">
|
||||
{{#foreach navigation}}
|
||||
<li class="nav-{{slug}}{{#if current}} nav-current{{/if}}" role="presentation"><a href="{{url absolute="true"}}">{{label}}</a></li>
|
||||
{{/foreach}}
|
||||
</ul>
|
88
core/test/unit/server_helpers/navigation_spec.js
Normal file
88
core/test/unit/server_helpers/navigation_spec.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*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({navigation: [{label: 1, url: 'bar'}]}).should.throwError('Invalid value, Url and Label must be strings');
|
||||
runHelper({navigation: [{label: 'foo', url: 1}]}).should.throwError('Invalid value, Url and Label must be strings');
|
||||
});
|
||||
|
||||
it('can render empty nav', function () {
|
||||
var navigation = {navigation:[]},
|
||||
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 = {navigation: [singleItem]},
|
||||
rendered = helpers.navigation.call(navigation),
|
||||
testUrl = 'href="' + utils.config.url + '/foo"';
|
||||
|
||||
should.exist(rendered);
|
||||
rendered.string.should.containEql('li');
|
||||
rendered.string.should.containEql('nav-foo');
|
||||
rendered.string.should.containEql(testUrl);
|
||||
});
|
||||
|
||||
it('can render multiple items', function () {
|
||||
var firstItem = {label: 'Foo', url: '/foo'},
|
||||
secondItem = {label: 'Bar Baz Qux', url: '/qux'},
|
||||
navigation = {navigation: [firstItem, secondItem]},
|
||||
rendered = helpers.navigation.call(navigation),
|
||||
testUrl = 'href="' + utils.config.url + '/foo"',
|
||||
testUrl2 = 'href="' + utils.config.url + '/qux"';
|
||||
|
||||
should.exist(rendered);
|
||||
rendered.string.should.containEql('nav-foo');
|
||||
rendered.string.should.containEql('nav-bar-baz-qux');
|
||||
rendered.string.should.containEql(testUrl);
|
||||
rendered.string.should.containEql(testUrl2);
|
||||
});
|
||||
|
||||
it('can annotate the current url', function () {
|
||||
var firstItem = {label: 'Foo', url: '/foo'},
|
||||
secondItem = {label: 'Bar', url: '/qux'},
|
||||
navigation = {
|
||||
relativeUrl: '/foo',
|
||||
navigation: [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"');
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue