/*! jsviews.js v1.0.0-beta.69 (Beta Candidate) single-file version: http://jsviews.com/ */
/*! includes JsRender, JsObservable and JsViews - see: http://jsviews.com/#download */
/* Interactive data-driven views using JsRender templates */
//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< JsRender >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
/* JsRender:
* See http://jsviews.com/#jsrender and http://github.com/BorisMoore/jsrender
* Copyright 2015, Boris Moore
* Released under the MIT License.
*/
//jshint -W018, -W041
(function(factory) {
// global var is the this object, which is window when running in the usual browser environment
var global = (0, eval)('this'), // jshint ignore:line
$ = global.jQuery;
if (typeof define === "function" && define.amd) { // AMD script loader, e.g. RequireJS
define(["jquery"], factory);
} else if (typeof exports === "object") { // CommonJS e.g. Browserify
module.exports = $
? factory($)
: function($) { // If no global jQuery, take jQuery passed as parameter: require("jsviews")(jQuery)
return factory($);
};
} else { // Browser using plain ',
openScript = ' - data-linked tag, close marker
// TODO add validation to track whether we are in attribute context (not yet hit preceding ending with a >) or element content of current 'parentTag'
// and accordingly disallow inserting script markers in attribute context. Similar for elCnt too, so no "
" 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 https://github.com/BorisMoore/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 https://github.com/BorisMoore/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-last ("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 = !$viewsSettings.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 (noDomLevel0 && firstTag && firstTag[2]) {
error("Unsupported: " + firstTag[2]); // For security reasons, don't allow insertion of elements with onFoo attributes.
}
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,
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));
rTagDatalink.lastIndex = 0;
while (tokens = rTagDatalink.exec(linkMarkup)) { // TODO require } to be followed by whitespace or $, and remove the \}(!\}) option.
linkExpressions.push(tokens);
}
while (tokens = linkExpressions.shift()) {
// Iterate over the data-link expressions, for different target attrs,
// e.g. )|!--((?:[^-]|-(?!-))*)--|(\*)))\s*((?:[^}]|}(?!}))*?))})
return this;
})(); // jshint ignore:line
//====================================
// Additional members for linked views
//====================================
function transferViewTokens(prevNode, nextNode, parentElem, id, viewOrTagChar, refresh) {
// Transfer tokens on prevNode of viewToRemove/viewToRefresh to nextNode or parentElem._df
var i, l, vwInfos, vwInfo, viewOrTag, viewId, tokens,
precedingLength = 0,
emptyView = prevNode === nextNode;
if (prevNode) {
// prevNode is either the first node in the viewOrTag, or has been replaced by the vwInfos tokens string
vwInfos = viewInfos(prevNode) || [];
for (i = 0, l = vwInfos.length; i < l; i++) {
// Step through views or tags on the prevNode
vwInfo = vwInfos[i];
viewId = vwInfo.id;
if (viewId === id && vwInfo.ch === viewOrTagChar) {
if (refresh) {
// This is viewOrTagToRefresh, this is the last viewOrTag to process...
l = 0;
} else {
// This is viewOrTagToRemove, so we are done...
break;
}
}
if (!emptyView) {
viewOrTag = vwInfo.ch === "_"
? viewStore[viewId]
: bindingStore[viewId].linkCtx.tag;
if (vwInfo.open) {
// A "#m" token
viewOrTag._prv = nextNode;
} else if (vwInfo.close) {
// A "/m" token
viewOrTag._nxt = nextNode;
}
}
precedingLength += viewId.length + 2;
}
if (precedingLength) {
prevNode.setAttribute(jsvAttrStr, prevNode.getAttribute(jsvAttrStr).slice(precedingLength));
}
tokens = nextNode ? nextNode.getAttribute(jsvAttrStr) : parentElem._df;
if (l = tokens.indexOf("/" + id + viewOrTagChar) + 1) {
tokens = vwInfos._tkns.slice(0, precedingLength) + tokens.slice(l + (refresh ? -1 : id.length + 1));
}
if (tokens) {
if (nextNode) {
// If viewOrTagToRemove was an empty viewOrTag, we will remove both #n and /n
// (and any intervening tokens) from the nextNode (=== prevNode)
// If viewOrTagToRemove was not empty, we will take tokens preceding #n from prevNode,
// and concatenate with tokens following /n on nextNode
nextNode.setAttribute(jsvAttrStr, tokens);
} else {
setDefer(parentElem, tokens);
}
}
} else {
// !prevNode, so there may be a deferred nodes token on the parentElem. Remove it.
setDefer(parentElem, removeSubStr(parentElem._df, "#" + id + viewOrTagChar));
if (!refresh && !nextNode) {
// If this viewOrTag is being removed, and there was no .nxt, remove closing token from deferred tokens
setDefer(parentElem, removeSubStr(parentElem._df, "/" + id + viewOrTagChar));
}
}
}
function disposeTokens(tokens) {
var i, l, vwItem, vwInfos;
if (vwInfos = viewInfos(tokens, true, rOpenMarkers)) {
for (i = 0, l = vwInfos.length; i < l; i++) {
vwItem = vwInfos[i];
if (vwItem.ch === "_") {
if ((vwItem = viewStore[vwItem.id]) && vwItem.type) {
// If this is the _prv (prevNode) for a view, remove the view
// - unless view.type is undefined, in which case it is already being removed
vwItem.parent.removeViews(vwItem._.key, undefined, true);
}
} else {
removeViewBinding(vwItem.id); // unbind bindings with this bindingId on this view
}
}
}
}
//====================================
// Add linked view methods to view prototype
//====================================
$extend(
$extend($sub.View.prototype, linkMethods), {
// Note: a linked view will also, after linking have nodes[], _prv (prevNode), _nxt (nextNode) ...
addViews: function(index, dataItems, tmpl) {
// if view is not an array view, do nothing
var i, viewsCount,
self = this,
itemsCount = dataItems.length,
views = self.views;
if (!self._.useKey && itemsCount && (tmpl = self.tmpl)) {
// view is of type "array"
// Use passed-in template if provided, since self added view may use a different template than the original one used to render the array.
viewsCount = views.length + itemsCount;
if (viewsCount === self.data.length // If views not already synced to array (e.g. triggered by array.length propertyChange - jsviews/issues/301)
&& renderAndLink(self, index, tmpl, views, dataItems, self.ctx) !== false) {
for (i = index + itemsCount; i < viewsCount; i++) {
$observable(views[i]).setProperty("index", i);
// This is fixing up index, but not key, and not index on child views. From child views, use view.getIndex()
}
}
}
return self;
},
removeViews: function(index, itemsCount, keepNodes) {
// view.removeViews() removes all the child views
// view.removeViews(index) removes the child view with specified index or key
// view.removeViews(index, count) removes the specified nummber of child views, starting with the specified index
function removeView(index) {
var id, bindId, parentElem, prevNode, nextNode, nodesToRemove,
viewToRemove = views[index];
if (viewToRemove && viewToRemove.link) {
id = viewToRemove._.id;
if (!keepNodes) {
// Remove the HTML nodes from the DOM, unless they have already been removed, including nodes of child views
nodesToRemove = viewToRemove.nodes();
}
// Remove child views, without removing nodes
viewToRemove.removeViews(undefined, undefined, true);
viewToRemove.type = undefined; // Set type to undefined: used as a flag that this view is being removed
prevNode = viewToRemove._prv;
nextNode = viewToRemove._nxt;
parentElem = viewToRemove.parentElem;
// If prevNode and nextNode are the same, the view is empty
if (!keepNodes) {
// Remove the HTML nodes from the DOM, unless they have already been removed, including nodes of child views
if (viewToRemove._elCnt) {
// if keepNodes is false (and transferring of tokens has not already been done at a higher level)
// then transfer tokens from prevNode which is being removed, to nextNode.
transferViewTokens(prevNode, nextNode, parentElem, id, "_");
}
$(nodesToRemove).remove();
}
if (!viewToRemove._elCnt) {
try {
prevNode.parentNode.removeChild(prevNode); // (prevNode.parentNode is parentElem, except if jQuery Mobile or similar has inserted an intermediate wrapper
nextNode.parentNode.removeChild(nextNode);
} catch (e) {}
}
setArrayChangeLink(viewToRemove);
for (bindId in viewToRemove._.bnds) {
removeViewBinding(bindId);
}
delete viewStore[id];
}
}
var current, view, viewsCount,
self = this,
isArray = !self._.useKey,
views = self.views;
if (isArray) {
viewsCount = views.length;
}
if (index === undefined) {
// Remove all child views
if (isArray) {
// views and data are arrays
current = viewsCount;
while (current--) {
removeView(current);
}
self.views = [];
} else {
// views and data are objects
for (view in views) {
// Remove by key
removeView(view);
}
self.views = {};
}
} else {
if (itemsCount === undefined) {
if (isArray) {
// The parentView is data array view.
// Set itemsCount to 1, to remove this item
itemsCount = 1;
} else {
// Remove child view with key 'index'
removeView(index);
delete views[index];
}
}
if (isArray && itemsCount
&& viewsCount - itemsCount === self.data.length) { // If views not already synced to array (e.g. triggered by array.length propertyChange - jsviews/issues/301)
current = index + itemsCount;
// Remove indexed items (parentView is data array view);
while (current-- > index) {
removeView(current);
}
views.splice(index, itemsCount);
if (viewsCount = views.length) {
// Fixup index on following view items...
while (index < viewsCount) {
$observable(views[index]).setProperty("index", index++);
}
}
}
}
return this;
},
refresh: function(context) {
var self = this,
parent = self.parent;
if (parent) {
renderAndLink(self, self.index, self.tmpl, parent.views, self.data, context, true);
setArrayChangeLink(self);
}
return self;
},
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 tag = this,
props = tagCtx.props,
content = tagCtx.content,
elemType = props.elem;
if (tag._.inline) {
tag.attr = HTML;
elemType = (elemType || "span") + ">";
tag.template = "<" + elemType + (props.label || content.markup || tagCtx.params.args[0]) + "" + elemType;
}
},
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 = 0,
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
tag.activeElem = tag.activeElem || $(tag._.inline ? (tag._elCnt && error('Use data-link="{on...}"'), tag.nodes()[0]) : linkCtx.elem);
while (i 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: $views.link = $link,
unlink: $views.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(expr) {
return $unlink(expr, 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
//===============
$extend(topView = $sub.topView, {tmpl: {links: {}}});
viewStore = { 0: topView }; // Top-level view
//=========================
// Extend $.views.settings
//=========================
$viewsSettings({
wrapMap: wrapMap = {
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