/**
* @module bauplan%helpers
* @description ## Helpers for Handlebars
* Registers helpers provided by [Larynx](http://larynx.solidgoldpig.com), setting locale and languages
* Registers additional helpers
*
* ### Additional helpers
* - {@link template:asset}
* - {@link template:price}
* - {@link template:console}
* - {@link template:compile}
* - {@link template:link}
* - {@link template:if}
* - {@link template:all}
* - {@link template:or}
*
* ### Client-side
* - {@link template:routeurl}
* - {@link template:authrouteurl}
* - {@link template:layout}
*/
(function (moduleFactory) {
if (typeof exports === "object") {
module.exports = moduleFactory(
require("lodash"),
require("bauplan"),
require("handlebars"),
require("larynx")
);
} else if (typeof define === "function" && define.amd) {
define([
"lodash",
"bauplan",
"handlebars",
"larynx",
"jquery",
"thorax",
"router"
], moduleFactory);
}
}(function (
_,
Bauplan,
Handlebars,
Larynx,
jQuery,
Thorax,
Router
) {
// Set up Larynx helpers
Larynx.registerHelpers(Handlebars);
Larynx.setLanguages(Bauplan.I18N.langs);
// Convenience filter for phrases
Larynx.Phrase.setFilter(function (str) {
return str.replace(/##(.*?)##/g, "<span>$1</span>");
});
// Register additional helpers
/**
* @template asset
* @block helper
* @description Outputs an asset path using an i18n key
*
* Given a i18n key/value pair: "logo.src=/path/to/logo.png"
*
* <img src="{{asset "logo.src"}}"> -> '<img src="/path/to/logo.png">'
*
* asset is currently just a wrapper for Handlebars.helpers.phrase
*/
Handlebars.registerHelper("asset", function () {
return Handlebars.helpers.phrase.apply({}, arguments);
});
/**
* @template currentTime
* @block helper
* @description Outputs the current time
*
* {{currentTime}}
*/
Handlebars.registerHelper("currentTime", function (block) {
return Date.now();
});
/**
* @template price
* @block helper
* @param {number} 0 Amount
* @description Outputs an amount in 1/100th of monetary unit as monetary unit
*
* {{price 2999}} -> "29.99"
*
* {{price 3000}} -> "30"
*
* Calls Handlebars.helpers.number, so accepts the same arguments and has the same defaults, eg.
*
* {{price 3000 format="0.00"}} -> "30.00"
*/
Handlebars.registerHelper("price", function (block) {
var args = Array.prototype.slice.call(arguments);
var price = args[0];
if (price) {
args[0] = price/100;
}
return Handlebars.helpers.number.apply({}, args);
});
/**
* @template console
* @block helper
* @param {*} 0 Item to dump
* @description Dumps output using console.log
*
* {{console foo}}
*/
Handlebars.registerHelper("console", function (block) {
console.log(arguments[0], Date.now());
});
/**
* @template compile
* @block helper
* @param {string} 0 String to compile
* @param {string} [context] Pass a different context - (the value must be a property key of the current context)
* @description Compile a string as a template
*
* Given a context where {bar: "baz", wham: {bar: "whizz"}}
*
* {{compile "foo {{bar}}"}} -> "foo bar"
*
* {{{compile "foo {{bar}}" context="wham"}}} -> "foo whizz"
*
* {{{compile "foo {{bar}}" context="nosuch"}}} -> "foo "
*/
Handlebars.registerHelper("compile", function (templatestr) {
var template = Handlebars.compile(templatestr);
var hash = arguments[1].hash;
var context = this;
if (hash.context) {
context = context[hash.context] || {};
}
return template(context);
});
/**
* @template set
* @block helper
* @param {string} 0 Property to set
* @param {*} 1 Value to be set
* @description Sets a value within the current context
* @example {{set foo "bar"}}
*/
Handlebars.registerHelper("set", function (name, value) {
//console.log(arguments[0], (new Date()).getTime());
var setReturn = "";
if (typeof name === "string") {
this[name] = value;
} else {
var options = name;
var hash = options.hash;
if (options.fn) {
var context = _.extend({}, this, hash);
setReturn = options.fn(context);
} else {
for (var prop in hash) {
this[prop] = hash[prop];
}
}
}
return setReturn;
});
/**
* @template link
* @block helper
* @param {string} 0 String to use for URL
* @param {string} [target] Window target for URL - defaults to "external"
* @param {string} [content] Content of the link - supercedes any yielded value
* @description Generates links using urls or localisation keys, prefixing with domain if necessary.
*
* Accepts protocol-less URLs and email addresses
*
* Given:
*
* - a i18n key/value pair: "foo.href=/foo"
* - the current hostname: "http://domain"
*
* {{#link "/foo"}}Bar{{/link}}
* -> '<a href="http://domain/foo" target="external">Bar</a>'
*
* Specify a different target
*
* {{#link "/foo" target="newtarget"}}Bar{{/link}}
* -> '<a href="http://domain/foo" target="newtarget">Bar</a>'
*
* Specify no target
*
* {{#link "/foo" target=""}}Bar{{/link}}
* -> '<a href="http://domain/foo">Bar</a>'
*
* Pass content explicitly
*
* {{link "/foo" content="Bar"}}
* -> '<a href="http://domain/foo" target="external">Bar</a>'
*
* Use a i18n key
*
* {{#link "foo.href"}}Bar{{/link}}
* -> '<a href="http://domain/foo" target="external">Bar</a>'
*
* URLs with a protocol
* {{#link "https://externaldomain/foo"}}Bar{{/link}}
* -> '<a href="https://externaldomain/foo" target="external">Bar</a>'
*
* Protocol-less URLs
*
* {{#link "//anotherdomain/foo"}}Bar{{/link}}
* -> '<a href="//anotherdomain/foo" target="external">Bar</a>'
*
* But if context contains {isMail: true}
*
* {{#link "//anotherdomain/foo"}}Bar{{/link}}
* -> '<a href="https://anotherdomain/foo" target="external">Bar</a>'
*
* Email addresses
*
* {{#link "foo@domain"}}Bar{{/link}}
* -> '<a href="mailto:/foo@domain">Bar</a>'
*/
var hostname = document.location.origin; //"//" + document.location.host;
Handlebars.registerHelper("link", function () {
var args = Array.prototype.slice.call(arguments);
var options = args.pop();
var url = args[0];
if (url) {
var urlphrase = Larynx.Phrase.get(url, {hostname: hostname});
if (urlphrase) {
url = urlphrase.toString();
}
} else {
url = "/";
}
if (url === "/") {
url = hostname;
} else if (url.match(/^\/[^\/]/)) {
url = hostname + url;
}
var params = options.hash || {};
var content = params.content || (options.fn && options.fn()) || url.replace(/.*\/\//, "");
if (params.target === undefined && url.match(/\/\//)) {
params.target = "external";
}
if (this.isMail) {
if (url.indexOf("//") === 0) {
url = "https:" + url;
}
}
if (url.match(/@/) && url.indexOf("mailto:") !== 0) {
url = "mailto:" + url;
}
return new Handlebars.SafeString('<a href="' + url + '"' + (params.target ? ' target="' + params.target + '"' : '') + '>' + content + '</a>');
});
/**
* @template if
* @block helper
* @extends Handlebars.helpers
* @override
* @param {*} 0 Item to compare
* @param {string} [1] Operator
* @param {*} [2] Item to be compared to
* @param {...*} [3...] See individual operators to see how further param works
* @description Overrides Handlebars.helpers.if
*
* ### No operator
* Truthiness - standard Handlebars if helper behaviour
*
* {{#if "foo"}}Yes{{/if}} -> "Yes"
*
* {{#if ""}}Yes{{else}}No{{/if}} -> "No"
*
* ### ==
* ‘Truthy’ equality
*
* {{#if "1" "==" 1}}Yes{{/if}} -> "Yes"
*
* {{#if "1" "==" "2"}}Yes{{else}}No{{/if}} -> "No"
*
* ### ===
* Strict equality
*
* {{#if "1" "===" "1"}}Yes{{/if}} -> "Yes"
*
* {{#if "1" "===" 1}}Yes{{else}}No{{/if}} -> "No"
*
* ### !=
* ‘Truthy’ inequality
*
* {{#if "1" "!=" "2"}}Yes{{/if}} -> "Yes"
*
* {{#if "1" "!=" 1}}Yes{{else}}No{{/if}} -> "No"
*
* ### !==
* Strict inequality
*
* {{#if "1" "!==" 1}}Yes{{/if}} -> "Yes"
*
* {{#if "1" "!==" "1"}}Yes{{else}}No{{/if}} -> "No"
*
* ### <
* Less than
*
* {{#if 1 "<" 2}}Yes{{/if}} -> "Yes"
*
* ### >
* More than
*
* {{#if 2 ">" 1}}Yes{{/if}} -> "Yes"
*
* ### <
* Less than or equal
*
* {{#if 1 "<=" 1}}Yes{{/if}} -> "Yes"
*
* ### >
* More than or equal
*
* {{#if 1 ">=" 1}}Yes{{/if}} -> "Yes"
*
* ### &&
* And operator
*
* {{#if "foo" "&&" "bar"}}Yes{{/if}} -> "Yes"
*
* {{#if "foo" "&&" "bar" "&&" "baz"}}Yes{{/if}} -> "Yes"
*
* {{#if "foo" "&&" "bar" "&&" ""}}Yes{{else}}No{{/if}} -> "No"
*
* NB. You cannot mix different operators
* See also {@link template:all}
*
* ### ||
* Or operator
*
* {{#if "" "||" "bar"}}Yes{{/if}} -> "Yes"
*
* {{#if "foo" "||" "" "||" "baz"}}Yes{{/if}} -> "Yes"
*
* {{#if "" "||" 0 "||" ""}}Yes{{else}}No{{/if}} -> "No"
*
* NB. You cannot mix different operators
* See also {@link template:or}
*
* ### is
* Lodash is* methods
*
* {{#if "foo" "is" "String"}}Yes{{/if}} -> "Yes"
*
* Method has first letter uppercased automatically
*
* {{#if "foo" "is" "string"}}Yes{{/if}} -> "Yes"
*
* ### constructor
*
* {{#if "foo" "constructor" "String"}}Yes{{/if}} -> "Yes"
*
* {{#if "foo" "constructor" "string"}}Yes{{else}}No{{/if}} -> "No"
*
* ### typeof
*
* {{#if "foo" "typeof" "string"}}Yes{{/if}} -> "Yes"
*
* {{#if "foo" "typeof" "String"}}Yes{{else}}No{{/if}} -> "No"
*
* ### has
*
* {{#if "foo" "has" "oo"}}Yes{{/if}} -> "Yes"
*
* Also aliased to <code>contains</code>
*
* {{#if "foo" "contains" "oo"}}Yes{{/if}} -> "Yes"
*
* See also {@link template:has}
*
* ### matches
*
* {{#if "foo" "matches" "fo{2,}"}}Yes{{/if}}
*
* Also aliased to <code>match</code>
*
* {{#if "foo" "match" "fo{2,}"}}Yes{{/if}}
*
* See also {@link template:match}
*
* ### in
*
* {{#if "foo" "in" "foo" "bar" "baz"}}Yes{{/if}} -> "Yes"
*
* {{#if "fo" "in" "foo" "bar" "baz"}}Yes{{else}}No{{/if}} -> "No"
*
* ### matchesin
*
* {{#if "foo" "matchesin" "fo" "bar" "daz"}}Yes{{/if}} -> "Yes"
*
* {{#if "foo" "matchesin" "fo\b" "bar" "daz"}}Yes{{else}}No{{/if}} -> "No"
*
* Also aliased to <code>matchin</code>
*
* {{#if "foo" "matchin" "fo" "bar" "daz"}}Yes{{/if}} -> "Yes"
*
* ### matchesall
*
* {{#if "foo" "matchesall" "f.{2}" "o+"}}Yes{{/if}} -> "Yes"
*
* {{#if "foo" "matchesin" "f.{2}" "\bo+"}}Yes{{else}}No{{/if}} -> "No"
*
* Also aliased to <code>matchin</code>
*
* {{#if "foo" "matchall" "f.{2}" "o+"}}Yes{{/if}} -> "Yes"
*
*/
/*
{{!--
Not implemented - but maybe a good idea
{{#has x $has}}foo{{/has}}
{{#match x $match}}foo{{/match}}
{{#in x $match1 $match2}}foo{{/in}} - contained?
{{#matchin x $match1 $match2}}foo{{/matchin}}
{{#matchin x $match1 $match2}}foo{{/matchin}}
{{#fallback valueToUse}}Fallback value{{/fallback}}
{{#if x "defined"}}
{{exists x y z}}
{{defined x y z}} -> print first value that is defined
no need for a defaultified version - just make sure the last one is an actual value
- but, what if we want to use it as a key for phrase?
- well, don't do that do {{#if x "||" y "||" z}}{{exists x y z}}{{else}}{{phrase default}}{{/if}} - hmmmm
- use “standard” @@foo@@ syntax? @foo@ [@foo@]
{{first x y z}} -> print first value to exist
{{last x y z}} -> prints first value to exist or last if none do
{{every x y z}} -> prints all values that exist
NB. "0" will be skipped
--}}
*/
function rightify(r, rs) {
if (typeof r === "object") {
rs = r;
}
return rs;
}
var operators = {
"==": function (l, r) { return l == r; },
"===": function (l, r) { return l === r; },
"!=": function (l, r) { return l != r; },
"!==": function (l, r) { return l !== r; },
"<": function (l, r) { return l < r; },
">": function (l, r) { return l > r; },
"<=": function (l, r) { return l <= r; },
">=": function (l, r) { return l >= r; },
"&&": function (l, r, rs) {
var truth = l && r;
if (truth && rs[1]) {
rs.shift();
var operator = rs.shift();
r = rs.shift();
if (operator === "&&") {
truth = operators["&&"](l, r, rs);
} else {
truth = false;
}
}
return truth;
},
"||": function (l, r, rs) {
var truth = l || r;
if (!truth && rs[1]) {
rs.shift();
var operator = rs.shift();
r = rs.shift();
if (operator === "||") {
truth = operators["||"](l, r, rs);
}
}
return truth;
},
"is": function (l, r) {
var method = "is" + r.replace(/^(.)/, function (x){
return x.toUpperCase();
});
if (!_[method]) {
return false;
}
return _[method](l);
},
"constructor": function (l, r) { return l && l.constructor && l.constructor.name == r; },
"typeof": function (l, r) { return typeof l == r; },
"has": function (l, r) { return l && l.indexOf(r) !== -1; },
"matches": function (l, r, rs) {
var matchreg = r.constructor.name === "RegExp" ? r : new RegExp(r, rs[1]);
return matchreg.exec(l) !== null;
},
"in": function (l, r, rs) {
rs = rightify(r, rs);
if (_.isArray(rs)) {
for (var ai = 0, rslen = rs.length; ai < rslen; ai++) {
if (l === rs[ai]) {
return true;
}
}
} else if (_.isObject(rs)) {
for (var oi in rs) {
if (l === rs[oi]) {
return true;
}
}
}
return false;
},
"matchesin": function (l, r, rs) {
rs = rightify(r, rs);
for (var ai = 0, rslen = rs.length; ai < rslen; ai++) {
if (operators.matches(l, rs[ai], [])) {
return true;
}
}
return false;
},
"matchesall": function (l, r, rs) {
rs = rightify(r, rs);
for (var ai = 0, rslen = rs.length; ai < rslen; ai++) {
if (!operators.matches(l, rs[ai], [])) {
return false;
}
}
return true;
}
};
operators.contains = operators.has;
operators.match = operators.matches;
operators.matchin = operators.matchesin;
operators.matchall = operators.matchesall;
Handlebars.registerHelper("if", function () {
var args = Array.prototype.slice.call(arguments),
options = args.pop(),
lvalue = args.shift(),
operator = args.shift(),
rvalues = _.extend([], args),
rvalue = args.shift(),
result = lvalue;
if (operator !== undefined) {
if (arguments.length < 3) {
throw new Error("Handlerbars Helper 'if' needs 2 parameters");
}
if (options === undefined) {
options = rvalue;
rvalue = operator;
operator = "==";
}
if (!operators[operator]) {
throw new Error("Handlerbars Helper 'if' doesn't know the operator " + operator);
}
result = operators[operator](lvalue, rvalue, rvalues);
}
var context = this; //_.extend({}, this, options.hash);
//console.log("context", context);
if (result) {
return options.fn(context);
} else {
return options.inverse(context);
}
});
/**
* @template all
* @block helper
* @description Outputs the yielded content if all conditions are met
*
* {{#all "a" "b" 3}}This will be output{{/all}} -> "This will be output"
*
* {{#all "a" "" 3}}This will not be output{{/all}} -> ""
*
* See also {@link template:if}~&&
*/
Handlebars.registerHelper("all", function () {
var args = Array.prototype.slice.call(arguments);
var options = args.pop();
var result = true;
for (var i = 0, argslen = args.length; i < argslen; i++) {
if (!args[i]) {
result = false;
break;
}
}
if (result) {
return options.fn(this);
} else {
return options.inverse(this);
}
});
/**
* @template or
* @block helper
* @description Outputs the yielded content if any of the conditions are met
*
* {{#or "" "b" ""}}This will be output{{/or}} -> "This will be output"
*
* {{#or "" 0 ""}}This will not be output{{/or}} -> ""
*
* See also {@link template:if}~||
*/
Handlebars.registerHelper("or", function () {
var args = Array.prototype.slice.call(arguments);
var options = args.pop();
var result = false;
for (var i = 0, argslen = args.length; i < argslen; i++) {
if (args[i]) {
result = true;
break;
}
}
if (result) {
return options.fn(this);
} else {
return options.inverse(this);
}
});
/*
Client-side helpers
*/
if (Router) {
/**
* @template routeurl
* @block helper
* @param {string} 0 Route name
* @param {object} [params] Additional params to be passed to the underlying router method
* @description Outputs a URL given the name of a route
*
* {{routeurl "route.key"}}
*
* See {@link module:bauplan%router%base.reverse}
*/
Handlebars.registerHelper("routeurl", function () {
var route = arguments[0];
var params = {};
var options = arguments[1];
if (options.hash) {
params = _.extend((options.hash.params) || {}, options.hash);
delete options.hash.params;
}
var url = Router.reverse(route, params);
return url;
});
/**
* @template authrouteurl
* @block helper
* @param {string} 0 Route name
* @param {object} [params] Additional params to be passed to the underlying router method
* @description Outputs a URL given the name of a route that ensures that the user is logged in first
*
* {{authrouteurl "route.key"}}
*
* See {@link module:bauplan%router%base.reverseWithAuth}
*
* (Possibly somewhat redundant since protected URLs will always redirect to the auth'd URL any way)
*/
Handlebars.registerHelper("authrouteurl", function () {
var route = arguments[0];
var params = {};
var options = arguments[1];
if (options.hash) {
params = _.extend((options.hash.params) || {}, options.hash);
delete options.hash.params;
}
var url = Router.reverseWithAuth(route, params);
return url;
});
/**
* @template layout
* @block helper
* @param {string} view Name of view to load
* @param {string} [id] Id to override default Id
* @param {object} [options] Options to pass to view loaded by layout view
* @param {string} [tagName] Tag name to override layout view’s default tag
* @param {string} [role] Role to add to layout view element
* @description Inserts a Thorax.LayoutView and loads a view
*
* {{layout view="foo"}}
*
* {{layout id="bar" view=view options=viewOptions}}
*
* {{layout tagName="header" role="banner" view="site-header"}}
*/
var layoutCallback = 0;
Handlebars.registerHelper("layout", function () {
var name;
var options;
if (arguments.length > 1) {
name = arguments[0];
options = arguments[1];
} else {
options = arguments[0];
}
var lOptions = options.hash || {};
if (name) {
lOptions.name = name;
}
var layoutView = new Thorax.LayoutView(lOptions);
if (lOptions.view) {
var nestedView = Thorax.LayoutViews[lOptions.view] || Thorax.Views[lOptions.view];
var nested = new nestedView(lOptions.options);
layoutView.setView(nested);
}
var callbackCount = 0;
var placeholder = "layoutViewPlaceholder" + layoutCallback;
var selector = "#" + placeholder;
function appendLayout() {
// Nasty - we have to keep checking until the element exists within the DOM,
// unfortunately a limitation of Thorax
if (!jQuery(selector).length) {
callbackCount++;
var pause = callbackCount > 1000 ? 1000 : 1;
setTimeout(appendLayout, pause);
} else {
jQuery(selector).replaceWith(layoutView.el);
layoutView.$el.find("[layoutshim]").remove();
jQuery("[layoutshim]").remove();
}
}
setTimeout(appendLayout, 0);
layoutCallback++;
return new Handlebars.SafeString("<span id=\"" + placeholder + "\"></span>");
});
}
return Handlebars;
}));