2479 lines
83 KiB
JavaScript
2479 lines
83 KiB
JavaScript
/*! JsRender v0.9.88 (Beta): http://jsviews.com/#jsrender */
|
|
/*! **VERSION FOR WEB** (For NODE.JS see http://jsviews.com/download/jsrender-node.js) */
|
|
/*
|
|
* Best-of-breed templating in browser or on Node.js.
|
|
* Does not require jQuery, or HTML DOM
|
|
* Integrates with JsViews (http://jsviews.com/#jsviews)
|
|
*
|
|
* Copyright 2017, Boris Moore
|
|
* Released under the MIT License.
|
|
*/
|
|
|
|
//jshint -W018, -W041, -W120
|
|
|
|
(function(factory, global) {
|
|
// global var is the this object, which is window when running in the usual browser environment
|
|
var $ = global.jQuery;
|
|
|
|
if (typeof exports === "object") { // CommonJS e.g. Browserify
|
|
module.exports = $
|
|
? factory(global, $)
|
|
: function($) { // If no global jQuery, take optional jQuery passed as parameter: require('jsrender')(jQuery)
|
|
if ($ && !$.fn) {
|
|
throw "Provide jQuery or null";
|
|
}
|
|
return factory(global, $);
|
|
};
|
|
} else if (typeof define === "function" && define.amd) { // AMD script loader, e.g. RequireJS
|
|
define(function() {
|
|
return factory(global);
|
|
});
|
|
} else { // Browser using plain <script> tag
|
|
factory(global, false);
|
|
}
|
|
} (
|
|
|
|
// factory (for jsrender.js)
|
|
function(global, $) {
|
|
"use strict";
|
|
|
|
//========================== Top-level vars ==========================
|
|
|
|
// global var is the this object, which is window when running in the usual browser environment
|
|
var setGlobals = $ === false; // Only set globals if script block in browser (not AMD and not CommonJS)
|
|
|
|
$ = $ && $.fn ? $ : global.jQuery; // $ is jQuery passed in by CommonJS loader (Browserify), or global jQuery.
|
|
|
|
var versionNumber = "v0.9.88",
|
|
jsvStoreName, rTag, rTmplString, topView, $views, $expando,
|
|
_ocp = "_ocp", // Observable contextual parameter
|
|
|
|
//TODO tmplFnsCache = {},
|
|
$isFunction, $isArray, $templates, $converters, $helpers, $tags, $sub, $subSettings, $subSettingsAdvanced, $viewsSettings, delimOpenChar0, delimOpenChar1, delimCloseChar0, delimCloseChar1, linkChar, setting, baseOnError,
|
|
|
|
rPath = /^(!*?)(?:null|true|false|\d[\d.]*|([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g,
|
|
// not object helper view viewProperty pathTokens leafToken
|
|
|
|
rParams = /(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?=\s*[.^]|\s*$|[^([])|[)\]])([([]?))|(\s+)/g,
|
|
// lftPrn0 lftPrn bound path operator err eq path2 prn comma lftPrn2 apos quot rtPrn rtPrnDot prn2 space
|
|
// (left paren? followed by (path? followed by operator) or (path followed by left paren?)) or comma or apos or quot or right paren or space
|
|
|
|
isRenderCall,
|
|
rNewLine = /[ \t]*(\r\n|\n|\r)/g,
|
|
rUnescapeQuotes = /\\(['"])/g,
|
|
rEscapeQuotes = /['"\\]/g, // Escape quotes and \ character
|
|
rBuildHash = /(?:\x08|^)(onerror:)?(?:(~?)(([\w$_\.]+):)?([^\x08]+))\x08(,)?([^\x08]+)/gi,
|
|
rTestElseIf = /^if\s/,
|
|
rFirstElem = /<(\w+)[>\s]/,
|
|
rAttrEncode = /[\x00`><"'&=]/g, // Includes > encoding since rConvertMarkers in JsViews does not skip > characters in attribute strings
|
|
rIsHtml = /[\x00`><\"'&=]/,
|
|
rHasHandlers = /^on[A-Z]|^convert(Back)?$/,
|
|
rWrappedInViewMarker = /^\#\d+_`[\s\S]*\/\d+_`$/,
|
|
rHtmlEncode = rAttrEncode,
|
|
viewId = 0,
|
|
charEntities = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
"\x00": "�",
|
|
"'": "'",
|
|
'"': """,
|
|
"`": "`",
|
|
"=": "="
|
|
},
|
|
HTML = "html",
|
|
OBJECT = "object",
|
|
tmplAttr = "data-jsv-tmpl",
|
|
jsvTmpl = "jsvTmpl",
|
|
indexStr = "For #index in nested block use #getIndex().",
|
|
$render = {},
|
|
|
|
jsr = global.jsrender,
|
|
jsrToJq = jsr && $ && !$.render, // JsRender already loaded, without jQuery. but we will re-load it now to attach to jQuery
|
|
|
|
jsvStores = {
|
|
template: {
|
|
compile: compileTmpl
|
|
},
|
|
tag: {
|
|
compile: compileTag
|
|
},
|
|
viewModel: {
|
|
compile: compileViewModel
|
|
},
|
|
helper: {},
|
|
converter: {}
|
|
};
|
|
|
|
// views object ($.views if jQuery is loaded, jsrender.views if no jQuery, e.g. in Node.js)
|
|
$views = {
|
|
jsviews: versionNumber,
|
|
sub: {
|
|
// subscription, e.g. JsViews integration
|
|
View: View,
|
|
Err: JsViewsError,
|
|
tmplFn: tmplFn,
|
|
parse: parseParams,
|
|
extend: $extend,
|
|
extendCtx: extendCtx,
|
|
syntaxErr: syntaxError,
|
|
onStore: {
|
|
template: function(name, item) {
|
|
if (item === null) {
|
|
delete $render[name];
|
|
} else {
|
|
$render[name] = item;
|
|
}
|
|
}
|
|
},
|
|
addSetting: addSetting,
|
|
settings: {
|
|
allowCode: false
|
|
},
|
|
advSet: noop, // Update advanced settings
|
|
_ths: tagHandlersFromProps,
|
|
_gm: getMethod,
|
|
_tg: function() {}, // Constructor for tagDef
|
|
_cnvt: convertVal,
|
|
_tag: renderTag,
|
|
_er: error,
|
|
_err: onRenderError,
|
|
_html: htmlEncode,
|
|
_cp: retVal, // Get observable contextual parameters (or properties) ~foo=expr. In JsRender, simply returns val.
|
|
_sq: function(token) {
|
|
if (token === "constructor") {
|
|
syntaxError("");
|
|
}
|
|
return token;
|
|
}
|
|
},
|
|
settings: {
|
|
delimiters: $viewsDelimiters,
|
|
advanced: function(value) {
|
|
return value
|
|
? (
|
|
$extend($subSettingsAdvanced, value),
|
|
$sub.advSet(),
|
|
$viewsSettings
|
|
)
|
|
: $subSettingsAdvanced;
|
|
}
|
|
},
|
|
map: dataMap // If jsObservable loaded first, use that definition of dataMap
|
|
};
|
|
|
|
function getDerivedMethod(baseMethod, method) {
|
|
return function() {
|
|
var ret,
|
|
tag = this,
|
|
prevBase = tag.base;
|
|
|
|
tag.base = baseMethod; // Within method call, calling this.base will call the base method
|
|
ret = method.apply(tag, arguments); // Call the method
|
|
tag.base = prevBase; // Replace this.base to be the base method of the previous call, for chained calls
|
|
return ret;
|
|
};
|
|
}
|
|
|
|
function getMethod(baseMethod, method) {
|
|
// For derived methods (or handlers declared declaratively as in {{:foo onChange=~fooChanged}} replace by a derived method, to allow using this.base(...)
|
|
// or this.baseApply(arguments) to call the base implementation. (Equivalent to this._super(...) and this._superApply(arguments) in jQuery UI)
|
|
if ($isFunction(method)) {
|
|
method = getDerivedMethod(
|
|
!baseMethod
|
|
? noop // no base method implementation, so use noop as base method
|
|
: baseMethod._d
|
|
? baseMethod // baseMethod is a derived method, so use it
|
|
: getDerivedMethod(noop, baseMethod), // baseMethod is not derived so make its base method be the noop method
|
|
method
|
|
);
|
|
method._d = 1; // Add flag that this is a derived method
|
|
}
|
|
return method;
|
|
}
|
|
|
|
function tagHandlersFromProps(tag, tagCtx) {
|
|
for (var prop in tagCtx.props) {
|
|
if (rHasHandlers.test(prop) && !(tag[prop] && tag[prop].fix)) { // Don't override handlers with fix expando (used in datepicker and spinner)
|
|
tag[prop] = getMethod(tag.constructor.prototype[prop], tagCtx.props[prop]);
|
|
// Copy over the onFoo props, convert and convertBack from tagCtx.props to tag (overrides values in tagDef).
|
|
// Note: unsupported scenario: if handlers are dynamically added ^onFoo=expression this will work, but dynamically removing will not work.
|
|
}
|
|
}
|
|
}
|
|
|
|
function retVal(val) {
|
|
return val;
|
|
}
|
|
|
|
function noop() {
|
|
return "";
|
|
}
|
|
|
|
function dbgBreak(val) {
|
|
// Usage examples: {{dbg:...}}, {{:~dbg(...)}}, {{dbg .../}}, {^{for ... onAfterLink=~dbg}} etc.
|
|
try {
|
|
console.log("JsRender dbg breakpoint: " + val);
|
|
throw "dbg breakpoint"; // To break here, stop on caught exceptions.
|
|
}
|
|
catch (e) {}
|
|
return this.base ? this.baseApply(arguments) : val;
|
|
}
|
|
|
|
function JsViewsError(message) {
|
|
// Error exception type for JsViews/JsRender
|
|
// Override of $.views.sub.Error is possible
|
|
this.name = ($.link ? "JsViews" : "JsRender") + " Error";
|
|
this.message = message || this.name;
|
|
}
|
|
|
|
function $extend(target, source) {
|
|
if (target) {
|
|
for (var name in source) {
|
|
target[name] = source[name];
|
|
}
|
|
return target;
|
|
}
|
|
}
|
|
|
|
(JsViewsError.prototype = new Error()).constructor = JsViewsError;
|
|
|
|
//========================== Top-level functions ==========================
|
|
|
|
//===================
|
|
// views.delimiters
|
|
//===================
|
|
|
|
function $viewsDelimiters(openChars, closeChars, link) {
|
|
// Set the tag opening and closing delimiters and 'link' character. Default is "{{", "}}" and "^"
|
|
// openChars, closeChars: opening and closing strings, each with two characters
|
|
if (!openChars) {
|
|
return $subSettings.delimiters;
|
|
}
|
|
if ($isArray(openChars)) {
|
|
return $viewsDelimiters.apply($views, openChars);
|
|
}
|
|
|
|
$subSettings.delimiters = [openChars, closeChars, linkChar = link ? link.charAt(0) : linkChar];
|
|
|
|
delimOpenChar0 = openChars.charAt(0); // Escape the characters - since they could be regex special characters
|
|
delimOpenChar1 = openChars.charAt(1);
|
|
delimCloseChar0 = closeChars.charAt(0);
|
|
delimCloseChar1 = closeChars.charAt(1);
|
|
openChars = "\\" + delimOpenChar0 + "(\\" + linkChar + ")?\\" + delimOpenChar1; // Default is "{^{"
|
|
closeChars = "\\" + delimCloseChar0 + "\\" + delimCloseChar1; // Default is "}}"
|
|
// Build regex with new delimiters
|
|
// [tag (followed by / space or }) or cvtr+colon or html or code] followed by space+params then convertBack?
|
|
rTag = "(?:(\\w+(?=[\\/\\s\\" + delimCloseChar0 + "]))|(\\w+)?(:)|(>)|(\\*))\\s*((?:[^\\"
|
|
+ delimCloseChar0 + "]|\\" + delimCloseChar0 + "(?!\\" + delimCloseChar1 + "))*?)";
|
|
|
|
// Make rTag available to JsViews (or other components) for parsing binding expressions
|
|
$sub.rTag = "(?:" + rTag + ")";
|
|
// { ^? { tag+params slash? or closingTag or comment
|
|
rTag = new RegExp("(?:" + openChars + rTag + "(\\/)?|\\" + delimOpenChar0 + "(\\" + linkChar + ")?\\" + delimOpenChar1 + "(?:(?:\\/(\\w+))\\s*|!--[\\s\\S]*?--))" + closeChars, "g");
|
|
|
|
// Default: bind tagName cvt cln html code params slash bind2 closeBlk comment
|
|
// /(?:{(\^)?{(?:(\w+(?=[\/\s}]))|(\w+)?(:)|(>)|(\*))\s*((?:[^}]|}(?!}))*?)(\/)?|{(\^)?{(?:(?:\/(\w+))\s*|!--[\s\S]*?--))}}
|
|
|
|
$sub.rTmpl = new RegExp("^\\s|\\s$|<.*>|([^\\\\]|^)[{}]|" + openChars + ".*" + closeChars);
|
|
// $sub.rTmpl looks for initial or final white space, html tags or { or } char not preceded by \\, or JsRender tags {{xxx}}.
|
|
// Each of these strings are considered NOT to be jQuery selectors
|
|
return $viewsSettings;
|
|
}
|
|
|
|
//=========
|
|
// View.get
|
|
//=========
|
|
|
|
function getView(inner, type) { //view.get(inner, type)
|
|
if (!type && inner !== true) {
|
|
// view.get(type)
|
|
type = inner;
|
|
inner = undefined;
|
|
}
|
|
|
|
var views, i, l, found,
|
|
view = this,
|
|
root = !type || type === "root";
|
|
// If type is undefined, returns root view (view under top view).
|
|
|
|
if (inner) {
|
|
// Go through views - this one, and all nested ones, depth-first - and return first one with given type.
|
|
// If type is undefined, i.e. view.get(true), return first child view.
|
|
found = type && view.type === type && view;
|
|
if (!found) {
|
|
views = view.views;
|
|
if (view._.useKey) {
|
|
for (i in views) {
|
|
if (found = type ? views[i].get(inner, type) : views[i]) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
for (i = 0, l = views.length; !found && i < l; i++) {
|
|
found = type ? views[i].get(inner, type) : views[i];
|
|
}
|
|
}
|
|
}
|
|
} else if (root) {
|
|
// Find root view. (view whose parent is top view)
|
|
found = view.root;
|
|
} else {
|
|
while (view && !found) {
|
|
// Go through views - this one, and all parent ones - and return first one with given type.
|
|
found = view.type === type ? view : undefined;
|
|
view = view.parent;
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
function getNestedIndex() {
|
|
var view = this.get("item");
|
|
return view ? view.index : undefined;
|
|
}
|
|
|
|
getNestedIndex.depends = function() {
|
|
return [this.get("item"), "index"];
|
|
};
|
|
|
|
function getIndex() {
|
|
return this.index;
|
|
}
|
|
|
|
getIndex.depends = "index";
|
|
|
|
//==========
|
|
// View.hlp
|
|
//==========
|
|
|
|
function contextParameter(key, value, isContextCb) {
|
|
// Helper method called as view.ctxPrm(key) for helpers or template parameters ~foo - from compiled template or from context callback
|
|
var wrapped, deps, res, obsCtxPrm,
|
|
storeView = this,
|
|
isUpdate = !isRenderCall && value !== undefined,
|
|
store = storeView.ctx;
|
|
|
|
if (key in store || key in (store = $helpers)) {
|
|
res = store && store[key];
|
|
if (key === "tag" || key === "root" || key === "parentTags" || storeView._.it === key ) {
|
|
return res;
|
|
}
|
|
} else {
|
|
store = undefined;
|
|
}
|
|
if (!res || !$isFunction(res) && storeView.linked || storeView.tagCtx) { // Data-linked view, or tag instance
|
|
if (!res || !res._cxp) {
|
|
// Not a contextual parameter
|
|
if (store !== $helpers) {
|
|
// Set storeView to tag (if this is a tag.ctxPrm() call) or to root view (view under top view)
|
|
storeView = storeView.ctx && storeView.ctx.tag || storeView.root;
|
|
store = storeView._ocps;
|
|
res = store && store[key] || res;
|
|
}
|
|
if (!(res && res._cxp) && (isContextCb || isUpdate)) {
|
|
res = $sub._crcp(key, res, storeView, store); // Create observable contextual parameter
|
|
}
|
|
}
|
|
if (obsCtxPrm = res && res._cxp) {
|
|
if (isUpdate) {
|
|
return $sub._ucp(key, value, storeView, obsCtxPrm); // Update observable contextual parameter
|
|
}
|
|
if (isContextCb) { // If this helper resource is an observable contextual parameter
|
|
// In a context callback for a contextual param, return the [view, dependencies...] array - needed for observe call
|
|
deps = res[1] ? $sub._ceo(res[1].deps) : [_ocp]; // fn deps (with any exprObs cloned using $sub._ceo)
|
|
deps.unshift(res[0]); // view
|
|
deps._cxp = obsCtxPrm;
|
|
return deps;
|
|
}
|
|
res = res[1] // linkFn for compiled expression
|
|
? obsCtxPrm.tag && obsCtxPrm.tag.cvtArgs
|
|
? obsCtxPrm.tag.cvtArgs(undefined, 1, obsCtxPrm.tagElse)[obsCtxPrm.ind] // = tag.bndArgs() - for tag contextual parameter
|
|
: res[1](res[0].data, res[0], $sub) // = fn(data, view, $sub) for compiled binding expression
|
|
: res[0]._ocp; // Observable contextual parameter (uninitialized, or initialized as static expression, so no path dependencies)
|
|
}
|
|
}
|
|
if (res && $isFunction(res)) {
|
|
// If a helper is of type function, and not already wrapped, we will wrap it, so if called with no this pointer it will be called with the
|
|
// view as 'this' context. If the helper ~foo() was in a data-link expression, the view will have a 'temporary' linkCtx property too.
|
|
// Note that helper functions on deeper paths will have specific this pointers, from the preceding path.
|
|
// For example, ~util.foo() will have the ~util object as 'this' pointer
|
|
wrapped = function() {
|
|
return res.apply((!this || this === global) ? storeView : this, arguments);
|
|
};
|
|
$extend(wrapped, res); // Attach same expandos (if any) to the wrapped function
|
|
wrapped._vw = storeView;
|
|
}
|
|
return wrapped || res;
|
|
}
|
|
|
|
function getTemplate(tmpl) {
|
|
return tmpl && (tmpl.fn
|
|
? tmpl
|
|
: this.getRsc("templates", tmpl) || $templates(tmpl)); // not yet compiled
|
|
}
|
|
|
|
//==============
|
|
// views._cnvt
|
|
//==============
|
|
|
|
function convertVal(converter, view, tagCtx, onError) {
|
|
// self is template object or linkCtx object
|
|
var tag, value,
|
|
// If tagCtx is an integer, then it is the key for the compiled function to return the boundTag tagCtx
|
|
boundTag = typeof tagCtx === "number" && view.tmpl.bnds[tagCtx-1],
|
|
linkCtx = view.linkCtx; // For data-link="{cvt:...}"...
|
|
|
|
if (onError === undefined && boundTag && boundTag._lr) { // lateRender
|
|
onError = "";
|
|
}
|
|
if (onError !== undefined) {
|
|
tagCtx = onError = {props: {}, args: [onError]};
|
|
} else if (boundTag) {
|
|
tagCtx = boundTag(view.data, view, $sub);
|
|
}
|
|
boundTag = boundTag._bd && boundTag;
|
|
value = tagCtx.args[0];
|
|
if (converter || boundTag) {
|
|
tag = linkCtx && linkCtx.tag;
|
|
tagCtx.view = view;
|
|
if (!tag) {
|
|
tag = $extend(new $sub._tg(), {
|
|
_: {
|
|
inline: !linkCtx,
|
|
bnd: boundTag,
|
|
unlinked: true
|
|
},
|
|
tagName: ":",
|
|
cvt: converter,
|
|
flow: true,
|
|
tagCtx: tagCtx
|
|
});
|
|
if (linkCtx) {
|
|
linkCtx.tag = tag;
|
|
tag.linkCtx = linkCtx;
|
|
}
|
|
tagCtx.ctx = extendCtx(tagCtx.ctx, (linkCtx ? linkCtx.view : view).ctx);
|
|
tagHandlersFromProps(tag, tagCtx);
|
|
}
|
|
tag._er = onError && value;
|
|
tag.ctx = tagCtx.ctx || tag.ctx || {};
|
|
tagCtx.ctx = undefined;
|
|
|
|
value = tag.cvtArgs(converter !== "true" && converter)[0]; // If there is a convertBack but no convert, converter will be "true"
|
|
}
|
|
|
|
// Call onRender (used by JsViews if present, to add binding annotations around rendered content)
|
|
value = boundTag && view._.onRender
|
|
? view._.onRender(value, view, tag)
|
|
: value;
|
|
return value != undefined ? value : "";
|
|
}
|
|
|
|
function convertArgs(converter, bound, tagElse) { // tag.cvtArgs()
|
|
var l, key, boundArgs, args, bindTo, tag,
|
|
tagCtx = this;
|
|
|
|
if (tagCtx.tagName) {
|
|
tag = tagCtx;
|
|
tagCtx = tag.tagCtxs ? tag.tagCtxs[tagElse || 0] : tag.tagCtx;
|
|
} else {
|
|
tag = tagCtx.tag;
|
|
tagElse = tagCtx.index;
|
|
}
|
|
|
|
bindTo = tag.bindTo;
|
|
args = tagCtx.args;
|
|
|
|
converter = converter || tag.convert;
|
|
if ("" + converter === converter) {
|
|
converter = tagCtx.view.getRsc("converters", converter) || error("Unknown converter: '" + converter + "'");
|
|
}
|
|
|
|
if (!args.length && tag.argDefault !== false && !tagCtx.index) {
|
|
args = [tagCtx.view.data]; // Missing first arg defaults to the current data context
|
|
} else if (converter && !bound) { // If there is a converter, use a copy of the tagCtx.args array for rendering, and replace the args[0] in
|
|
args = args.slice(); // the copied array with the converted value. But we do not modify the value of tag.tagCtx.args[0] (the original args array)
|
|
}
|
|
|
|
if (bindTo) { // Get the values of the boundArgs
|
|
boundArgs = [];
|
|
l = bindTo.length;
|
|
while (l--) {
|
|
key = bindTo[l];
|
|
boundArgs.unshift(argOrProp(tagCtx, key));
|
|
}
|
|
if (bound) {
|
|
args = boundArgs; // Call to convertBoundArgs() - returns the boundArgs
|
|
}
|
|
}
|
|
|
|
if (converter) {
|
|
bindTo = bindTo || [0];
|
|
converter = converter.apply(tag, boundArgs || args);
|
|
l = bindTo.length;
|
|
converter = l < 2 ? [converter] : converter || [];
|
|
if (bound) { // Call to bndArgs convertBoundArgs() - so apply converter to all boundArgs
|
|
args = converter; // The array of values returned from the converter
|
|
} else { // Call to cvtArgs()
|
|
while (l--) {
|
|
key = bindTo[l];
|
|
if (+key === key) {
|
|
args[key] = converter ? converter[l] : undefined;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function argOrProp(context, key) {
|
|
context = context[+key === key ? "args" : "props"];
|
|
return context && context[key];
|
|
}
|
|
|
|
function convertBoundArgs(tagElse) { // tag.bndArgs()
|
|
return this.cvtArgs(undefined, true, tagElse);
|
|
}
|
|
|
|
//=============
|
|
// views._tag
|
|
//=============
|
|
|
|
function getResource(resourceType, itemName) {
|
|
var res, store,
|
|
view = this;
|
|
while ((res === undefined) && view) {
|
|
store = view.tmpl && view.tmpl[resourceType];
|
|
res = store && store[itemName];
|
|
view = view.parent;
|
|
}
|
|
return res || $views[resourceType][itemName];
|
|
}
|
|
|
|
function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) {
|
|
parentView = parentView || topView;
|
|
var tag, tag_, tagDef, template, tags, attr, parentTag, l, m, n, itemRet, tagCtx, tagCtxCtx, ctxPrm, bindTo,
|
|
content, callInit, mapDef, thisMap, args, props, tagDataMap, contentCtx, key,
|
|
i = 0,
|
|
ret = "",
|
|
linkCtx = parentView.linkCtx || 0,
|
|
ctx = parentView.ctx,
|
|
parentTmpl = tmpl || parentView.tmpl,
|
|
// If tagCtxs is an integer, then it is the key for the compiled function to return the boundTag tagCtxs
|
|
boundTag = typeof tagCtxs === "number" && parentView.tmpl.bnds[tagCtxs-1];
|
|
|
|
if (tagName._is === "tag") {
|
|
tag = tagName;
|
|
tagName = tag.tagName;
|
|
tagCtxs = tag.tagCtxs;
|
|
template = tag.template;
|
|
} else {
|
|
tagDef = parentView.getRsc("tags", tagName) || error("Unknown tag: {{" + tagName + "}} ");
|
|
template = tagDef.template;
|
|
}
|
|
|
|
if (onError === undefined && boundTag && boundTag._lr) {
|
|
onError = "";
|
|
}
|
|
if (onError !== undefined) {
|
|
ret += onError;
|
|
tagCtxs = onError = [{props: {}, args: []}];
|
|
} else if (boundTag) {
|
|
tagCtxs = boundTag(parentView.data, parentView, $sub);
|
|
}
|
|
|
|
l = tagCtxs.length;
|
|
for (; i < l; i++) {
|
|
tagCtx = tagCtxs[i];
|
|
if (!linkCtx || !linkCtx.tag || i && !linkCtx.tag._.inline || tag._er) {
|
|
// Initialize tagCtx
|
|
// For block tags, tagCtx.tmpl is an integer > 0
|
|
if (content = parentTmpl.tmpls && tagCtx.tmpl) {
|
|
content = tagCtx.content = parentTmpl.tmpls[content - 1];
|
|
}
|
|
tagCtx.index = i;
|
|
tagCtx.tmpl = content; // Set the tmpl property to the content of the block tag
|
|
tagCtx.render = renderContent;
|
|
tagCtx.view = parentView;
|
|
tagCtx.ctx = extendCtx(tagCtx.ctx, ctx); // Clone and extend parentView.ctx
|
|
}
|
|
if (tmpl = tagCtx.props.tmpl) {
|
|
// If the tmpl property is overridden, set the value (when initializing, or, in case of binding: ^tmpl=..., when updating)
|
|
tagCtx.tmpl = parentView.getTmpl(tmpl);
|
|
}
|
|
|
|
if (!tag) {
|
|
// This will only be hit for initial tagCtx (not for {{else}}) - if the tag instance does not exist yet
|
|
// If the tag has not already been instantiated, we will create a new instance.
|
|
// ~tag will access the tag, even within the rendering of the template content of this tag.
|
|
// From child/descendant tags, can access using ~tag.parent, or ~parentTags.tagName
|
|
tag = new tagDef._ctr();
|
|
callInit = !!tag.init;
|
|
|
|
tag.parent = parentTag = ctx && ctx.tag;
|
|
tag.tagCtxs = tagCtxs;
|
|
tagDataMap = tag.dataMap;
|
|
|
|
if (linkCtx) {
|
|
tag._.inline = false;
|
|
linkCtx.tag = tag;
|
|
tag.linkCtx = linkCtx;
|
|
}
|
|
if (tag._.bnd = boundTag || linkCtx.fn) {
|
|
// Bound if {^{tag...}} or data-link="{tag...}"
|
|
tag._.arrVws = {};
|
|
} else if (tag.dataBoundOnly) {
|
|
error(tagName + " must be data-bound:\n{^{" + tagName + "}}");
|
|
}
|
|
//TODO better perf for childTags() - keep child tag.tags array, (and remove child, when disposed)
|
|
// tag.tags = [];
|
|
}
|
|
bindTo = tag.bindTo || [0];
|
|
tagCtxs = tag.tagCtxs;
|
|
tagDataMap = tag.dataMap;
|
|
|
|
tagCtx.tag = tag;
|
|
if (tagDataMap && tagCtxs) {
|
|
tagCtx.map = tagCtxs[i].map; // Copy over the compiled map instance from the previous tagCtxs to the refreshed ones
|
|
}
|
|
if (!tag.flow) {
|
|
tagCtxCtx = tagCtx.ctx = tagCtx.ctx || {};
|
|
|
|
// tags hash: tag.ctx.tags, merged with parentView.ctx.tags,
|
|
tags = tag.parents = tagCtxCtx.parentTags = ctx && extendCtx(tagCtxCtx.parentTags, ctx.parentTags) || {};
|
|
if (parentTag) {
|
|
tags[parentTag.tagName] = parentTag;
|
|
//TODO better perf for childTags: parentTag.tags.push(tag);
|
|
}
|
|
tags[tag.tagName] = tagCtxCtx.tag = tag;
|
|
}
|
|
}
|
|
if (!(tag._er = onError)) {
|
|
tagHandlersFromProps(tag, tagCtxs[0]);
|
|
tag.rendering = {}; // Provide object for state during render calls to tag and elses. (Used by {{if}} and {{for}}...)
|
|
for (i = 0; i < l; i++) { // Iterate tagCtx for each {{else}} block
|
|
tagCtx = tag.tagCtx = tagCtxs[i];
|
|
props = tagCtx.props;
|
|
tag.ctx = tagCtx.ctx;
|
|
|
|
if (!i) {
|
|
if (callInit) {
|
|
tag.init(tagCtx, linkCtx, tag.ctx);
|
|
callInit = undefined;
|
|
}
|
|
if (linkCtx) {
|
|
// Set attr on linkCtx to ensure outputting to the correct target attribute.
|
|
// Setting either linkCtx.attr or this.attr in the init() allows per-instance choice of target attrib.
|
|
linkCtx.attr = tag.attr = linkCtx.attr || tag.attr;
|
|
}
|
|
attr = tag.attr;
|
|
tag._.noVws = attr && attr !== HTML;
|
|
}
|
|
args = tag.cvtArgs(undefined, undefined, i);
|
|
if (tag.linkedCtxParam) {
|
|
m = bindTo.length;
|
|
while (m--) {
|
|
if (ctxPrm = tag.linkedCtxParam[m]) {
|
|
key = bindTo[m];
|
|
// Create tag contextual parameter
|
|
tagCtx.ctx[ctxPrm] = $sub._cp(argOrProp(tagCtx, key), argOrProp(tagCtx.params, key), tagCtx.view, tag._.bnd && {tag: tag, ind: m, tagElse: i});
|
|
}
|
|
}
|
|
}
|
|
if (mapDef = props.dataMap || tagDataMap) {
|
|
if (args.length || props.dataMap) {
|
|
thisMap = tagCtx.map;
|
|
if (!thisMap || thisMap.src !== args[0] || isUpdate) {
|
|
if (thisMap && thisMap.src) {
|
|
thisMap.unmap(); // only called if observable map - not when only used in JsRender, e.g. by {{props}}
|
|
}
|
|
thisMap = tagCtx.map = mapDef.map(args[0], props, undefined, !tag._.bnd);
|
|
}
|
|
args = [thisMap.tgt];
|
|
}
|
|
}
|
|
|
|
itemRet = undefined;
|
|
if (tag.render) {
|
|
itemRet = tag.render.apply(tag, args);
|
|
if (parentView.linked && itemRet && !rWrappedInViewMarker.test(itemRet)) {
|
|
// When a tag renders content from the render method, with data linking then we need to wrap with view markers, if absent,
|
|
// to provide a contentView for the tag, which will correctly dispose bindings if deleted. The 'tmpl' for this view will
|
|
// be a dumbed down template which will always return the itemRet string (no matter what the data is). The itemRet string
|
|
// is not compiled as template markup, so can include "{{" or "}}" without triggering syntax errors
|
|
tmpl = { // 'Dumbed down' template which always renders 'static' itemRet string
|
|
links: []
|
|
};
|
|
tmpl.render = tmpl.fn = function() {
|
|
return itemRet;
|
|
};
|
|
itemRet = renderWithViews(tmpl, parentView.data, undefined, true, parentView, undefined, undefined, tag);
|
|
}
|
|
}
|
|
if (!args.length) {
|
|
args = [parentView]; // no arguments - (e.g. {{else}}) get data context from view.
|
|
}
|
|
if (itemRet === undefined) {
|
|
contentCtx = args[0]; // Default data context for wrapped block content is the first argument
|
|
if (tag.contentCtx) { // Set tag.contentCtx to true, to inherit parent context, or to a function to provide alternate context.
|
|
contentCtx = tag.contentCtx === true ? parentView : tag.contentCtx(contentCtx);
|
|
}
|
|
itemRet = tagCtx.render(contentCtx, true) || (isUpdate ? undefined : "");
|
|
}
|
|
// No return value from render, and no template/content tagCtx.render(...), so return undefined
|
|
ret = ret ? ret + (itemRet || "") : itemRet; // If no rendered content, this will be undefined
|
|
}
|
|
tag.rendering = undefined;
|
|
}
|
|
tag.tagCtx = tagCtxs[0];
|
|
tag.ctx = tag.tagCtx.ctx;
|
|
|
|
if (tag._.noVws) {
|
|
if (tag._.inline) {
|
|
// inline tag with attr set to "text" will insert HTML-encoded content - as if it was element-based innerText
|
|
ret = attr === "text"
|
|
? $converters.html(ret)
|
|
: "";
|
|
}
|
|
}
|
|
return boundTag && parentView._.onRender
|
|
// Call onRender (used by JsViews if present, to add binding annotations around rendered content)
|
|
? parentView._.onRender(ret, parentView, tag)
|
|
: ret;
|
|
}
|
|
|
|
//=================
|
|
// View constructor
|
|
//=================
|
|
|
|
function View(context, type, parentView, data, template, key, onRender, contentTmpl) {
|
|
// Constructor for view object in view hierarchy. (Augmented by JsViews if JsViews is loaded)
|
|
var views, parentView_, tag, self_,
|
|
self = this,
|
|
isArray = type === "array";
|
|
|
|
self.content = contentTmpl;
|
|
self.views = isArray ? [] : {};
|
|
self.parent = parentView;
|
|
self.type = type || "top";
|
|
self.root = parentView && parentView.root || type && self; // view whose parent is top view
|
|
self.data = data;
|
|
self.tmpl = template;
|
|
// If the data is an array, this is an 'array view' with a views array for each child 'item view'
|
|
// If the data is not an array, this is an 'item view' with a views 'hash' object for any child nested views
|
|
// ._.useKey is non zero if is not an 'array view' (owning a data array). Use this as next key for adding to child views hash
|
|
self_ = self._ = {
|
|
key: 0,
|
|
useKey: isArray ? 0 : 1,
|
|
id: "" + viewId++,
|
|
onRender: onRender,
|
|
bnds: {}
|
|
};
|
|
self.linked = !!onRender;
|
|
if (parentView) {
|
|
views = parentView.views;
|
|
parentView_ = parentView._;
|
|
if (parentView_.useKey) {
|
|
// Parent is not an 'array view'. Add this view to its views object
|
|
// self._key = is the key in the parent view hash
|
|
views[self_.key = "_" + parentView_.useKey++] = self;
|
|
self.index = indexStr;
|
|
self.getIndex = getNestedIndex;
|
|
} else if (views.length === (self_.key = self.index = key)) { // Parent is an 'array view'. Add this view to its views array
|
|
views.push(self); // Adding to end of views array. (Using push when possible - better perf than splice)
|
|
} else {
|
|
views.splice(key, 0, self); // Inserting in views array
|
|
}
|
|
// If no context was passed in, use parent context
|
|
// If context was passed in, it should have been merged already with parent context
|
|
self.ctx = context || parentView.ctx;
|
|
} else {
|
|
self.ctx = context || {};
|
|
}
|
|
}
|
|
|
|
View.prototype = {
|
|
get: getView,
|
|
getIndex: getIndex,
|
|
getRsc: getResource,
|
|
getTmpl: getTemplate,
|
|
ctxPrm: contextParameter,
|
|
_is: "view"
|
|
};
|
|
|
|
//====================================================
|
|
// Registration
|
|
//====================================================
|
|
|
|
function compileChildResources(parentTmpl) {
|
|
var storeName, storeNames, resources;
|
|
for (storeName in jsvStores) {
|
|
storeNames = storeName + "s";
|
|
if (parentTmpl[storeNames]) {
|
|
resources = parentTmpl[storeNames]; // Resources not yet compiled
|
|
parentTmpl[storeNames] = {}; // Remove uncompiled resources
|
|
$views[storeNames](resources, parentTmpl); // Add back in the compiled resources
|
|
}
|
|
}
|
|
}
|
|
|
|
//===============
|
|
// compileTag
|
|
//===============
|
|
|
|
function compileTag(name, tagDef, parentTmpl) {
|
|
var tmpl, baseTag, prop, l, key, bindToLength,
|
|
bindTo = tagDef.bindTo,
|
|
compiledDef = new $sub._tg();
|
|
|
|
function Tag() {
|
|
var tag = this;
|
|
tag._ = {
|
|
inline: true,
|
|
unlinked: true
|
|
};
|
|
|
|
tag.tagName = name;
|
|
}
|
|
|
|
function makeArray(type) {
|
|
var linkedElement;
|
|
if (linkedElement = tagDef[type]) {
|
|
tagDef[type] = linkedElement = $isArray(linkedElement) ? linkedElement: [linkedElement];
|
|
if ((bindToLength || 1) !== linkedElement.length) {
|
|
error(type + " length not same as bindTo ");
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($isFunction(tagDef)) {
|
|
// Simple tag declared as function. No presenter instantation.
|
|
tagDef = {
|
|
depends: tagDef.depends,
|
|
render: tagDef
|
|
};
|
|
} else if ("" + tagDef === tagDef) {
|
|
tagDef = {template: tagDef};
|
|
}
|
|
|
|
if (bindTo !== undefined) {
|
|
bindTo = tagDef.bindTo = $isArray(bindTo) ? bindTo : [bindTo];
|
|
l = bindToLength = bindTo.length;
|
|
while (l--) {
|
|
key = bindTo[l];
|
|
if (!isNaN(parseInt(key))) {
|
|
key = parseInt(key); // Convert "0" to 0, etc.
|
|
}
|
|
bindTo[l] = key;
|
|
}
|
|
}
|
|
|
|
makeArray("linkedElement");
|
|
makeArray("linkedCtxParam");
|
|
|
|
if (baseTag = tagDef.baseTag) {
|
|
tagDef.flow = !!tagDef.flow; // Set flow property, so defaults to false even if baseTag has flow=true
|
|
tagDef.baseTag = baseTag = "" + baseTag === baseTag
|
|
? (parentTmpl && parentTmpl.tags[baseTag] || $tags[baseTag])
|
|
: baseTag;
|
|
|
|
compiledDef = $extend(compiledDef, baseTag);
|
|
|
|
for (prop in tagDef) {
|
|
compiledDef[prop] = getMethod(baseTag[prop], tagDef[prop]);
|
|
}
|
|
} else {
|
|
compiledDef = $extend(compiledDef, tagDef);
|
|
}
|
|
|
|
// Tag declared as object, used as the prototype for tag instantiation (control/presenter)
|
|
if ((tmpl = compiledDef.template) !== undefined) {
|
|
compiledDef.template = "" + tmpl === tmpl ? ($templates[tmpl] || $templates(tmpl)) : tmpl;
|
|
}
|
|
(Tag.prototype = compiledDef).constructor = compiledDef._ctr = Tag;
|
|
|
|
if (parentTmpl) {
|
|
compiledDef._parentTmpl = parentTmpl;
|
|
}
|
|
return compiledDef;
|
|
}
|
|
|
|
function baseApply(args) {
|
|
// In derived method (or handler declared declaratively as in {{:foo onChange=~fooChanged}} can call base method,
|
|
// using this.baseApply(arguments) (Equivalent to this._superApply(arguments) in jQuery UI)
|
|
return this.base.apply(this, args);
|
|
}
|
|
|
|
//===============
|
|
// compileTmpl
|
|
//===============
|
|
|
|
function compileTmpl(name, tmpl, parentTmpl, options) {
|
|
// tmpl is either a template object, a selector for a template script block, the name of a compiled template, or a template object
|
|
|
|
//==== nested functions ====
|
|
function lookupTemplate(value) {
|
|
// If value is of type string - treat as selector, or name of compiled template
|
|
// Return the template object, if already compiled, or the markup string
|
|
var currentName, tmpl;
|
|
if (("" + value === value) || value.nodeType > 0 && (elem = value)) {
|
|
if (!elem) {
|
|
if (/^\.\/[^\\:*?"<>]*$/.test(value)) {
|
|
// tmpl="./some/file.html"
|
|
// If the template is not named, use "./some/file.html" as name.
|
|
if (tmpl = $templates[name = name || value]) {
|
|
value = tmpl;
|
|
} else {
|
|
// BROWSER-SPECIFIC CODE (not on Node.js):
|
|
// Look for server-generated script block with id "./some/file.html"
|
|
elem = document.getElementById(value);
|
|
}
|
|
} else if ($.fn && !$sub.rTmpl.test(value)) {
|
|
try {
|
|
elem = $ (value, document)[0]; // if jQuery is loaded, test for selector returning elements, and get first element
|
|
} catch (e) {}
|
|
}// END BROWSER-SPECIFIC CODE
|
|
} //BROWSER-SPECIFIC CODE
|
|
if (elem) {
|
|
// Generally this is a script element.
|
|
// However we allow it to be any element, so you can for example take the content of a div,
|
|
// use it as a template, and replace it by the same content rendered against data.
|
|
// e.g. for linking the content of a div to a container, and using the initial content as template:
|
|
// $.link("#content", model, {tmpl: "#content"});
|
|
if (options) {
|
|
// We will compile a new template using the markup in the script element
|
|
value = elem.innerHTML;
|
|
} else {
|
|
// We will cache a single copy of the compiled template, and associate it with the name
|
|
// (renaming from a previous name if there was one).
|
|
currentName = elem.getAttribute(tmplAttr);
|
|
if (currentName) {
|
|
if (currentName !== jsvTmpl) {
|
|
value = $templates[currentName];
|
|
delete $templates[currentName];
|
|
} else if ($.fn) {
|
|
value = $.data(elem)[jsvTmpl]; // Get cached compiled template
|
|
}
|
|
}
|
|
if (!currentName || !value) { // Not yet compiled, or cached version lost
|
|
name = name || ($.fn ? jsvTmpl : value);
|
|
value = compileTmpl(name, elem.innerHTML, parentTmpl, options);
|
|
}
|
|
value.tmplName = name = name || currentName;
|
|
if (name !== jsvTmpl) {
|
|
$templates[name] = value;
|
|
}
|
|
elem.setAttribute(tmplAttr, name);
|
|
if ($.fn) {
|
|
$.data(elem, jsvTmpl, value);
|
|
}
|
|
}
|
|
} // END BROWSER-SPECIFIC CODE
|
|
elem = undefined;
|
|
} else if (!value.fn) {
|
|
value = undefined;
|
|
// If value is not a string. HTML element, or compiled template, return undefined
|
|
}
|
|
return value;
|
|
}
|
|
|
|
var elem, compiledTmpl,
|
|
tmplOrMarkup = tmpl = tmpl || "";
|
|
|
|
//==== Compile the template ====
|
|
if (options === 0) {
|
|
options = undefined;
|
|
tmplOrMarkup = lookupTemplate(tmplOrMarkup); // Top-level compile so do a template lookup
|
|
}
|
|
|
|
// If options, then this was already compiled from a (script) element template declaration.
|
|
// If not, then if tmpl is a template object, use it for options
|
|
options = options || (tmpl.markup ? tmpl : {});
|
|
options.tmplName = name;
|
|
if (parentTmpl) {
|
|
options._parentTmpl = parentTmpl;
|
|
}
|
|
// If tmpl is not a markup string or a selector string, then it must be a template object
|
|
// In that case, get it from the markup property of the object
|
|
if (!tmplOrMarkup && tmpl.markup && (tmplOrMarkup = lookupTemplate(tmpl.markup))) {
|
|
if (tmplOrMarkup.fn) {
|
|
// If the string references a compiled template object, need to recompile to merge any modified options
|
|
tmplOrMarkup = tmplOrMarkup.markup;
|
|
}
|
|
}
|
|
if (tmplOrMarkup !== undefined) {
|
|
if (tmplOrMarkup.fn || tmpl.fn) {
|
|
// tmpl is already compiled, so use it
|
|
if (tmplOrMarkup.fn) {
|
|
compiledTmpl = tmplOrMarkup;
|
|
}
|
|
} else {
|
|
// tmplOrMarkup is a markup string, not a compiled template
|
|
// Create template object
|
|
tmpl = tmplObject(tmplOrMarkup, options);
|
|
// Compile to AST and then to compiled function
|
|
tmplFn(tmplOrMarkup.replace(rEscapeQuotes, "\\$&"), tmpl);
|
|
}
|
|
if (!compiledTmpl) {
|
|
compiledTmpl = $extend(function() {
|
|
return compiledTmpl.render.apply(compiledTmpl, arguments);
|
|
}, tmpl);
|
|
|
|
compileChildResources(compiledTmpl);
|
|
}
|
|
return compiledTmpl;
|
|
}
|
|
}
|
|
|
|
//==== /end of function compileTmpl ====
|
|
|
|
//=================
|
|
// compileViewModel
|
|
//=================
|
|
|
|
function getDefaultVal(defaultVal, data) {
|
|
return $isFunction(defaultVal)
|
|
? defaultVal.call(data)
|
|
: defaultVal;
|
|
}
|
|
|
|
function unmapArray(modelArr) {
|
|
var arr = [],
|
|
i = 0,
|
|
l = modelArr.length;
|
|
for (; i<l; i++) {
|
|
arr.push(modelArr[i].unmap());
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
function compileViewModel(name, type) {
|
|
var i, constructor,
|
|
viewModels = this,
|
|
getters = type.getters,
|
|
extend = type.extend,
|
|
id = type.id,
|
|
proto = $.extend({
|
|
_is: name || "unnamed",
|
|
unmap: unmap,
|
|
merge: merge
|
|
}, extend),
|
|
args = "",
|
|
body = "",
|
|
g = getters ? getters.length : 0,
|
|
$observable = $.observable,
|
|
getterNames = {};
|
|
|
|
function GetNew(args) {
|
|
constructor.apply(this, args);
|
|
}
|
|
|
|
function vm() {
|
|
return new GetNew(arguments);
|
|
}
|
|
|
|
function iterate(data, action) {
|
|
var getterType, defaultVal, prop, ob,
|
|
j = 0;
|
|
for (; j<g; j++) {
|
|
prop = getters[j];
|
|
getterType = undefined;
|
|
if (prop + "" !== prop) {
|
|
getterType = prop;
|
|
prop = getterType.getter;
|
|
}
|
|
if ((ob = data[prop]) === undefined && getterType && (defaultVal = getterType.defaultVal) !== undefined) {
|
|
ob = getDefaultVal(defaultVal, data);
|
|
}
|
|
action(ob, getterType && viewModels[getterType.type], prop);
|
|
}
|
|
}
|
|
|
|
function map(data) {
|
|
data = data + "" === data
|
|
? JSON.parse(data) // Accept JSON string
|
|
: data; // or object/array
|
|
var l, prop,
|
|
j = 0,
|
|
ob = data,
|
|
arr = [];
|
|
|
|
if ($isArray(data)) {
|
|
data = data || [];
|
|
l = data.length;
|
|
for (; j<l; j++) {
|
|
arr.push(this.map(data[j]));
|
|
}
|
|
arr._is = name;
|
|
arr.unmap = unmap;
|
|
arr.merge = merge;
|
|
return arr;
|
|
}
|
|
|
|
if (data) {
|
|
iterate(data, function(ob, viewModel) {
|
|
if (viewModel) { // Iterate to build getters arg array (value, or mapped value)
|
|
ob = viewModel.map(ob);
|
|
}
|
|
arr.push(ob);
|
|
});
|
|
|
|
ob = this.apply(this, arr); // Insantiate this View Model, passing getters args array to constructor
|
|
for (prop in data) { // Copy over any other properties. that are not get/set properties
|
|
if (prop !== $expando && !getterNames[prop]) {
|
|
ob[prop] = data[prop];
|
|
}
|
|
}
|
|
}
|
|
return ob;
|
|
}
|
|
|
|
function merge(data) {
|
|
data = data + "" === data
|
|
? JSON.parse(data) // Accept JSON string
|
|
: data; // or object/array
|
|
var j, l, m, prop, mod, found, assigned, ob, newModArr,
|
|
k = 0,
|
|
model = this;
|
|
|
|
if ($isArray(model)) {
|
|
assigned = {};
|
|
newModArr = [];
|
|
l = data.length;
|
|
m = model.length;
|
|
for (; k<l; k++) {
|
|
ob = data[k];
|
|
found = false;
|
|
for (j=0; j<m && !found; j++) {
|
|
if (assigned[j]) {
|
|
continue;
|
|
}
|
|
mod = model[j];
|
|
|
|
if (id) {
|
|
assigned[j] = found = id + "" === id
|
|
? (ob[id] && (getterNames[id] ? mod[id]() : mod[id]) === ob[id])
|
|
: id(mod, ob);
|
|
}
|
|
}
|
|
if (found) {
|
|
mod.merge(ob);
|
|
newModArr.push(mod);
|
|
} else {
|
|
newModArr.push(vm.map(ob));
|
|
}
|
|
}
|
|
if ($observable) {
|
|
$observable(model).refresh(newModArr, true);
|
|
} else {
|
|
model.splice.apply(model, [0, model.length].concat(newModArr));
|
|
}
|
|
return;
|
|
}
|
|
iterate(data, function(ob, viewModel, getter) {
|
|
if (viewModel) {
|
|
model[getter]().merge(ob); // Update typed property
|
|
} else {
|
|
model[getter](ob); // Update non-typed property
|
|
}
|
|
});
|
|
for (prop in data) {
|
|
if (prop !== $expando && !getterNames[prop]) {
|
|
model[prop] = data[prop];
|
|
}
|
|
}
|
|
}
|
|
|
|
function unmap() {
|
|
var ob, prop, getterType, arr, value,
|
|
k = 0,
|
|
model = this;
|
|
|
|
if ($isArray(model)) {
|
|
return unmapArray(model);
|
|
}
|
|
ob = {};
|
|
for (; k<g; k++) {
|
|
prop = getters[k];
|
|
getterType = undefined;
|
|
if (prop + "" !== prop) {
|
|
getterType = prop;
|
|
prop = getterType.getter;
|
|
}
|
|
value = model[prop]();
|
|
ob[prop] = getterType && value && viewModels[getterType.type]
|
|
? $isArray(value)
|
|
? unmapArray(value)
|
|
: value.unmap()
|
|
: value;
|
|
}
|
|
for (prop in model) {
|
|
if (prop !== "_is" && !getterNames[prop] && prop !== $expando && (prop.charAt(0) !== "_" || !getterNames[prop.slice(1)]) && !$isFunction(model[prop])) {
|
|
ob[prop] = model[prop];
|
|
}
|
|
}
|
|
return ob;
|
|
}
|
|
|
|
GetNew.prototype = proto;
|
|
|
|
for (i=0; i<g; i++) {
|
|
(function(getter) {
|
|
getter = getter.getter || getter;
|
|
getterNames[getter] = i+1;
|
|
var privField = "_" + getter;
|
|
|
|
args += (args ? "," : "") + getter;
|
|
body += "this." + privField + " = " + getter + ";\n";
|
|
proto[getter] = proto[getter] || function(val) {
|
|
if (!arguments.length) {
|
|
return this[privField]; // If there is no argument, use as a getter
|
|
}
|
|
if ($observable) {
|
|
$observable(this).setProperty(getter, val);
|
|
} else {
|
|
this[privField] = val;
|
|
}
|
|
};
|
|
|
|
if ($observable) {
|
|
proto[getter].set = proto[getter].set || function(val) {
|
|
this[privField] = val; // Setter called by observable property change
|
|
};
|
|
}
|
|
})(getters[i]);
|
|
}
|
|
|
|
constructor = new Function(args, body.slice(0, -1));
|
|
constructor.prototype = proto;
|
|
proto.constructor = constructor;
|
|
|
|
vm.map = map;
|
|
vm.getters = getters;
|
|
vm.extend = extend;
|
|
vm.id = id;
|
|
return vm;
|
|
}
|
|
|
|
function tmplObject(markup, options) {
|
|
// Template object constructor
|
|
var htmlTag,
|
|
wrapMap = $subSettingsAdvanced._wm || {}, // Only used in JsViews. Otherwise empty: {}
|
|
tmpl = $extend(
|
|
{
|
|
tmpls: [],
|
|
links: {}, // Compiled functions for link expressions
|
|
bnds: [],
|
|
_is: "template",
|
|
render: renderContent
|
|
},
|
|
options
|
|
);
|
|
|
|
tmpl.markup = markup;
|
|
if (!options.htmlTag) {
|
|
// Set tmpl.tag to the top-level HTML tag used in the template, if any...
|
|
htmlTag = rFirstElem.exec(markup);
|
|
tmpl.htmlTag = htmlTag ? htmlTag[1].toLowerCase() : "";
|
|
}
|
|
htmlTag = wrapMap[tmpl.htmlTag];
|
|
if (htmlTag && htmlTag !== wrapMap.div) {
|
|
// When using JsViews, we trim templates which are inserted into HTML contexts where text nodes are not rendered (i.e. not 'Phrasing Content').
|
|
// Currently not trimmed for <li> tag. (Not worth adding perf cost)
|
|
tmpl.markup = $.trim(tmpl.markup);
|
|
}
|
|
|
|
return tmpl;
|
|
}
|
|
|
|
//==============
|
|
// registerStore
|
|
//==============
|
|
|
|
function registerStore(storeName, storeSettings) {
|
|
|
|
function theStore(name, item, parentTmpl) {
|
|
// The store is also the function used to add items to the store. e.g. $.templates, or $.views.tags
|
|
|
|
// For store of name 'thing', Call as:
|
|
// $.views.things(items[, parentTmpl]),
|
|
// or $.views.things(name, item[, parentTmpl])
|
|
|
|
var compile, itemName, thisStore, cnt,
|
|
onStore = $sub.onStore[storeName];
|
|
|
|
if (name && typeof name === OBJECT && !name.nodeType && !name.markup && !name.getTgt && !(storeName === "viewModel" && name.getters || name.extend)) {
|
|
// Call to $.views.things(items[, parentTmpl]),
|
|
|
|
// Adding items to the store
|
|
// If name is a hash, then item is parentTmpl. Iterate over hash and call store for key.
|
|
for (itemName in name) {
|
|
theStore(itemName, name[itemName], item);
|
|
}
|
|
return item || $views;
|
|
}
|
|
// Adding a single unnamed item to the store
|
|
if (item === undefined) {
|
|
item = name;
|
|
name = undefined;
|
|
}
|
|
if (name && "" + name !== name) { // name must be a string
|
|
parentTmpl = item;
|
|
item = name;
|
|
name = undefined;
|
|
}
|
|
thisStore = parentTmpl
|
|
? storeName === "viewModel"
|
|
? parentTmpl
|
|
: (parentTmpl[storeNames] = parentTmpl[storeNames] || {})
|
|
: theStore;
|
|
compile = storeSettings.compile;
|
|
|
|
if (item === null) {
|
|
// If item is null, delete this entry
|
|
if (name) {
|
|
delete thisStore[name];
|
|
}
|
|
} else {
|
|
if (compile) {
|
|
item = compile.call(thisStore, name, item, parentTmpl, 0);
|
|
item._is = storeName; // Only do this for compiled objects (tags, templates...)
|
|
}
|
|
// e.g. JsViews integration
|
|
|
|
if (name) {
|
|
thisStore[name] = item;
|
|
}
|
|
}
|
|
if (onStore) {
|
|
onStore(name, item, parentTmpl, compile);
|
|
}
|
|
return item;
|
|
}
|
|
|
|
var storeNames = storeName + "s";
|
|
$views[storeNames] = theStore;
|
|
}
|
|
|
|
function addSetting(st) {
|
|
$viewsSettings[st] = function(value) {
|
|
return arguments.length
|
|
? ($subSettings[st] = value, $viewsSettings)
|
|
: $subSettings[st];
|
|
};
|
|
}
|
|
|
|
//=========
|
|
// dataMap
|
|
//=========
|
|
|
|
function dataMap(mapDef) {
|
|
function Map(source, options) {
|
|
this.tgt = mapDef.getTgt(source, options);
|
|
}
|
|
|
|
if ($isFunction(mapDef)) {
|
|
// Simple map declared as function
|
|
mapDef = {
|
|
getTgt: mapDef
|
|
};
|
|
}
|
|
|
|
if (mapDef.baseMap) {
|
|
mapDef = $extend($extend({}, mapDef.baseMap), mapDef);
|
|
}
|
|
|
|
mapDef.map = function(source, options) {
|
|
return new Map(source, options);
|
|
};
|
|
return mapDef;
|
|
}
|
|
|
|
//==============
|
|
// renderContent
|
|
//==============
|
|
|
|
function renderContent(data, context, noIteration, parentView, key, onRender) {
|
|
var i, l, tag, tmpl, tagCtx, isTopRenderCall, prevData, prevIndex,
|
|
view = parentView,
|
|
result = "";
|
|
|
|
if (context === true) {
|
|
noIteration = context; // passing boolean as second param - noIteration
|
|
context = undefined;
|
|
} else if (typeof context !== OBJECT) {
|
|
context = undefined; // context must be a boolean (noIteration) or a plain object
|
|
}
|
|
|
|
if (tag = this.tag) {
|
|
// This is a call from renderTag or tagCtx.render(...)
|
|
tagCtx = this;
|
|
view = view || tagCtx.view;
|
|
tmpl = view.getTmpl(tag.template || tagCtx.tmpl);
|
|
if (!arguments.length) {
|
|
data = view;
|
|
}
|
|
} else {
|
|
// This is a template.render(...) call
|
|
tmpl = this;
|
|
}
|
|
|
|
if (tmpl) {
|
|
if (!parentView && data && data._is === "view") {
|
|
view = data; // When passing in a view to render or link (and not passing in a parent view) use the passed-in view as parentView
|
|
}
|
|
|
|
if (view) {
|
|
if (data === view) {
|
|
// Inherit the data from the parent view.
|
|
// This may be the contents of an {{if}} block
|
|
data = view.data;
|
|
}
|
|
}
|
|
|
|
isTopRenderCall = !view;
|
|
isRenderCall = isRenderCall || isTopRenderCall;
|
|
if (!view) {
|
|
(context = context || {}).root = data; // Provide ~root as shortcut to top-level data.
|
|
}
|
|
if (!isRenderCall || $subSettingsAdvanced.useViews || tmpl.useViews || view && view !== topView) {
|
|
result = renderWithViews(tmpl, data, context, noIteration, view, key, onRender, tag);
|
|
} else {
|
|
if (view) { // In a block
|
|
prevData = view.data;
|
|
prevIndex = view.index;
|
|
view.index = indexStr;
|
|
} else {
|
|
view = topView;
|
|
view.data = data;
|
|
view.ctx = context;
|
|
}
|
|
if ($isArray(data) && !noIteration) {
|
|
// Create a view for the array, whose child views correspond to each data item. (Note: if key and parentView are passed in
|
|
// along with parent view, treat as insert -e.g. from view.addViews - so parentView is already the view item for array)
|
|
for (i = 0, l = data.length; i < l; i++) {
|
|
view.index = i;
|
|
view.data = data[i];
|
|
result += tmpl.fn(data[i], view, $sub);
|
|
}
|
|
} else {
|
|
view.data = data;
|
|
result += tmpl.fn(data, view, $sub);
|
|
}
|
|
view.data = prevData;
|
|
view.index = prevIndex;
|
|
}
|
|
if (isTopRenderCall) {
|
|
isRenderCall = undefined;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function renderWithViews(tmpl, data, context, noIteration, view, key, onRender, tag) {
|
|
function setItemVar(item) {
|
|
// When itemVar is specified, set modified ctx with user-named ~item
|
|
newCtx = $extend({}, context);
|
|
newCtx[itemVar] = item;
|
|
}
|
|
|
|
// Render template against data as a tree of subviews (nested rendered template instances), or as a string (top-level template).
|
|
// If the data is the parent view, treat as noIteration, re-render with the same data context.
|
|
// tmpl can be a string (e.g. rendered by a tag.render() method), or a compiled template.
|
|
var i, l, newView, childView, itemResult, swapContent, contentTmpl, outerOnRender, tmplName, itemVar, newCtx, tagCtx,
|
|
result = "";
|
|
|
|
if (tag) {
|
|
// This is a call from renderTag or tagCtx.render(...)
|
|
tmplName = tag.tagName;
|
|
tagCtx = tag.tagCtx;
|
|
context = context ? extendCtx(context, tag.ctx) : tag.ctx;
|
|
|
|
if (tmpl === view.content) { // {{xxx tmpl=#content}}
|
|
contentTmpl = tmpl !== view.ctx._wrp // We are rendering the #content
|
|
? view.ctx._wrp // #content was the tagCtx.props.tmpl wrapper of the block content - so within this view, #content will now be the view.ctx._wrp block content
|
|
: undefined; // #content was the view.ctx._wrp block content - so within this view, there is no longer any #content to wrap.
|
|
} else if (tmpl !== tagCtx.content) {
|
|
if (tmpl === tag.template) { // Rendering {{tag}} tag.template, replacing block content.
|
|
contentTmpl = tagCtx.tmpl; // Set #content to block content (or wrapped block content if tagCtx.props.tmpl is set)
|
|
context._wrp = tagCtx.content; // Pass wrapped block content to nested views
|
|
} else { // Rendering tagCtx.props.tmpl wrapper
|
|
contentTmpl = tagCtx.content || view.content; // Set #content to wrapped block content
|
|
}
|
|
} else {
|
|
contentTmpl = view.content; // Nested views inherit same wrapped #content property
|
|
}
|
|
|
|
if (tagCtx.props.link === false) {
|
|
// link=false setting on block tag
|
|
// We will override inherited value of link by the explicit setting link=false taken from props
|
|
// The child views of an unlinked view are also unlinked. So setting child back to true will not have any effect.
|
|
context = context || {};
|
|
context.link = false;
|
|
}
|
|
|
|
if (itemVar = tagCtx.props.itemVar) {
|
|
if (itemVar.charAt(0) !== "~") {
|
|
syntaxError("Use itemVar='~myItem'");
|
|
}
|
|
itemVar = itemVar.slice(1);
|
|
}
|
|
}
|
|
|
|
if (view) {
|
|
onRender = onRender || view._.onRender;
|
|
context = extendCtx(context, view.ctx);
|
|
}
|
|
|
|
if (key === true) {
|
|
swapContent = true;
|
|
key = 0;
|
|
}
|
|
|
|
// If link===false, do not call onRender, so no data-linking marker nodes
|
|
if (onRender && (context && context.link === false || tag && tag._.noVws)) {
|
|
onRender = undefined;
|
|
}
|
|
outerOnRender = onRender;
|
|
if (onRender === true) {
|
|
// Used by view.refresh(). Don't create a new wrapper view.
|
|
outerOnRender = undefined;
|
|
onRender = view._.onRender;
|
|
}
|
|
// Set additional context on views created here, (as modified context inherited from the parent, and to be inherited by child views)
|
|
context = tmpl.helpers
|
|
? extendCtx(tmpl.helpers, context)
|
|
: context;
|
|
|
|
newCtx = context;
|
|
if ($isArray(data) && !noIteration) {
|
|
// Create a view for the array, whose child views correspond to each data item. (Note: if key and view are passed in
|
|
// along with parent view, treat as insert -e.g. from view.addViews - so view is already the view item for array)
|
|
newView = swapContent
|
|
? view
|
|
: (key !== undefined && view)
|
|
|| new View(context, "array", view, data, tmpl, key, onRender, contentTmpl);
|
|
if (view && view._.useKey) {
|
|
// Parent is not an 'array view'
|
|
newView._.bnd = !tag || tag._.bnd && tag; // For array views that are data bound for collection change events, set the
|
|
// view._.bnd property to true for top-level link() or data-link="{for}", or to the tag instance for a data-bound tag, e.g. {^{for ...}}
|
|
}
|
|
for (i = 0, l = data.length; i < l; i++) {
|
|
// Create a view for each data item.
|
|
if (itemVar) {
|
|
setItemVar(data[i]); // use modified ctx with user-named ~item
|
|
}
|
|
childView = new View(newCtx, "item", newView, data[i], tmpl, (key || 0) + i, onRender, newView.content);
|
|
childView._.it = itemVar;
|
|
|
|
itemResult = tmpl.fn(data[i], childView, $sub);
|
|
result += newView._.onRender ? newView._.onRender(itemResult, childView) : itemResult;
|
|
}
|
|
} else {
|
|
// Create a view for singleton data object. The type of the view will be the tag name, e.g. "if" or "mytag" except for
|
|
// "item", "array" and "data" views. A "data" view is from programmatic render(object) against a 'singleton'.
|
|
if (itemVar) {
|
|
setItemVar(data);
|
|
}
|
|
newView = swapContent ? view : new View(newCtx, tmplName || "data", view, data, tmpl, key, onRender, contentTmpl);
|
|
newView._.it = itemVar;
|
|
result += tmpl.fn(data, newView, $sub);
|
|
}
|
|
if (tag) {
|
|
newView.tag = tag;
|
|
newView.tagElse = tagCtx.index;
|
|
tagCtx.contentView = newView;
|
|
}
|
|
return outerOnRender ? outerOnRender(result, newView) : result;
|
|
}
|
|
|
|
//===========================
|
|
// Build and compile template
|
|
//===========================
|
|
|
|
// Generate a reusable function that will serve to render a template against data
|
|
// (Compile AST then build template function)
|
|
|
|
function onRenderError(e, view, fallback) {
|
|
var message = fallback !== undefined
|
|
? $isFunction(fallback)
|
|
? fallback.call(view.data, e, view)
|
|
: fallback || ""
|
|
: "{Error: " + (e.message||e) + "}";
|
|
|
|
if ($subSettings.onError && (fallback = $subSettings.onError.call(view.data, e, fallback && message, view)) !== undefined) {
|
|
message = fallback; // There is a settings.debugMode(handler) onError override. Call it, and use return value (if any) to replace message
|
|
}
|
|
|
|
return view && !view.linkCtx ? $converters.html(message) : message;
|
|
}
|
|
|
|
function error(message) {
|
|
throw new $sub.Err(message);
|
|
}
|
|
|
|
function syntaxError(message) {
|
|
error("Syntax error\n" + message);
|
|
}
|
|
|
|
function tmplFn(markup, tmpl, isLinkExpr, convertBack, hasElse) {
|
|
// Compile markup to AST (abtract syntax tree) then build the template function code from the AST nodes
|
|
// Used for compiling templates, and also by JsViews to build functions for data link expressions
|
|
|
|
//==== nested functions ====
|
|
function pushprecedingContent(shift) {
|
|
shift -= loc;
|
|
if (shift) {
|
|
content.push(markup.substr(loc, shift).replace(rNewLine, "\\n"));
|
|
}
|
|
}
|
|
|
|
function blockTagCheck(tagName, block) {
|
|
if (tagName) {
|
|
tagName += '}}';
|
|
// '{{include}} block has {{/for}} with no open {{for}}'
|
|
syntaxError((
|
|
block
|
|
? '{{' + block + '}} block has {{/' + tagName + ' without {{' + tagName
|
|
: 'Unmatched or missing {{/' + tagName) + ', in template:\n' + markup);
|
|
}
|
|
}
|
|
|
|
function parseTag(all, bind, tagName, converter, colon, html, codeTag, params, slash, bind2, closeBlock, index) {
|
|
/*
|
|
|
|
bind tagName cvt cln html code params slash bind2 closeBlk comment
|
|
/(?:{(\^)?{(?:(\w+(?=[\/\s}]))|(\w+)?(:)|(>)|(\*))\s*((?:[^}]|}(?!}))*?)(\/)?|{(\^)?{(?:(?:\/(\w+))\s*|!--[\s\S]*?--))}}/g
|
|
|
|
(?:
|
|
{(\^)?{ bind
|
|
(?:
|
|
(\w+ tagName
|
|
(?=[\/\s}])
|
|
)
|
|
|
|
|
(\w+)?(:) converter colon
|
|
|
|
|
(>) html
|
|
|
|
|
(\*) codeTag
|
|
)
|
|
\s*
|
|
( params
|
|
(?:[^}]|}(?!}))*?
|
|
)
|
|
(\/)? slash
|
|
|
|
|
{(\^)?{ bind2
|
|
(?:
|
|
(?:\/(\w+))\s* closeBlock
|
|
|
|
|
!--[\s\S]*?-- comment
|
|
)
|
|
)
|
|
}}/g
|
|
|
|
*/
|
|
if (codeTag && bind || slash && !tagName || params && params.slice(-1) === ":" || bind2) {
|
|
syntaxError(all);
|
|
}
|
|
|
|
// Build abstract syntax tree (AST): [tagName, converter, params, content, hash, bindings, contentMarkup]
|
|
if (html) {
|
|
colon = ":";
|
|
converter = HTML;
|
|
}
|
|
slash = slash || isLinkExpr && !hasElse;
|
|
|
|
var late,
|
|
pathBindings = (bind || isLinkExpr) && [[]], // pathBindings is an array of arrays for arg bindings and a hash of arrays for prop bindings
|
|
props = "",
|
|
args = "",
|
|
ctxProps = "",
|
|
paramsArgs = "",
|
|
paramsProps = "",
|
|
paramsCtxProps = "",
|
|
onError = "",
|
|
useTrigger = "",
|
|
// Block tag if not self-closing and not {{:}} or {{>}} (special case) and not a data-link expression
|
|
block = !slash && !colon;
|
|
|
|
//==== nested helper function ====
|
|
tagName = tagName || (params = params || "#data", colon); // {{:}} is equivalent to {{:#data}}
|
|
pushprecedingContent(index);
|
|
loc = index + all.length; // location marker - parsed up to here
|
|
if (codeTag) {
|
|
if (allowCode) {
|
|
content.push(["*", "\n" + params.replace(/^:/, "ret+= ").replace(rUnescapeQuotes, "$1") + ";\n"]);
|
|
}
|
|
} else if (tagName) {
|
|
if (tagName === "else") {
|
|
if (rTestElseIf.test(params)) {
|
|
syntaxError('for "{{else if expr}}" use "{{else expr}}"');
|
|
}
|
|
pathBindings = current[8] && [[]];
|
|
current[9] = markup.substring(current[9], index); // contentMarkup for block tag
|
|
current = stack.pop();
|
|
content = current[2];
|
|
block = true;
|
|
}
|
|
if (params) {
|
|
// remove newlines from the params string, to avoid compiled code errors for unterminated strings
|
|
parseParams(params.replace(rNewLine, " "), pathBindings, tmpl)
|
|
.replace(rBuildHash, function(all, onerror, isCtx, key, keyToken, keyValue, arg, param) {
|
|
key = "'" + keyToken + "':";
|
|
if (arg) {
|
|
args += keyValue + ",";
|
|
paramsArgs += "'" + param + "',";
|
|
} else if (isCtx) {
|
|
ctxProps += key + 'j._cp(' + keyValue + ',"' + param + '",view),';
|
|
// Compiled code for evaluating tagCtx on a tag will have: ctx:{'foo':j._cp(compiledExpr, "expr", view)}
|
|
paramsCtxProps += key + "'" + param + "',";
|
|
} else if (onerror) {
|
|
onError += keyValue;
|
|
} else {
|
|
if (keyToken === "trigger") {
|
|
useTrigger += keyValue;
|
|
}
|
|
if (keyToken === "lateRender") {
|
|
late = 1; // Render after first pass
|
|
}
|
|
props += key + keyValue + ",";
|
|
paramsProps += key + "'" + param + "',";
|
|
hasHandlers = hasHandlers || rHasHandlers.test(keyToken);
|
|
}
|
|
return "";
|
|
}).slice(0, -1);
|
|
}
|
|
|
|
if (pathBindings && pathBindings[0]) {
|
|
pathBindings.pop(); // Remove the binding that was prepared for next arg. (There is always an extra one ready).
|
|
}
|
|
|
|
newNode = [
|
|
tagName,
|
|
converter || !!convertBack || hasHandlers || "",
|
|
block && [],
|
|
parsedParam(paramsArgs || (tagName === ":" ? "'#data'," : ""), paramsProps, paramsCtxProps), // {{:}} equivalent to {{:#data}}
|
|
parsedParam(args || (tagName === ":" ? "data," : ""), props, ctxProps),
|
|
onError,
|
|
useTrigger,
|
|
late,
|
|
pathBindings || 0
|
|
];
|
|
content.push(newNode);
|
|
if (block) {
|
|
stack.push(current);
|
|
current = newNode;
|
|
current[9] = loc; // Store current location of open tag, to be able to add contentMarkup when we reach closing tag
|
|
}
|
|
} else if (closeBlock) {
|
|
blockTagCheck(closeBlock !== current[0] && current[0] !== "else" && closeBlock, current[0]);
|
|
current[9] = markup.substring(current[9], index); // contentMarkup for block tag
|
|
current = stack.pop();
|
|
}
|
|
blockTagCheck(!current && closeBlock);
|
|
content = current[2];
|
|
}
|
|
//==== /end of nested functions ====
|
|
|
|
var i, result, newNode, hasHandlers, bindings,
|
|
allowCode = $subSettings.allowCode || tmpl && tmpl.allowCode
|
|
|| $viewsSettings.allowCode === true, // include direct setting of settings.allowCode true for backward compat only
|
|
astTop = [],
|
|
loc = 0,
|
|
stack = [],
|
|
content = astTop,
|
|
current = [,,astTop];
|
|
|
|
if (allowCode && tmpl._is) {
|
|
tmpl.allowCode = allowCode;
|
|
}
|
|
|
|
//TODO result = tmplFnsCache[markup]; // Only cache if template is not named and markup length < ...,
|
|
//and there are no bindings or subtemplates?? Consider standard optimization for data-link="a.b.c"
|
|
// if (result) {
|
|
// tmpl.fn = result;
|
|
// } else {
|
|
|
|
// result = markup;
|
|
if (isLinkExpr) {
|
|
if (convertBack !== undefined) {
|
|
markup = markup.slice(0, -convertBack.length - 2) + delimCloseChar0;
|
|
}
|
|
markup = delimOpenChar0 + markup + delimCloseChar1;
|
|
}
|
|
|
|
blockTagCheck(stack[0] && stack[0][2].pop()[0]);
|
|
// Build the AST (abstract syntax tree) under astTop
|
|
markup.replace(rTag, parseTag);
|
|
|
|
pushprecedingContent(markup.length);
|
|
|
|
if (loc = astTop[astTop.length - 1]) {
|
|
blockTagCheck("" + loc !== loc && (+loc[9] === loc[9]) && loc[0]);
|
|
}
|
|
// result = tmplFnsCache[markup] = buildCode(astTop, tmpl);
|
|
// }
|
|
|
|
if (isLinkExpr) {
|
|
result = buildCode(astTop, markup, isLinkExpr);
|
|
bindings = [];
|
|
i = astTop.length;
|
|
while (i--) {
|
|
bindings.unshift(astTop[i][8]); // With data-link expressions, pathBindings array for tagCtx[i] is astTop[i][8]
|
|
}
|
|
setPaths(result, bindings);
|
|
} else {
|
|
result = buildCode(astTop, tmpl);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function setPaths(fn, pathsArr) {
|
|
var key, paths,
|
|
i = 0,
|
|
l = pathsArr.length;
|
|
fn.deps = [];
|
|
fn.paths = []; // The array of path binding (array/dictionary)s for each tag/else block's args and props
|
|
for (; i < l; i++) {
|
|
fn.paths.push(paths = pathsArr[i]);
|
|
for (key in paths) {
|
|
if (key !== "_jsvto" && paths.hasOwnProperty(key) && paths[key].length && !paths[key].skp) {
|
|
fn.deps = fn.deps.concat(paths[key]); // deps is the concatenation of the paths arrays for the different bindings
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function parsedParam(args, props, ctx) {
|
|
return [args.slice(0, -1), props.slice(0, -1), ctx.slice(0, -1)];
|
|
}
|
|
|
|
function paramStructure(parts, type) {
|
|
return '\n\t'
|
|
+ (type
|
|
? type + ':{'
|
|
: '')
|
|
+ 'args:[' + parts[0] + ']'
|
|
+ (parts[1] || !type
|
|
? ',\n\tprops:{' + parts[1] + '}'
|
|
: "")
|
|
+ (parts[2] ? ',\n\tctx:{' + parts[2] + '}' : "");
|
|
}
|
|
|
|
function parseParams(params, pathBindings, tmpl) {
|
|
|
|
function parseTokens(all, lftPrn0, lftPrn, bound, path, operator, err, eq, path2, prn, comma, lftPrn2, apos, quot, rtPrn, rtPrnDot, prn2, space, index, full) {
|
|
// /(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?=\s*[.^]|\s*$|[^([])|[)\]])([([]?))|(\s+)/g,
|
|
// lftPrn0 lftPrn bound path operator err eq path2 prn comma lftPrn2 apos quot rtPrn rtPrnDot prn2 space
|
|
// (left paren? followed by (path? followed by operator) or (path followed by paren?)) or comma or apos or quot or right paren or space
|
|
function parsePath(allPath, not, object, helper, view, viewProperty, pathTokens, leafToken) {
|
|
//rPath = /^(!*?)(?:null|true|false|\d[\d.]*|([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g,
|
|
// not object helper view viewProperty pathTokens leafToken
|
|
var subPath = object === ".";
|
|
if (object) {
|
|
path = path.slice(not.length);
|
|
if (/^\.?constructor$/.test(leafToken||path)) {
|
|
syntaxError(allPath);
|
|
}
|
|
if (!subPath) {
|
|
allPath = (helper
|
|
? 'view.ctxPrm("' + helper + '")'
|
|
: view
|
|
? "view"
|
|
: "data")
|
|
+ (leafToken
|
|
? (viewProperty
|
|
? "." + viewProperty
|
|
: helper
|
|
? ""
|
|
: (view ? "" : "." + object)
|
|
) + (pathTokens || "")
|
|
: (leafToken = helper ? "" : view ? viewProperty || "" : object, ""));
|
|
|
|
allPath = allPath + (leafToken ? "." + leafToken : "");
|
|
|
|
allPath = not + (allPath.slice(0, 9) === "view.data"
|
|
? allPath.slice(5) // convert #view.data... to data...
|
|
: allPath);
|
|
}
|
|
if (bindings) {
|
|
binds = named === "linkTo" ? (bindto = pathBindings._jsvto = pathBindings._jsvto || []) : bndCtx.bd;
|
|
if (theOb = subPath && binds[binds.length-1]) {
|
|
if (theOb._cpfn) { // Computed property exprOb
|
|
while (theOb.sb) {
|
|
theOb = theOb.sb;
|
|
}
|
|
if (theOb.bnd) {
|
|
path = "^" + path.slice(1);
|
|
}
|
|
theOb.sb = path;
|
|
theOb.bnd = theOb.bnd || path.charAt(0) === "^";
|
|
}
|
|
} else {
|
|
binds.push(path);
|
|
}
|
|
pathStart[parenDepth] = index + (subPath ? 1 : 0);
|
|
}
|
|
}
|
|
return allPath;
|
|
}
|
|
|
|
//bound = bindings && bound;
|
|
if (bound && !eq) {
|
|
path = bound + path; // e.g. some.fn(...)^some.path - so here path is "^some.path"
|
|
}
|
|
operator = operator || "";
|
|
lftPrn = lftPrn || lftPrn0 || lftPrn2;
|
|
path = path || path2;
|
|
// Could do this - but not worth perf cost?? :-
|
|
// if (!path.lastIndexOf("#data.", 0)) { path = path.slice(6); } // If path starts with "#data.", remove that.
|
|
prn = prn || prn2 || "";
|
|
|
|
var expr, exprFn, binds, theOb, newOb,
|
|
rtSq = ")";
|
|
|
|
if (prn === "[") {
|
|
prn ="[j._sq(";
|
|
rtSq = ")]";
|
|
}
|
|
|
|
if (err && !aposed && !quoted) {
|
|
syntaxError(params);
|
|
} else {
|
|
if (bindings && rtPrnDot && !aposed && !quoted) {
|
|
// This is a binding to a path in which an object is returned by a helper/data function/expression, e.g. foo()^x.y or (a?b:c)^x.y
|
|
// We create a compiled function to get the object instance (which will be called when the dependent data of the subexpression changes, to return the new object, and trigger re-binding of the subsequent path)
|
|
if (!named || boundName || bindto) {
|
|
expr = pathStart[parenDepth - 1];
|
|
if (full.length - 1 > index - (expr || 0)) { // We need to compile a subexpression
|
|
expr = full.slice(expr, index + all.length);
|
|
if (exprFn !== true) { // If not reentrant call during compilation
|
|
binds = bindto || bndStack[parenDepth-1].bd;
|
|
// Insert exprOb object, to be used during binding to return the computed object
|
|
theOb = binds[binds.length-1];
|
|
if (theOb && theOb.prm) {
|
|
while (theOb.sb && theOb.sb.prm) {
|
|
theOb = theOb.sb;
|
|
}
|
|
newOb = theOb.sb = {path: theOb.sb, bnd: theOb.bnd};
|
|
} else {
|
|
binds.push(newOb = {path: binds.pop()}); // Insert exprOb object, to be used during binding to return the computed object
|
|
} // (e.g. "some.object()" in "some.object().a.b" - to be used as context for binding the following tokens "a.b")
|
|
}
|
|
rtPrnDot = delimOpenChar1 + ":" + expr // The parameter or function subexpression
|
|
+ " onerror=''" // set onerror='' in order to wrap generated code with a try catch - returning '' as object instance if there is an error/missing parent
|
|
+ delimCloseChar0;
|
|
exprFn = tmplLinks[rtPrnDot];
|
|
if (!exprFn) {
|
|
tmplLinks[rtPrnDot] = true; // Flag that this exprFn (for rtPrnDot) is being compiled
|
|
tmplLinks[rtPrnDot] = exprFn = tmplFn(rtPrnDot, tmpl, true); // Compile the expression (or use cached copy already in tmpl.links)
|
|
}
|
|
if (exprFn !== true && newOb) {
|
|
// If not reentrant call during compilation
|
|
newOb._cpfn = exprFn;
|
|
newOb.prm = bndCtx.bd;
|
|
newOb.bnd = newOb.bnd || newOb.path && newOb.path.indexOf("^") >= 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return (aposed
|
|
// within single-quoted string
|
|
? (aposed = !apos, (aposed ? all : lftPrn2 + '"'))
|
|
: quoted
|
|
// within double-quoted string
|
|
? (quoted = !quot, (quoted ? all : lftPrn2 + '"'))
|
|
:
|
|
(
|
|
(lftPrn
|
|
? (pathStart[parenDepth] = index++, bndCtx = bndStack[++parenDepth] = {bd: []}, lftPrn)
|
|
: "")
|
|
+ (space
|
|
? (parenDepth
|
|
? ""
|
|
// New arg or prop - so insert backspace \b (\x08) as separator for named params, used subsequently by rBuildHash, and prepare new bindings array
|
|
: (paramIndex = full.slice(paramIndex, index), named
|
|
? (named = boundName = bindto = false, "\b")
|
|
: "\b,") + paramIndex + (paramIndex = index + all.length, bindings && pathBindings.push(bndCtx.bd = []), "\b")
|
|
)
|
|
: eq
|
|
// named param. Remove bindings for arg and create instead bindings array for prop
|
|
? (parenDepth && syntaxError(params), bindings && pathBindings.pop(), named = path, boundName = bound, paramIndex = index + all.length,
|
|
bindings && ((bindings = bndCtx.bd = pathBindings[named] = []), bindings.skp = !bound), path + ':')
|
|
: path
|
|
// path
|
|
? (path.split("^").join(".").replace(rPath, parsePath)
|
|
+ (prn
|
|
// some.fncall(
|
|
? (bndCtx = bndStack[++parenDepth] = {bd: []}, fnCall[parenDepth] = rtSq, prn)
|
|
: operator)
|
|
)
|
|
: operator
|
|
// operator
|
|
? operator
|
|
: rtPrn
|
|
// function
|
|
? ((rtPrn = fnCall[parenDepth] || rtPrn, fnCall[parenDepth] = false, bndCtx = bndStack[--parenDepth], rtPrn)
|
|
+ (prn // rtPrn and prn, e.g )( in (a)() or a()(), or )[ in a()[]
|
|
? (bndCtx = bndStack[++parenDepth], fnCall[parenDepth] = rtSq, prn)
|
|
: "")
|
|
)
|
|
: comma
|
|
? (fnCall[parenDepth] || syntaxError(params), ",") // We don't allow top-level literal arrays or objects
|
|
: lftPrn0
|
|
? ""
|
|
: (aposed = apos, quoted = quot, '"')
|
|
))
|
|
);
|
|
}
|
|
}
|
|
|
|
var named, bindto, boundName,
|
|
quoted, // boolean for string content in double quotes
|
|
aposed, // or in single quotes
|
|
bindings = pathBindings && pathBindings[0], // bindings array for the first arg
|
|
bndCtx = {bd: bindings},
|
|
bndStack = {0: bndCtx},
|
|
paramIndex = 0, // list,
|
|
tmplLinks = (tmpl ? tmpl.links : bindings && (bindings.links = bindings.links || {})) || topView.tmpl.links,
|
|
// The following are used for tracking path parsing including nested paths, such as "a.b(c^d + (e))^f", and chained computed paths such as
|
|
// "a.b().c^d().e.f().g" - which has four chained paths, "a.b()", "^c.d()", ".e.f()" and ".g"
|
|
parenDepth = 0,
|
|
fnCall = {}, // We are in a function call
|
|
pathStart = {}, // tracks the start of the current path such as c^d() in the above example
|
|
result = (params + (tmpl ? " " : "")).replace(rParams, parseTokens);
|
|
|
|
return !parenDepth && result || syntaxError(params); // Syntax error if unbalanced parens in params expression
|
|
}
|
|
|
|
function buildCode(ast, tmpl, isLinkExpr) {
|
|
// Build the template function code from the AST nodes, and set as property on the passed-in template object
|
|
// Used for compiling templates, and also by JsViews to build functions for data link expressions
|
|
var i, node, tagName, converter, tagCtx, hasTag, hasEncoder, getsVal, hasCnvt, useCnvt, tmplBindings, pathBindings, params, boundOnErrStart,
|
|
boundOnErrEnd, tagRender, nestedTmpls, tmplName, nestedTmpl, tagAndElses, content, markup, nextIsElse, oldCode, isElse, isGetVal, tagCtxFn,
|
|
onError, tagStart, trigger, lateRender,
|
|
tmplBindingKey = 0,
|
|
useViews = $subSettingsAdvanced.useViews || tmpl.useViews || tmpl.tags || tmpl.templates || tmpl.helpers || tmpl.converters,
|
|
code = "",
|
|
tmplOptions = {},
|
|
l = ast.length;
|
|
|
|
if ("" + tmpl === tmpl) {
|
|
tmplName = isLinkExpr ? 'data-link="' + tmpl.replace(rNewLine, " ").slice(1, -1) + '"' : tmpl;
|
|
tmpl = 0;
|
|
} else {
|
|
tmplName = tmpl.tmplName || "unnamed";
|
|
if (tmpl.allowCode) {
|
|
tmplOptions.allowCode = true;
|
|
}
|
|
if (tmpl.debug) {
|
|
tmplOptions.debug = true;
|
|
}
|
|
tmplBindings = tmpl.bnds;
|
|
nestedTmpls = tmpl.tmpls;
|
|
}
|
|
for (i = 0; i < l; i++) {
|
|
// AST nodes: [0: tagName, 1: converter, 2: content, 3: params, 4: code, 5: onError, 6: trigger, 7:pathBindings, 8: contentMarkup]
|
|
node = ast[i];
|
|
|
|
// Add newline for each callout to t() c() etc. and each markup string
|
|
if ("" + node === node) {
|
|
// a markup string to be inserted
|
|
code += '\n+"' + node + '"';
|
|
} else {
|
|
// a compiled tag expression to be inserted
|
|
tagName = node[0];
|
|
if (tagName === "*") {
|
|
// Code tag: {{* }}
|
|
code += ";\n" + node[1] + "\nret=ret";
|
|
} else {
|
|
converter = node[1];
|
|
content = !isLinkExpr && node[2];
|
|
tagCtx = paramStructure(node[3], 'params') + '},' + paramStructure(params = node[4]);
|
|
onError = node[5];
|
|
trigger = node[6];
|
|
lateRender = node[7];
|
|
markup = node[9] && node[9].replace(rUnescapeQuotes, "$1");
|
|
if (isElse = tagName === "else") {
|
|
if (pathBindings) {
|
|
pathBindings.push(node[8]);
|
|
}
|
|
} else if (tmplBindings && (pathBindings = node[8])) { // Array of paths, or false if not data-bound
|
|
pathBindings = [pathBindings];
|
|
tmplBindingKey = tmplBindings.push(1); // Add placeholder in tmplBindings for compiled function
|
|
}
|
|
useViews = useViews || params[1] || params[2] || pathBindings || /view.(?!index)/.test(params[0]);
|
|
// useViews is for perf optimization. For render() we only use views if necessary - for the more advanced scenarios.
|
|
// We use views if there are props, contextual properties or args with #... (other than #index) - but you can force
|
|
// using the full view infrastructure, (and pay a perf price) by opting in: Set useViews: true on the template, manually...
|
|
if (isGetVal = tagName === ":") {
|
|
if (converter) {
|
|
tagName = converter === HTML ? ">" : converter + tagName;
|
|
}
|
|
} else {
|
|
if (content) { // TODO optimize - if content.length === 0 or if there is a tmpl="..." specified - set content to null / don't run this compilation code - since content won't get used!!
|
|
// Create template object for nested template
|
|
nestedTmpl = tmplObject(markup, tmplOptions);
|
|
nestedTmpl.tmplName = tmplName + "/" + tagName;
|
|
// Compile to AST and then to compiled function
|
|
nestedTmpl.useViews = nestedTmpl.useViews || useViews;
|
|
buildCode(content, nestedTmpl);
|
|
useViews = nestedTmpl.useViews;
|
|
nestedTmpls.push(nestedTmpl);
|
|
}
|
|
|
|
if (!isElse) {
|
|
// This is not an else tag.
|
|
tagAndElses = tagName;
|
|
useViews = useViews || tagName && (!$tags[tagName] || !$tags[tagName].flow);
|
|
// Switch to a new code string for this bound tag (and its elses, if it has any) - for returning the tagCtxs array
|
|
oldCode = code;
|
|
code = "";
|
|
}
|
|
nextIsElse = ast[i + 1];
|
|
nextIsElse = nextIsElse && nextIsElse[0] === "else";
|
|
}
|
|
tagStart = onError ? ";\ntry{\nret+=" : "\n+";
|
|
boundOnErrStart = "";
|
|
boundOnErrEnd = "";
|
|
|
|
if (isGetVal && (pathBindings || trigger || converter && converter !== HTML || lateRender)) {
|
|
// For convertVal we need a compiled function to return the new tagCtx(s)
|
|
tagCtxFn = new Function("data,view,j,u", "// " + tmplName + " " + (++tmplBindingKey) + " " + tagName
|
|
+ "\nreturn {" + tagCtx + "};");
|
|
tagCtxFn._er = onError;
|
|
tagCtxFn._tag = tagName;
|
|
tagCtxFn._bd = !!pathBindings; // data-linked tag {^{.../}}
|
|
tagCtxFn._lr = lateRender;
|
|
|
|
if (isLinkExpr) {
|
|
return tagCtxFn;
|
|
}
|
|
|
|
setPaths(tagCtxFn, pathBindings);
|
|
tagRender = 'c("' + converter + '",view,';
|
|
useCnvt = true;
|
|
boundOnErrStart = tagRender + tmplBindingKey + ",";
|
|
boundOnErrEnd = ")";
|
|
}
|
|
code += (isGetVal
|
|
? (isLinkExpr ? (onError ? "try{\n" : "") + "return " : tagStart) + (useCnvt // Call _cnvt if there is a converter: {{cnvt: ... }} or {^{cnvt: ... }}
|
|
? (useCnvt = undefined, useViews = hasCnvt = true, tagRender + (tagCtxFn
|
|
? ((tmplBindings[tmplBindingKey - 1] = tagCtxFn), tmplBindingKey) // Store the compiled tagCtxFn in tmpl.bnds, and pass the key to convertVal()
|
|
: "{" + tagCtx + "}") + ")")
|
|
: tagName === ">"
|
|
? (hasEncoder = true, "h(" + params[0] + ")")
|
|
: (getsVal = true, "((v=" + params[0] + ')!=null?v:' + (isLinkExpr ? 'null)' : '"")'))
|
|
// Non strict equality so data-link="title{:expr}" with expr=null/undefined removes title attribute
|
|
)
|
|
: (hasTag = true, "\n{view:view,tmpl:" // Add this tagCtx to the compiled code for the tagCtxs to be passed to renderTag()
|
|
+ (content ? nestedTmpls.length : "0") + "," // For block tags, pass in the key (nestedTmpls.length) to the nested content template
|
|
+ tagCtx + "},"));
|
|
|
|
if (tagAndElses && !nextIsElse) {
|
|
// This is a data-link expression or an inline tag without any elses, or the last {{else}} of an inline tag
|
|
// We complete the code for returning the tagCtxs array
|
|
code = "[" + code.slice(0, -1) + "]";
|
|
tagRender = 't("' + tagAndElses + '",view,this,';
|
|
if (isLinkExpr || pathBindings) {
|
|
// This is a bound tag (data-link expression or inline bound tag {^{tag ...}}) so we store a compiled tagCtxs function in tmp.bnds
|
|
code = new Function("data,view,j,u", " // " + tmplName + " " + tmplBindingKey + " " + tagAndElses + "\nreturn " + code + ";");
|
|
code._er = onError;
|
|
code._tag = tagAndElses;
|
|
if (pathBindings) {
|
|
setPaths(tmplBindings[tmplBindingKey - 1] = code, pathBindings);
|
|
}
|
|
code._lr = lateRender;
|
|
if (isLinkExpr) {
|
|
return code; // For a data-link expression we return the compiled tagCtxs function
|
|
}
|
|
boundOnErrStart = tagRender + tmplBindingKey + ",undefined,";
|
|
boundOnErrEnd = ")";
|
|
}
|
|
|
|
// This is the last {{else}} for an inline tag.
|
|
// For a bound tag, pass the tagCtxs fn lookup key to renderTag.
|
|
// For an unbound tag, include the code directly for evaluating tagCtxs array
|
|
code = oldCode + tagStart + tagRender + (code.deps && tmplBindingKey || code) + ")";
|
|
pathBindings = 0;
|
|
tagAndElses = 0;
|
|
}
|
|
if (onError) {
|
|
useViews = true;
|
|
code += ';\n}catch(e){ret' + (isLinkExpr ? "urn " : "+=") + boundOnErrStart + 'j._err(e,view,' + onError + ')' + boundOnErrEnd + ';}' + (isLinkExpr ? "" : 'ret=ret');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Include only the var references that are needed in the code
|
|
code = "// " + tmplName
|
|
|
|
+ "\nvar v"
|
|
+ (hasTag ? ",t=j._tag" : "") // has tag
|
|
+ (hasCnvt ? ",c=j._cnvt" : "") // converter
|
|
+ (hasEncoder ? ",h=j._html" : "") // html converter
|
|
+ (isLinkExpr ? ";\n" : ',ret=""\n')
|
|
+ (tmplOptions.debug ? "debugger;" : "")
|
|
+ code
|
|
+ (isLinkExpr ? "\n" : ";\nreturn ret;");
|
|
|
|
if ($subSettings.debugMode !== false) {
|
|
code = "try {\n" + code + "\n}catch(e){\nreturn j._err(e, view);\n}";
|
|
}
|
|
|
|
try {
|
|
code = new Function("data,view,j,u", code);
|
|
} catch (e) {
|
|
syntaxError("Compiled template code:\n\n" + code + '\n: "' + (e.message||e) + '"');
|
|
}
|
|
if (tmpl) {
|
|
tmpl.fn = code;
|
|
tmpl.useViews = !!useViews;
|
|
}
|
|
return code;
|
|
}
|
|
|
|
//==========
|
|
// Utilities
|
|
//==========
|
|
|
|
// Merge objects, in particular contexts which inherit from parent contexts
|
|
function extendCtx(context, parentContext) {
|
|
// Return copy of parentContext, unless context is defined and is different, in which case return a new merged context
|
|
// If neither context nor parentContext are defined, return undefined
|
|
return context && context !== parentContext
|
|
? (parentContext
|
|
? $extend($extend({}, parentContext), context)
|
|
: context)
|
|
: parentContext && $extend({}, parentContext);
|
|
}
|
|
|
|
// Get character entity for HTML and Attribute encoding
|
|
function getCharEntity(ch) {
|
|
return charEntities[ch] || (charEntities[ch] = "&#" + ch.charCodeAt(0) + ";");
|
|
}
|
|
|
|
function getTargetProps(source) {
|
|
// this pointer is theMap - which has tagCtx.props too
|
|
// arguments: tagCtx.args.
|
|
var key, prop,
|
|
props = [];
|
|
|
|
if (typeof source === OBJECT) {
|
|
for (key in source) {
|
|
prop = source[key];
|
|
if (key !== $expando && source.hasOwnProperty(key) && !$isFunction(prop)) {
|
|
props.push({key: key, prop: prop});
|
|
}
|
|
}
|
|
}
|
|
return props;
|
|
}
|
|
|
|
function $fnRender(data, context, noIteration) {
|
|
var tmplElem = this.jquery && (this[0] || error('Unknown template')), // Targeted element not found for jQuery template selector such as "#myTmpl"
|
|
tmpl = tmplElem.getAttribute(tmplAttr);
|
|
|
|
return renderContent.call(tmpl && $.data(tmplElem)[jsvTmpl] || $templates(tmplElem),
|
|
data, context, noIteration);
|
|
}
|
|
|
|
//========================== Register converters ==========================
|
|
|
|
function htmlEncode(text) {
|
|
// HTML encode: Replace < > & ' and " by corresponding entities.
|
|
return text != undefined ? rIsHtml.test(text) && ("" + text).replace(rHtmlEncode, getCharEntity) || text : "";
|
|
}
|
|
|
|
//========================== Initialize ==========================
|
|
|
|
$sub = $views.sub;
|
|
$viewsSettings = $views.settings;
|
|
|
|
if (!(jsr || $ && $.render)) {
|
|
// JsRender not already loaded, or loaded without jQuery, and we are now moving from jsrender namespace to jQuery namepace
|
|
for (jsvStoreName in jsvStores) {
|
|
registerStore(jsvStoreName, jsvStores[jsvStoreName]);
|
|
}
|
|
|
|
$converters = $views.converters;
|
|
$helpers = $views.helpers;
|
|
$tags = $views.tags;
|
|
|
|
$sub._tg.prototype = {
|
|
baseApply: baseApply,
|
|
cvtArgs: convertArgs,
|
|
bndArgs: convertBoundArgs,
|
|
ctxPrm: contextParameter
|
|
};
|
|
|
|
topView = $sub.topView = new View();
|
|
|
|
//BROWSER-SPECIFIC CODE
|
|
if ($) {
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// jQuery (= $) is loaded
|
|
|
|
$.fn.render = $fnRender;
|
|
$expando = $.expando;
|
|
if ($.observable) {
|
|
$extend($sub, $.views.sub); // jquery.observable.js was loaded before jsrender.js
|
|
$views.map = $.views.map;
|
|
}
|
|
|
|
} else {
|
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// jQuery is not loaded.
|
|
|
|
$ = {};
|
|
|
|
if (setGlobals) {
|
|
global.jsrender = $; // We are loading jsrender.js from a script element, not AMD or CommonJS, so set global
|
|
}
|
|
|
|
// Error warning if jsrender.js is used as template engine on Node.js (e.g. Express or Hapi...)
|
|
// Use jsrender-node.js instead...
|
|
$.renderFile = $.__express = $.compile = function() { throw "Node.js: use npm jsrender, or jsrender-node.js"; };
|
|
|
|
//END BROWSER-SPECIFIC CODE
|
|
$.isFunction = function(ob) {
|
|
return typeof ob === "function";
|
|
};
|
|
|
|
$.isArray = Array.isArray || function(obj) {
|
|
return ({}.toString).call(obj) === "[object Array]";
|
|
};
|
|
|
|
$sub._jq = function(jq) { // private method to move from JsRender APIs from jsrender namespace to jQuery namespace
|
|
if (jq !== $) {
|
|
$extend(jq, $); // map over from jsrender namespace to jQuery namespace
|
|
$ = jq;
|
|
$.fn.render = $fnRender;
|
|
delete $.jsrender;
|
|
$expando = $.expando;
|
|
}
|
|
};
|
|
|
|
$.jsrender = versionNumber;
|
|
}
|
|
$subSettings = $sub.settings;
|
|
$subSettings.allowCode = false;
|
|
$isFunction = $.isFunction;
|
|
$.render = $render;
|
|
$.views = $views;
|
|
$.templates = $templates = $views.templates;
|
|
|
|
for (setting in $subSettings) {
|
|
addSetting(setting);
|
|
}
|
|
|
|
($viewsSettings.debugMode = function(debugMode) {
|
|
return debugMode === undefined
|
|
? $subSettings.debugMode
|
|
: (
|
|
$subSettings.debugMode = debugMode,
|
|
$subSettings.onError = debugMode + "" === debugMode
|
|
? new Function("", "return '" + debugMode + "';")
|
|
: $isFunction(debugMode)
|
|
? debugMode
|
|
: undefined,
|
|
$viewsSettings);
|
|
})(false); // jshint ignore:line
|
|
|
|
$subSettingsAdvanced = $subSettings.advanced = {
|
|
useViews: false,
|
|
_jsv: false // For global access to JsViews store
|
|
};
|
|
|
|
//========================== Register tags ==========================
|
|
|
|
$tags({
|
|
"if": {
|
|
render: function(val) {
|
|
// This function is called once for {{if}} and once for each {{else}}.
|
|
// We will use the tag.rendering object for carrying rendering state across the calls.
|
|
// If not done (a previous block has not been rendered), look at expression for this block and render the block if expression is truthy
|
|
// Otherwise return ""
|
|
var self = this,
|
|
tagCtx = self.tagCtx,
|
|
ret = (self.rendering.done || !val && (arguments.length || !tagCtx.index))
|
|
? ""
|
|
: (self.rendering.done = true, self.selected = tagCtx.index,
|
|
// Test is satisfied, so render content on current context. We call tagCtx.render() rather than return undefined
|
|
// (which would also render the tmpl/content on the current context but would iterate if it is an array)
|
|
tagCtx.render(tagCtx.view, true)); // no arg, so renders against parentView.data
|
|
return ret;
|
|
},
|
|
flow: true
|
|
},
|
|
"for": {
|
|
render: function(val) {
|
|
// This function is called once for {{for}} and once for each {{else}}.
|
|
// We will use the tag.rendering object for carrying rendering state across the calls.
|
|
var finalElse = !arguments.length,
|
|
value,
|
|
self = this,
|
|
tagCtx = self.tagCtx,
|
|
result = "",
|
|
done = 0;
|
|
|
|
if (!self.rendering.done) {
|
|
value = finalElse ? tagCtx.view.data : val; // For the final else, defaults to current data without iteration.
|
|
if (value !== undefined) {
|
|
result += tagCtx.render(value, finalElse); // Iterates except on final else, if data is an array. (Use {{include}} to compose templates without array iteration)
|
|
done += $isArray(value) ? value.length : 1;
|
|
}
|
|
if (self.rendering.done = done) {
|
|
self.selected = tagCtx.index;
|
|
}
|
|
// If nothing was rendered we will look at the next {{else}}. Otherwise, we are done.
|
|
}
|
|
return result;
|
|
},
|
|
flow: true
|
|
},
|
|
props: {
|
|
baseTag: "for",
|
|
dataMap: dataMap(getTargetProps),
|
|
flow: true
|
|
},
|
|
include: {
|
|
flow: true
|
|
},
|
|
"*": {
|
|
// {{* code... }} - Ignored if template.allowCode and $.views.settings.allowCode are false. Otherwise include code in compiled template
|
|
render: retVal,
|
|
flow: true
|
|
},
|
|
":*": {
|
|
// {{:* returnedExpression }} - Ignored if template.allowCode and $.views.settings.allowCode are false. Otherwise include code in compiled template
|
|
render: retVal,
|
|
flow: true
|
|
},
|
|
dbg: $helpers.dbg = $converters.dbg = dbgBreak // Register {{dbg/}}, {{dbg:...}} and ~dbg() to throw and catch, as breakpoints for debugging.
|
|
});
|
|
|
|
$converters({
|
|
html: htmlEncode,
|
|
attr: htmlEncode, // Includes > encoding since rConvertMarkers in JsViews does not skip > characters in attribute strings
|
|
url: function(text) {
|
|
// URL encoding helper.
|
|
return text != undefined ? encodeURI("" + text) : text === null ? text : ""; // null returns null, e.g. to remove attribute. undefined returns ""
|
|
}
|
|
});
|
|
}
|
|
//========================== Define default delimiters ==========================
|
|
$subSettings = $sub.settings;
|
|
$isArray = ($||jsr).isArray;
|
|
$viewsSettings.delimiters("{{", "}}", "^");
|
|
|
|
|
|
if (jsrToJq) { // Moving from jsrender namespace to jQuery namepace - copy over the stored items (templates, converters, helpers...)
|
|
jsr.views.sub._jq($);
|
|
}
|
|
return $ || jsr;
|
|
}, window));
|