/*! 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]) + " 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
", "
"] }, linkAttr: $viewsLinkAttr = "data-link", merge: { input: { from: inputAttrib, to: "value" }, textarea: valueBinding, select: valueBinding, optgroup: { to: "label" } }, jsrDbgMode: $viewsSettings.debugMode, // debugMode for JsRender debugMode: function(debugMode) { // debugMode for JsViews $viewsSettings._dbgMode = debugMode !== false; if ($viewsSettings._dbgMode) { global._jsv = { // In debug mode create global _jsv, for accessing views, etc views: viewStore, bindings: bindingStore }; } else if (global._jsv) { global._jsv = undefined; // In IE8 cannot do delete global._jsv } }, jsv: function() { $viewsSettings.debugMode($viewsSettings._dbgMode); $viewsLinkAttr = $viewsSettings.linkAttr; error = $views._err; linkViewsSel = bindElsSel + ",[" + $viewsLinkAttr + "]"; noDomLevel0 = $viewsSettings.noDomLevel0; wrapMap.optgroup = wrapMap.option; wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; wrapMap.th = wrapMap.td; } }); return $; }));