/*! jquery.views.js v0.9.78 (Beta): http://jsviews.com/ */
/*
* Interactive data-driven views using JsRender templates.
* Subcomponent of JsViews
* Requires jQuery and jsrender.js (Best-of-breed templating in browser or on Node.js)
* See JsRender at http://jsviews.com/#download and http://github.com/BorisMoore/jsrender
* Also requires jquery.observable.js
* See JsObservable at http://jsviews.com/#download and http://github.com/BorisMoore/jsviews
*
* Copyright 2016, Boris Moore
* Released under the MIT License.
*/
//jshint -W018, -W041
(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 jQuery passed as parameter (with JsRender and JsObservable): require("jquery.views")(jQuery)
return factory(global, $);
};
} else if (typeof define === "function" && define.amd) { // AMD script loader, e.g. RequireJS
define(["jquery", "./jsrender", "./jquery.observable"], function($, jsr, jso) {
return factory(global, $, jsr, jso);
}); // Require jQuery, JsRender, JsObservable
} else { // Browser using plain ',
openScript = ' - data-linked tag, close marker
// We validate with inTag so no script markers are inserted in attribute context e.g. for:
// "
" or "
...{{/if}}..."
preceding = id
? (preceding + endOfElCnt + spaceBefore + (inTag ? "" : openScript + id + closeScript)+ spaceAfter + tag)
: endOfElCnt || all;
}
if (validate && boundId) {
if (inTag) {
// JsViews data-linking tags are not allowed within element markup.
// See jsviews/issues/303
syntaxError('{^{ within elem markup (' + inTag + ' ). Use data-link="..."');
}
if (id.charAt(0) === "#") {
tagStack.unshift(id.slice(1));
} else if (id.slice(1) !== (bndId = tagStack.shift())) {
// See jsviews/issues/213
syntaxError('Closing tag for {^{...}} under different elem: <' + bndId + '>');
}
}
if (tag) {
inTag = tag;
// If there are ids (markers since the last tag), move them to the defer string
tagStack.unshift(parentTag);
parentTag = tag.slice(1);
if (validate && tagStack[0] && tagStack[0] === badParent[parentTag]) {
// Missing
// TODO: replace this by smart insertion of tags
error('Parent of
must be
');
}
isVoid = voidElems[parentTag];
if ((elCnt = elContent[parentTag]) && !prevElCnt) {
deferStack.unshift(defer);
defer = "";
}
prevElCnt = elCnt;
//TODO Consider providing validation which throws if you place as child of
, etc. - since if not caught,
//this can cause errors subsequently which are difficult to debug.
// if (elContent[tagStack[0]]>2 && !elCnt) {
// error(parentTag + " in " + tagStack[0]);
// }
if (defer && elCnt) {
defer += "+"; // Will be used for stepping back through deferred tokens
}
}
return preceding;
}
function processViewInfos(vwInfos, targetParent) {
// If targetParent, we are processing viewInfos (which may include navigation through '+-' paths) and hooking up to the right parentElem etc.
// (and elem may also be defined - the next node)
// If no targetParent, then we are processing viewInfos on newly inserted content
var deferPath, deferChar, bindChar, parentElem, id, onAftCr, deep,
addedBindEls = [];
// In elCnt context (element-only content model), prevNode is the first node after the open, nextNode is the first node after the close.
// If both are null/undefined, then open and close are at end of parent content, so the view is empty, and its placeholder is the
// 'lastChild' of the parentNode. If there is a prevNode, then it is either the first node in the view, or the view is empty and
// its placeholder is the 'previousSibling' of the prevNode, which is also the nextNode.
if (vwInfos) {
if (vwInfos._tkns.charAt(0) === "@") {
// We are processing newly inserted content. This is a special script element that was created in convertMarkers() to process deferred bindings,
// and inserted following the target parent element - because no element tags (outside elCnt) were encountered to carry those binding tokens.
// We will step back from the preceding sibling of this element, looking at targetParent elements until we find the one that the current binding
// token belongs to. Set elem to null (the special script element), and remove it from the DOM.
targetParent = elem.previousSibling;
elem.parentNode.removeChild(elem);
elem = undefined;
}
len = vwInfos.length;
while (len--) {
vwInfo = vwInfos[len];
//if (prevIds.indexOf(vwInfo.token) < 0) { // This token is a newly created view or tag binding
bindChar = vwInfo.ch;
if (deferPath = vwInfo.path) {
// We have a 'deferred path'
j = deferPath.length - 1;
while (deferChar = deferPath.charAt(j--)) {
// Use the "+" and"-" characters to navigate the path back to the original parent node where the deferred bindings ocurred
if (deferChar === "+") {
if (deferPath.charAt(j) === "-") {
j--;
targetParent = targetParent.previousSibling;
} else {
targetParent = targetParent.parentNode;
}
} else {
targetParent = targetParent.lastChild;
}
// Note: Can use previousSibling and lastChild, not previousElementSibling and lastElementChild,
// since we have removed white space within elCnt. Hence support IE < 9
}
}
if (bindChar === "^") {
if (tag = bindingStore[id = vwInfo.id]) {
// The binding may have been deleted, for example in a different handler to an array collectionChange event
// This is a tag binding
deep = targetParent && (!elem || elem.parentNode !== targetParent); // We are stepping back looking for the right targetParent,
// or we are linking existing content and this element is in elCnt, not an immediate child of the targetParent.
if (!elem || deep) {
tag.parentElem = targetParent;
}
if (vwInfo.elCnt && deep) {
// With element only content, if there is no following element, or if the binding is deeper than the following element
// then we need to set the open or close token as a deferred binding annotation on the parent
setDefer(targetParent, (vwInfo.open ? "#" : "/") + id + bindChar + (targetParent._df || ""));
}
// This is an open or close marker for a data-linked tag {^{...}}. Add it to bindEls.
addedBindEls.push([deep ? null : elem, vwInfo]);
}
} else if (view = viewStore[id = vwInfo.id]) {
// The view may have been deleted, for example in a different handler to an array collectionChange event
if (!view.parentElem) {
// If view is not already extended for JsViews, extend and initialize the view object created in JsRender, as a JsViews view
view.parentElem = targetParent || elem && elem.parentNode || parentNode;
view._.onRender = addBindingMarkers;
view._.onArrayChange = arrayChangeHandler;
setArrayChangeLink(view);
}
parentElem = view.parentElem;
if (vwInfo.open) {
// This is an 'open view' node (preceding script marker node,
// or if elCnt, the first element in the view, with a data-jsv annotation) for binding
view._elCnt = vwInfo.elCnt;
if (targetParent && !elem) {
setDefer(targetParent, "#" + id + bindChar + (targetParent._df || ""));
} else {
// No targetParent, so there is a ._nxt elem (and this is processing tokens on the elem)
if (!view._prv) {
setDefer(parentElem, removeSubStr(parentElem._df, "#" + id + bindChar));
}
view._prv = elem;
}
} else {
// This is a 'close view' marker node for binding
if (targetParent && (!elem || elem.parentNode !== targetParent)) {
// There is no ._nxt so add token to _df. It is deferred.
setDefer(targetParent, "/" + id + bindChar + (targetParent._df || ""));
view._nxt = undefined;
} else if (elem) {
// This view did not have a ._nxt, but has one now, so token may be in _df, and must be removed. (No longer deferred)
if (!view._nxt) {
setDefer(parentElem, removeSubStr(parentElem._df, "/" + id + bindChar));
}
view._nxt = elem;
}
linkCtx = view.linkCtx;
if (onAftCr = view.ctx && view.ctx.onAfterCreate || onAfterCreate) {
onAftCr.call(linkCtx, view);
}
}
//}
}
}
len = addedBindEls.length;
while (len--) {
// These were added in reverse order to addedBindEls. We push them in BindEls in the correct order.
bindEls.push(addedBindEls[len]);
}
}
return !vwInfos || vwInfos.elCnt;
}
function getViewInfos(vwInfos) {
// Used by view.childTags() and tag.childTags()
// Similar to processViewInfos in how it steps through bindings to find tags. Only finds data-linked tags.
var level, parentTag, named;
if (vwInfos) {
len = vwInfos.length;
for (j = 0; j < len; j++) {
vwInfo = vwInfos[j];
// This is an open marker for a data-linked tag {^{...}}, within the content of the tag whose id is get.id. Add it to bindEls.
// Note - if bindingStore[vwInfo.id]._is === "tag" then getViewInfos is being called too soon - during first linking pass
parentTag = tag = bindingStore[vwInfo.id].linkCtx.tag;
named = tag.tagName === tagName;
if (!tag.flow || named) {
if (!deep) {
level = 1;
while (parentTag = parentTag.parent) {
level++;
}
tagDepth = tagDepth || level; // The level of the first tag encountered.
}
if ((deep || level === tagDepth) && (!tagName || named)) {
// Filter on top-level or tagName as appropriate
tags.push(tag);
}
}
}
}
}
function dataLink() {
//================ Data-link and fixup of data-jsv annotations ================
var j, index,
tokens = "",
wrap = {},
selector = linkViewsSel + (get ? ",[" + deferAttr + "]" : "");
// If a childTags() call, get = ",[" + deferAttr + "]" - since we need to include elements that have a ._df expando for deferred tokens
elems = qsa ? parentNode.querySelectorAll(selector) : $(selector, parentNode).get();
l = elems.length;
// The prevNode will be in the returned query, since we called markPrevOrNextNode() on it.
// But it may have contained nodes that satisfy the selector also.
if (prevNode && prevNode.innerHTML) {
// Find the last contained node of prevNode, to use as the prevNode - so we only link subsequent elems in the query
prevNodes = qsa ? prevNode.querySelectorAll(selector) : $(selector, prevNode).get();
prevNode = prevNodes.length ? prevNodes[prevNodes.length - 1] : prevNode;
}
tagDepth = 0;
for (i = 0; i < l; i++) {
elem = elems[i];
if (prevNode && !found) {
// If prevNode is set, not false, skip linking. If this element is the prevNode, set to false so subsequent elements will link.
found = (elem === prevNode);
} else if (nextNode && elem === nextNode) {
// If nextNode is set then break when we get to nextNode
if (get) {
tokens += markerNodeInfo(elem);
}
break;
} else if (elem.parentNode) {
// elem has not been removed from DOM
if (get) {
tokens += markerNodeInfo(elem);
if (elem._df) {
j = i + 1;
while (j < l && elem.contains(elems[j])) {
j++;
}
// Add defered tokens after any tokens on descendant elements of this one
wrap[j-1] = elem._df;
}
if (wrap[i]) {
tokens += wrap[i] || "";
}
} else {
if (isLink && (vwInfo = viewInfos(elem, undefined, rViewMarkers)) && (vwInfo = vwInfo[0])) {
// If this is a link(trueOrString ...) call we will avoid re-binding to elems that are within template-rendered views
skip = skip ? (vwInfo.id !== skip && skip) : vwInfo.open && vwInfo.id;
}
if (!skip && processInfos(viewInfos(elem))
// If a link() call, processViewInfos() adds bindings to bindEls, and returns true for non-script nodes, for adding data-link bindings
// If a childTags() call, getViewInfos returns array of tag bindings.
&& elem.getAttribute($viewsLinkAttr)) {
bindEls.push([elem]); // A data-linked element so add to bindEls too
}
}
}
}
if (get) {
tokens += parentNode._df || "";
if (index = tokens.indexOf("#" + get.id) + 1) {
// We are looking for view.childTags() or tag.childTags() - so start after the open token of the parent view or tag.
tokens = tokens.slice(index + get.id.length);
}
index = tokens.indexOf("/" + get.id);
if (index + 1) {
// We are looking for view.childTags() or tag.childTags() - so don't look beyond the close token of the parent view or tag.
tokens = tokens.slice(0, index);
}
// Call getViewInfos to add the found childTags to the tags array
getViewInfos(viewInfos(tokens, undefined, rOpenTagMarkers));
}
if (html === undefined && parentNode.getAttribute($viewsLinkAttr)) {
bindEls.push([parentNode]); // Support data-linking top-level element directly (not within a data-linked container)
}
// Remove temporary marker script nodes they were added by markPrevOrNextNode
unmarkPrevOrNextNode(prevNode, elCnt);
unmarkPrevOrNextNode(nextNode, elCnt);
if (get) {
if (lazyLink) {
lazyLink.resolve();
}
return; // We have added childTags to the tags array, so we are done
}
if (elCnt && defer + ids) {
// There are some views with elCnt, for which the open or close did not precede any HTML tag - so they have not been processed yet
elem = nextNode;
if (defer) {
if (nextNode) {
processViewInfos(viewInfos(defer + "+", true), nextNode);
} else {
processViewInfos(viewInfos(defer, true), parentNode);
}
}
processViewInfos(viewInfos(ids, true), parentNode);
// If there were any tokens on nextNode which have now been associated with inserted HTML tags, remove them from nextNode
if (nextNode) {
tokens = nextNode.getAttribute(jsvAttrStr);
if (l = tokens.indexOf(prevIds) + 1) {
tokens = tokens.slice(l + prevIds.length - 1);
}
nextNode.setAttribute(jsvAttrStr, ids + tokens);
}
}
//================ Bind the data-linked elements and tags ================
l = bindEls.length;
for (i = 0; i < l; i++) {
elem = bindEls[i];
linkInfo = elem[1];
elem = elem[0];
if (linkInfo) {
if (tag = bindingStore[linkInfo.id]) {
if (linkCtx = tag.linkCtx) {
// The tag may have been stored temporarily on the bindingStore - or may have already been replaced by the actual binding
tag = linkCtx.tag;
tag.linkCtx = linkCtx;
}
if (linkInfo.open) {
// This is an 'open linked tag' binding annotation for a data-linked tag {^{...}}
if (elem) {
tag.parentElem = elem.parentNode;
tag._prv = elem;
}
tag._elCnt = linkInfo.elCnt;
if (tag.onBeforeLink) {
tag.onBeforeLink();
}
// We data-link depth-first ("on the way in"), which is better for perf - and allows setting parent tags etc.
view = tag.tagCtx.view;
addDataBinding(undefined, tag._prv, view, linkInfo.id);
} else {
tag._nxt = elem;
if (tag._.unlinked) {
// This is a 'close linked tag' binding annotation
// Add data binding
tagCtx = tag.tagCtx;
view = tagCtx.view;
callAfterLink(tag);
}
}
}
} else {
// Add data binding for a data-linked element (with data-link attribute)
addDataBinding(elem.getAttribute($viewsLinkAttr), elem, $view(elem), undefined, isLink, outerData, context);
}
}
if (lazyLink) {
lazyLink.resolve();
}
}
//==== /end of nested functions ====
var inTag, linkCtx, tag, i, l, j, len, elems, elem, view, vwInfo, linkInfo, prevNodes, token, prevView, nextView,
node, tags, deep, tagName, tagCtx, validate, tagDepth, depth, fragment, copiedNode, firstTag, parentTag,
isVoid, wrapper, div, tokens, elCnt, prevElCnt, htmlTag, ids, prevIds, found, skip, lazyLink, isLink, get,
self = this,
thisId = self._.id + "_",
defer = "",
// The marker ids for which no tag was encountered (empty views or final closing markers) which we carry over to container tag
bindEls = [],
tagStack = [],
deferStack = [],
onAfterCreate = self.hlp(onAfterCreateStr),
processInfos = processViewInfos;
if (refresh) {
lazyLink = refresh.lazyLink && $.Deferred();
if (refresh.tmpl) {
// refresh is the prevView, passed in from addViews()
prevView = "/" + refresh._.id + "_";
} else {
isLink = refresh.lnk; // Top-level linking
if (refresh.tag) {
thisId = refresh.tag + "^";
refresh = true;
}
if (get = refresh.get) {
processInfos = getViewInfos;
tags = get.tags;
deep = get.deep;
tagName = get.name;
}
}
refresh = refresh === true;
}
parentNode = parentNode
? ("" + parentNode === parentNode
? $(parentNode)[0] // It is a string, so treat as selector
: parentNode.jquery
? parentNode[0] // A jQuery object - take first element.
: parentNode)
: (self.parentElem // view.link()
|| document.body); // link(null, data) to link the whole document
validate = !$subSettingsAdvanced.noValidate && parentNode.contentEditable !== TRUE;
parentTag = parentNode.tagName.toLowerCase();
elCnt = !!elContent[parentTag];
prevNode = prevNode && markPrevOrNextNode(prevNode, elCnt);
nextNode = nextNode && markPrevOrNextNode(nextNode, elCnt) || null;
if (html != undefined) {
//================ Insert html into DOM using documentFragments (and wrapping HTML appropriately). ================
// Also convert markers to DOM annotations, based on content model.
// Corresponds to nextNode ? $(nextNode).before(html) : $(parentNode).html(html);
// but allows insertion to wrap correctly even with inserted script nodes. jQuery version will fail e.g. under tbody or select.
// This version should also be slightly faster
div = document.createElement("div");
wrapper = div;
prevIds = ids = "";
htmlTag = parentNode.namespaceURI === "http://www.w3.org/2000/svg" ? "svg_ns" : (firstTag = rFirstElem.exec(html)) && firstTag[1] || "";
if (elCnt) {
// Now look for following view, and find its tokens, or if not found, get the parentNode._df tokens
node = nextNode;
while (node && !(nextView = viewInfos(node))) {
node = node.nextSibling;
}
if (tokens = nextView ? nextView._tkns : parentNode._df) {
token = prevView || "";
if (refresh || !prevView) {
token += "#" + thisId;
}
j = tokens.indexOf(token);
if (j + 1) {
j += token.length;
// Transfer the initial tokens to inserted nodes, by setting them as the ids variable, picked up in convertMarkers
prevIds = ids = tokens.slice(0, j);
tokens = tokens.slice(j);
if (nextView) {
node.setAttribute(jsvAttrStr, tokens);
} else {
setDefer(parentNode, tokens);
}
}
}
}
//================ Convert the markers to DOM annotations, based on content model. ================
// oldElCnt = elCnt;
isVoid = undefined;
html = ("" + html).replace(rConvertMarkers, convertMarkers);
// if (!!oldElCnt !== !!elCnt) {
// error("Parse: " + html); // Parse error. Content not well-formed?
// }
if (validate && tagStack.length) {
syntaxError("Mismatched '<" + parentTag + "...>' in:\n" + html); // Unmatched tag
}
if (validateOnly) {
return;
}
// Append wrapper element to doc fragment
safeFragment.appendChild(div);
// Go to html and back, then peel off extra wrappers
// Corresponds to jQuery $(nextNode).before(html) or $(parentNode).html(html);
// but supports svg elements, and other features missing from jQuery version (and this version should also be slightly faster)
htmlTag = wrapMap[htmlTag] || wrapMap.div;
depth = htmlTag[0];
wrapper.innerHTML = htmlTag[1] + html + htmlTag[2];
while (depth--) {
wrapper = wrapper.lastChild;
}
safeFragment.removeChild(div);
fragment = document.createDocumentFragment();
while (copiedNode = wrapper.firstChild) {
fragment.appendChild(copiedNode);
}
// Insert into the DOM
parentNode.insertBefore(fragment, nextNode);
}
if (lazyLink) {
setTimeout(dataLink, 0);
} else {
dataLink();
}
return lazyLink && lazyLink.promise();
}
function addDataBinding(linkMarkup, node, currentView, boundTagId, isLink, data, context) {
// Add data binding for data-linked elements or {^{...}} data-linked tags
var tmpl, tokens, attr, convertBack, params, trimLen, tagExpr, linkFn, linkCtx, tag, rTagIndex, hasElse, lastIndex,
linkExpressions = [];
if (boundTagId) {
// boundTagId is a string for {^{...}} data-linked tag. So only one linkTag in linkMarkup
// data and context parameters are undefined
tag = bindingStore[boundTagId];
tag = tag.linkCtx ? tag.linkCtx.tag : tag;
linkCtx = tag.linkCtx || {
data: currentView.data, // source
elem: tag._elCnt ? tag.parentElem : node, // target
view: currentView,
ctx: currentView.ctx,
attr: HTML, // Script marker nodes are associated with {^{ and always target HTML.
fn: tag._.bnd,
tag: tag,
// Pass the boundTagId in the linkCtx, so that it can be picked up in observeAndBind
_bndId: boundTagId
};
bindDataLinkTarget(linkCtx, linkCtx.fn);
} else if (linkMarkup && node) {
// If isLink then this is a top-level linking: .link(expression, target, data, ....) or
// .link(true, target, data, ....) scenario - and data and context are passed in separately from the view
data = isLink ? data : currentView.data;
// Compiled linkFn expressions could be stored in the tmpl.links array of the template
// TODO - consider also caching globally so that if {{:foo}} or data-link="foo" occurs in different places,
// the compiled template for this is cached and only compiled once...
//links = currentView.links || currentView.tmpl.links;
tmpl = currentView.tmpl;
// if (!(linkTags = links[linkMarkup])) {
// This is the first time this view template has been linked, so we compile the data-link expressions, and store them on the template.
linkMarkup = normalizeLinkTag(linkMarkup, defaultAttr(node));
lastIndex = rTagDatalink.lastIndex = 0;
while (tokens = rTagDatalink.exec(linkMarkup)) { // TODO require } to be followed by whitespace or $, and remove the \}(!\}) option.
linkExpressions.push(tokens);
lastIndex = rTagDatalink.lastIndex;
}
if (lastIndex < linkMarkup.length) {
syntaxError(linkMarkup);
}
while (tokens = linkExpressions.shift()) {
// Iterate over the data-link expressions, for different target attrs,
// e.g. index) {
removeView(current);
}
views.splice(index, itemsCount);
if (!self._.sort) {
self.fixIndex(index);
}
}
}
};
tagOrView.refresh = function() {
var self = this,
parent = self.parent;
if (parent) {
renderAndLink(self, self.index, self.tmpl, parent.views, self.data, undefined, true);
setArrayChangeLink(self);
}
};
tagOrView.fixIndex = function(fromIndex) {
// Fixup index on following view items...
var views = this.views,
index = views.length;
while (fromIndex < index--) {
if (views[index].index !== index) {
$observable(views[index]).setProperty("index", index);
// This is fixing up index, but not key, and not index on child views. From child views, use view.getIndex()
}
}
};
tagOrView.link = viewLink;
}
}
//========================
// JsViews-specific converters
//========================
$converters.merge = function(val) {
// Special converter used in data-linking to space-separated lists, such as className:
// Currently only supports toggle semantics - and has no effect if toggle string is not specified
// data-link="class{merge:boolExpr toggle=className}"
var regularExpression,
currentValue = this.linkCtx._val || "",
toggle = this.tagCtx.props.toggle;
if (toggle) {
// We are toggling the class specified by the toggle property,
// and the boolean val binding is driving the insert/remove toggle
regularExpression = toggle.replace(/[\\^$.|?*+()[{]/g, "\\$&");
// Escape any regular expression special characters (metacharacters) within the toggle string
regularExpression = "(\\s(?=" + regularExpression + "$)|(\\s)|^)(" + regularExpression + "(\\s|$))";
// Example: /(\s(?=myclass$)|(\s)|^)?(myclass(\s|$))/ - so matches (" myclass" or " " or ^ ) followed by ("myclass " or "myclass$") where ^/$ are beginning/end of string
currentValue = currentValue.replace(new RegExp(regularExpression), "$2");
val = currentValue + (val ? (currentValue && " ") + toggle : "");
}
return val;
};
//========================
// JsViews-specific tags
//========================
$tags("on", {
attr: NONE,
init: function(tagCtx) {
var props, elemProp, classProp, content,
tag = this,
i = 0,
args = tagCtx.args, // [events,] [selector,] handler
l = args.length;
for (; ii && i+1; // handler index
if (tag._.inline) {
tag.attr = HTML;
content = tagCtx.content;
content = content && content.markup;
props = tagCtx.props;
elemProp = props.elem || "button";
classProp = props["class"];
tag.template = rFirstElem.exec(content) && content || "<" + elemProp + (classProp ? ' class="' + classProp + '">' : ">")
+ ($.trim(content) || props.label || tagCtx.params.args[i] || "noop") + "" + elemProp + ">";
}
},
render: function() {
var tagCtx = this.tagCtx;
return tagCtx.render(tagCtx.view, true); // no arg, so renders against parentView.data
},
onAfterLink: function(tagCtx, linkCtx) {
var handler, params,
tag = this,
i = tag._hi,
args = tagCtx.args, // [events,] [selector,] handler
l = args.length,
props = tagCtx.props,
data = props.data,
view = tagCtx.view,
contextOb = props.context; // Context ('this' pointer) for attached handler
if (i) { // There is a handler
handler = args[i-1];
params = args.slice(i); // Subsequent args are params
args = args.slice(0, i-1); // Preceding args (if any) are events and selector
tag._sel = args[1];
tag.activeElem = tag.activeElem || (tag._.inline
? (tag._elCnt && error('Use data-link="{on...}"'), tag._sel = undefined, tag.contents(true, args[1] || "*"))
: $(linkCtx.elem));
if (!contextOb) {
// Get the path for the preceding object (context object) of handler (which is the last arg), compile function
// to return that context object, and run compiled function against data
contextOb = /^(.*)[\.^][\w$]+$/.exec(tagCtx.params.args.slice(-params.length - 1)[0]);
contextOb = contextOb && $sub.tmplFn("{:" + contextOb[1] + "}", view.tmpl, true)(linkCtx.data, view);
}
if (tag._evs) {
tag.onDispose();
}
tag.activeElem.on(
tag._evs = args[0] || "click", // events defaults to "click"
tag._sel,
data == undefined ? null : data,
tag._hlr = function(ev) {
return handler.apply(contextOb || linkCtx.data, [].concat(
params, // e.g. par1, par2
ev,
{change: ev.type, view: view, linkCtx: linkCtx},
params.slice.call(arguments, 1) // If triggering event (e.g. jsv-domchange) has additional arguments after ev, pass them too
));
// for {on 'click' handler par1 par2} use handler(par1, par2, ev, domchangeEventArgs)
// for {on 'jsv-domchange' handler par1 par2} use handler(par1, par2, ev, domchangeEventArgs, tagCtx, linkCtx, observableEventArgs)
}
);
}
},
onUpdate: function() {
return false;
},
onDispose: function() {
this.activeElem.off(this._evs, this._sel, this._hlr);
},
flow: true,
dataBoundOnly: true
});
$extend($tags["for"], {
//onUpdate: function(ev, eventArgs, tagCtxs) {
//Consider adding filtering for perf optimization. However the below prevents update on some scenarios which _should_ update - namely when there is another array on which for also depends.
//var i, l, tci, prevArg;
//for (tci = 0; (prevArg = this.tagCtxs[tci]) && prevArg.args.length; tci++) {
// if (prevArg.args[0] !== tagCtxs[tci].args[0]) {
// return true;
// }
//}
//return false;
//},
onArrayChange: function(ev, eventArgs, tagCtx, linkCtx) {
var arrayView,
target = ev.target,
targetLength = target.length,
tag = this,
change = eventArgs.change;
if (tag._.noVws // Child views not supported because target is not html - e.g. data-link="title{for ...}"
|| tag.tagCtxs[1] && ( // There is an {{else}}
change === "insert" && targetLength === eventArgs.items.length // inserting, and new length is same as inserted length, so going from 0 to n
|| change === "remove" && !targetLength // removing , and new length 0, so going from n to 0
)) {
tag.refresh();
} else {
for (arrayView in tag._.arrVws) {
arrayView = tag._.arrVws[arrayView];
if (arrayView.data === target) {
arrayView._.onArrayChange.apply(arrayView, arguments);
}
}
}
tag.domChange(tagCtx, linkCtx, eventArgs);
ev.done = true;
},
onAfterLink: function(tagCtx, linkCtx) {
var i, arrHandler, arrBinding, data,
tag = this,
arrayBindings = tag._ars || {},
tagCtxs = tag.tagCtxs,
l = tagCtxs.length,
selected = tag.selected || 0;
for (i = 0; i <= selected; i++) {
tagCtx = tagCtxs[i]; // loop through tagCtxs up to selected
data = tagCtx.map
? tagCtx.map.tgt // 'data' is mapped data
: tagCtx.args.length
? tagCtx.args[0] // or args[0]
: tagCtx.view.data; // or defaults to current data.
if ((arrBinding = arrayBindings[i]) && data !== arrBinding[0]) { // Is there previous array data on this tagCtx, different from new data
$observe(arrBinding[0], arrBinding[1], true); //unobserve previous array
delete arrayBindings[i];
}
if (!arrayBindings[i] && $isArray(data)) {
$observe(data, arrHandler = function(ev, eventArgs) {
var tagCt = tagCtx;
tag.onArrayChange(ev, eventArgs, tagCt, linkCtx);
});
arrayBindings[i] = [data, arrHandler]; // Store array data and arrayChangeHandler on tag._ars[i]
}
}
for (i = selected + 1; i < l; i++) { // If there were previous bindings on later tagCtxs, remove them
if (arrBinding = arrayBindings[i]) {
$observe(arrBinding[0], arrBinding[1], true); //unobserve previous binding
delete arrayBindings[i];
}
}
tag._ars = arrayBindings;
},
onDispose: function() {
var l, tag = this;
for (l in tag._ars) {
$observe(tag._ars[l][0], tag._ars[l][1], true); //unobserve
}
}
});
$extend($tags["if"], {
onUpdate: function(ev, eventArgs, tagCtxs) {
var tci, prevArg, different;
for (tci = 0; (prevArg = this.tagCtxs[tci]); tci++) {
different = prevArg.props.tmpl !== tagCtxs[tci].props.tmpl || prevArg.args.length && !(prevArg = prevArg.args[0]) !== !tagCtxs[tci].args[0];
if ((!this.convert && !!prevArg) || different) {
return different;
// If there is not a change of template, and there is no converter, and newArg and prevArg are both truthy, return false to cancel update.
// (Even if values on later elses are different, we still don't want to update, since rendered output would be unchanged)
// If newArg and prevArg are different, return true, to update
// If newArg and prevArg are both falsey, move to the next {{else ...}}
}
}
// Boolean value of all args are unchanged (falsey), so return false to cancel update
return false;
},
onAfterLink: function(tagCtx, linkCtx, eventArgs) {
if (eventArgs) {
this.domChange(tagCtx, linkCtx, eventArgs);
}
}
});
function observeProps(map, ev, eventArgs) {
if (eventArgs.change === "set") {
var target = map.tgt,
l = target.length;
while (l--) {
if (target[l].key === eventArgs.path) {
break;
}
}
if (l === -1) {
if (eventArgs.path && !eventArgs.remove) {
$observable(target).insert({key: eventArgs.path, prop: eventArgs.value});
}
} else if (eventArgs.remove) {
$observable(target).remove(l);
} else {
$observable(target[l]).setProperty("prop", eventArgs.value);
}
}
}
function observeMappedProps(map, ev, eventArgs) {
var item,
source = map.src,
change = eventArgs.change;
if (change === "set") {
if (eventArgs.path === "prop") {
$observable(source).setProperty(ev.target.key, eventArgs.value);
} else { // path === "key"
$observable(source).removeProperty(eventArgs.oldValue); // When key is modified observably, remove old one and set new one
$observable(source).setProperty(eventArgs.value, ev.target.prop);
}
} else if (change === "remove") {
item = eventArgs.items[0];
$observable(source).removeProperty(item.key);
delete source[item.key];
} else if (change === "insert") {
item = eventArgs.items[0];
if (item.key) {
$observable(source).setProperty(item.key, item.prop);
}
}
}
function shallowArrayFilter(allPath /*, object, parentObs*/) { // Filter used by {{props}} for the mappedProps target array
return allPath.indexOf(".") < 0;
}
$tags("props", {
baseTag: "for",
dataMap: $views.map({
getTgt: $tags.props.dataMap.getTgt,
obsSrc: observeProps,
obsTgt: observeMappedProps,
tgtFlt: shallowArrayFilter
}),
flow: true
});
//========================
// Extend jQuery namespace
//========================
$extend($, {
//=======================
// jQuery $.view() plugin
//=======================
view: $view = function(node, inner, type) {
// $.view() returns top view
// $.view(node) returns view that contains node
// $.view(selector) returns view that contains first selected element
// $.view(nodeOrSelector, type) returns nearest containing view of given type
// $.view(nodeOrSelector, "root") returns root containing view (child of top view)
// $.view(nodeOrSelector, true, type) returns nearest inner (contained) view of given type
function getInnerView(nd, isVl) {
if (nd) {
vwInfos = viewInfos(nd, isVl, rOpenViewMarkers);
for (j = 0, k = vwInfos.length; j < k; j++) {
if ((view = viewStore[vwInfos[j].id]) && (view = view && type ? view.get(true, type) : view)) {
break;
}
}
}
}
if (inner !== !!inner) {
// inner not boolean, so this is view(nodeOrSelector, type)
type = inner;
inner = undefined;
}
var view, vwInfos, i, j, k, l, elems,
level = 0,
body = document.body;
if (node && node !== body && topView._.useKey > 1) {
// Perf optimization for common cases
node = "" + node === node
? $(node)[0]
: node.jquery
? node[0]
: node;
if (node) {
if (inner) {
getInnerView(node._df, true);
if (!view) {
// Treat supplied node as a container element and return the first view encountered.
elems = qsa ? node.querySelectorAll(bindElsSel) : $(bindElsSel, node).get();
l = elems.length;
for (i = 0; !view && i < l; i++) {
getInnerView(elems[i]);
}
}
return view;
}
while (node) {
// Move back through siblings and up through parents to find preceding node which is a _prv (prevNode)
// script marker node for a non-element-content view, or a _prv (first node) for an elCnt view
if (vwInfos = viewInfos(node, undefined, rViewMarkers)) {
l = vwInfos.length;
while (l--) {
view = vwInfos[l];
if (view.open) {
if (level < 1) {
view = viewStore[view.id];
return view && type ? view.get(type) : view || topView;
}
level--;
} else {
// level starts at zero. If we hit a view.close, then we move level to 1, and we don't return a view until
// we are back at level zero (or a parent view with level < 0)
level++;
}
}
}
node = node.previousSibling || node.parentNode;
}
}
}
return topView;
},
link: $link,
unlink: $unlink,
//=====================
// override $.cleanData
//=====================
cleanData: function(elems) {
if (elems.length && isCleanCall) {
// Remove JsViews bindings. Also, remove from the DOM any corresponding script marker nodes
clean(elems);
}
oldCleanData.apply($, arguments);
}
});
// Possible future addition - e.g. for ckeditor tag control
//$views.utility = {
// validate: function(html) {
// try {
// topView.link(undefined, document.createElement("div"), undefined, undefined, html, undefined, undefined, 1);
// }
// catch (e) {
// return e.message;
// }
// }
//};
//===============================
// Extend jQuery instance plugins
//===============================
$extend($.fn, {
link: function(expr, from, context, noIteration, parentView, prevNode, nextNode) {
return $link(expr, this, from, context, noIteration, parentView, prevNode, nextNode);
},
unlink: function() {
return $unlink(this);
},
view: function(inner, type) {
return $view(this[0], inner, type);
}
});
//==============================================================================
// Override jQuery methods that call our overridden cleanData, for disposal etc.
//==============================================================================
$.each([HTML, "replaceWith", "empty", "remove"], function(i, name) {
var oldFn = $.fn[name];
$.fn[name] = function() {
var result;
isCleanCall = 1; // Make sure cleanData does disposal only when coming from these calls.
try {
result = oldFn.apply(this, arguments);
}
finally {
isCleanCall = 0;
}
return result;
};
});
//===============
// Extend topView
//===============
addLinkMethods($extend(topView = $sub.topView, {tmpl: {links: {}}}));
viewStore = {0: topView}; // Top-level view
//=========================
// Extend $.views.settings
//=========================
oldAdvSet = $sub.advSet;
$sub.advSet = function() { // refresh advanced settings
oldAdvSet();
global._jsv = $subSettingsAdvanced._jsv
? $extend(global._jsv || {}, { // create global _jsv, for accessing views, etc
views: viewStore,
bindings: bindingStore
})
: undefined; // In IE8 cannot do delete global._jsv
$viewsLinkAttr = $subSettingsAdvanced.linkAttr;
linkViewsSel = bindElsSel + ",[" + $viewsLinkAttr + "]";
wrapMap = $subSettingsAdvanced._wm;
wrapMap.optgroup = wrapMap.option;
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;
};
$viewsSettings.advanced({
linkAttr: "data-link",
useViews: false,
noValidate: false,
// wrapMap provide appropriate wrappers for inserting innerHTML, used in insertBefore
// We have to close these tags to support XHTML (#13200)
// TODO investigate whether more recent jQuery implementation using wrapMap in domManip/$().html() etc. is better optimized now...
_wm: {
option: [1, ""],
legend: [1, ""],
area: [1, ""],
param: [1, ""],
thead: [1, "
", "
"],
tr: [2, "
", "
"],
td: [3, "
", "
"],
col: [2, "
", "
"],
svg_ns: [1, ""],
// IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags,
// unless wrapped in a div with non-breaking characters in front of it.
div: $.support.htmlSerialize ? [0, "", ""] : [1, "X