/*! jquery.views.js v1.0.14: 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 2024, Boris Moore * Released under the MIT License. */ //jshint -W018, -W041, -W120 (function(factory, global) { // global var is the this object, which is window when running in the usual browser environment var $ = global.jQuery; if (typeof exports === "object") { // CommonJS e.g. Browserify module.exports = $ ? factory(global, $) : function($) { // If no global jQuery, take 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.previousElementSibling; // IE9 or later only } else { targetParent = targetParent.parentNode; } } else { targetParent = targetParent.lastElementChild; // IE9 or later only } } } 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; } if (onAftCr = changeHandler(view, onAfterCreateStr) || onAfterCreate) { onAftCr.call(view.ctx.tag, 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 tag = bindingStore[vwInfo.id]; if (!tag._is && tag.linkCtx) { parentTag = tag = tag.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 deferred 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) { 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); } } // if (context.lazyLink) { // setTimeout(doLinking) (doLinking is function wrapper of following lines) // See Future tasks, and https://github.com/BorisMoore/jsviews/issues/368. // Could call context.lazyLink as callback, on async completion - or return promise. //================ 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; // We data-link depth-first ("on the way in"), which is better for perf - and allows setting parent tags etc. view = tag.tagCtx.view; // Add data binding (unless skipped due to lateRender) addDataBinding(late, undefined, tag._prv, view, linkInfo.id); } else { tag._nxt = elem; if (tag._.unlinked && !tag._toLk) { // This is a 'close linked tag' binding annotation (and data-binding was not skipped due to lateRender) tagCtx = tag.tagCtx; view = tagCtx.view; callAfterLink(tag); } } } } else { // Add data binding for a data-linked element (with data-link attribute) addDataBinding(late, elem.getAttribute($viewsLinkAttr), elem, $view(elem), undefined, isLink, outerData, context); } } //}); } //==== /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, 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 = [], late = [], onAfterCreate = changeHandler(self, onAfterCreateStr), processInfos = processViewInfos; if (refresh) { 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 ? (typeof parentNode === STRING ? $(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); } dataLink(); return late; } function addDataBinding(late, linkMarkup, node, currentView, boundTagId, isLink, data, context) { // Add data binding for data-linked elements or {^{...}} data-linked tags var tmpl, tokens, attr, convertBack, 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 arguments are undefined tag = bindingStore[boundTagId]; tag = tag.linkCtx ? tag.linkCtx.tag : tag; linkCtx = tag.linkCtx || { type: "inline", 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 }; tag.linkCtx = linkCtx; bindDataLinkTarget(linkCtx, late); tag._toLk = linkCtx._bndId; // If data binding happened, remove _toLk flag from tag } else if (linkMarkup && node) { // Data-linked element // 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; tmpl = currentView.tmpl; 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. 15 && which < 21 || which > 32 && which < 41 || which > 111 && which < 131 || which === 27 || which === 144)) { // Shift, Ctrl, Alt, Pause, Caplock, Page up/down End, Home, Left, Up, Right, Down, Function keys, Escape, Numlock setTimeout(function() { onElemChange(ev); }); } } function bindTriggerEvent($elem, trig, onoff) { // Bind keydown, or other trigger - (rather than use the default change event bubbled to activeBody) if (trig === true && useInput && (!isIE || $elem[0].contentEditable !== TRUE)) { // IE oninput event is not raised for contenteditable changes $elem[onoff]("input.jsv", onElemChange); // For HTML5 browser with "oninput" support - for mouse editing of text } else { trig = typeof trig === STRING ? trig : "keydown.jsv"; // Set trigger to (true || truey non-string (e.g. 1) || 'keydown') $elem[onoff](trig, trig.indexOf("keydown") >= 0 ? asyncOnElemChange : onElemChange); // Get 'keydown' with async } } function bindLinkedElChange(tag, linkedElem) { // Two-way binding for linkedElem - in the case of input, textarea or contentEditable elements. // Trigger setting may have changed. Unbind previous trigger binding (if any) and bind new one. var $linkedElem, newTrig, oldTrig = linkedElem._jsvTr || false; if (tag) { newTrig = tag.tagCtx.props.trigger; if (newTrig === undefined) { newTrig = tag.trigger; } } if (newTrig === undefined) { newTrig = $subSettings.trigger; } // Trigger is noop except for text box, textarea, contenteditable... newTrig = newTrig && (linkedElem.tagName === "INPUT" && linkedElem.type !== CHECKBOX && linkedElem.type !== RADIO || linkedElem.type === "textarea" || linkedElem.contentEditable === TRUE) && newTrig || false; if (oldTrig !== newTrig) { $linkedElem = $(linkedElem); bindTriggerEvent($linkedElem, oldTrig, "off"); bindTriggerEvent($linkedElem, linkedElem._jsvTr = newTrig, "on"); } } function defineBindToDataTargets(binding, tag, cvtBk) { // Two-way binding. // We set the binding.to[1] to be the cvtBack, and binding.to[0] to be either the path to the target, or [object, path] where the target is the // path on the provided object. So for a computed path with an object call: a.b.getObject().d.e, we set to[0] to be [exprOb, "d.e"], and // we bind to the path on the returned object, exprOb.ob, as target. Otherwise our target is the first path, paths[0], which we will convert // with contextCb() for paths like ~a.b.c or #x.y.z var pathIndex, path, lastPath, bindtoOb, to, bindTo, paths, k, obsCtxPrm, linkedCtxParam, contextCb, targetPaths, bindTos, fromIndex, isCpfn, tagElse = 1, tos = [], linkCtx = binding.linkCtx, source = linkCtx.data, targetPathsElses = linkCtx.fn.paths; if (binding && !binding.to) { if (tag) { if (!tag.convertBack) { tag.convertBack = cvtBk; } bindTo = tag.bindTo; tagElse = tag.tagCtxs ? tag.tagCtxs.length : 1; } while (tagElse--) { bindTos = []; if (targetPaths = targetPathsElses[tagElse]) { bindTo = targetPaths._jsvto ? ["jsvto"] : (bindTo || [0]); if (!tagElse && tag && tag._.ths) { // Tag has a this=expr bindign for which we will create an additional 'to' target (at index bindTo.length) bindTo = bindTo.concat("this"); } k = bindTo.length; while (k--) { path = ""; contextCb = linkCtx._ctxCb; paths = bindTo[k]; paths = targetPaths[+paths === paths ? paths : "_" + paths]; // If path is a string, prepend "_" to avoid collision (e.g. with array.length if path is "length") if (pathIndex = paths && paths.length) { lastPath = paths[pathIndex - 1]; if (lastPath._cpfn) { // Computed property exprOb bindtoOb = lastPath; while (lastPath.sb && lastPath.sb._cpfn) { path = lastPath = lastPath.sb; } path = lastPath.sb || path && path.path; isCpfn = lastPath._cpfn && !lastPath.sb; // leaf binding to computed property/function "a.b.c()" lastPath = path ? path.slice(1) : bindtoOb.path; } to = path ? [bindtoOb, // 'exprOb' for this expression and view-binding. So bindtoOb.ob is current object returned by expression. lastPath] : resolveDataTargetPath(lastPath, source, contextCb); // Get 'to' for target path: lastPath } else { // Contextual parameter ~foo with no external binding - has ctx.foo = [{_ocp: xxx}] and binds to ctx.foo._ocp linkedCtxParam = tag.linkedCtxParam; to = []; fromIndex = tag._.fromIndex; if (fromIndex && linkedCtxParam && linkedCtxParam[fromIndex[k]]) { // This is a tag binding, with linked tag contextual parameters to = [tag.tagCtxs[tagElse].ctx[linkedCtxParam[fromIndex[k]]][0], _ocp]; } } if ((obsCtxPrm = to._cxp) && obsCtxPrm.tag && lastPath.indexOf(".")<0) { // This is a binding for a tag contextual parameter (e.g. within a tag block content to = obsCtxPrm; } to.isCpfn = isCpfn; bindTos.unshift(to); } } tos.unshift(bindTos); } binding.to = tos; } } function resolveDataTargetPath(targetPath, source, contextCb) { // Iteratively process targetPath, resolving ~a.b.c paths for contextual parameters var path, bindtoOb, to, l, obsCtxPrm, view, topCp, data; while (targetPath && targetPath !== _ocp && (to = contextCb(path = targetPath.split("^").join("."))) && (l = to.length)) { if (obsCtxPrm = to[0]._cxp) { // Two-way binding to a contextual parameter reference, ~foo (declared as ~foo=expr on a parent tag) topCp = topCp || obsCtxPrm; view = to[0][0]; if (_ocp in view) { data = view; view = view._vw; } else { data = view.data; } topCp.path = targetPath = to[0][1]; to = [topCp.data = data, targetPath]; contextCb = $sub._gccb(view); if (targetPath._cpfn) { // computed property bindtoOb = targetPath; bindtoOb.data = to[0]; bindtoOb._cpCtx = contextCb; while (targetPath.sb && targetPath.sb._cpfn) { path = targetPath = targetPath.sb; } path = targetPath.sb || path && path.path; targetPath = path ? path.slice(1) : bindtoOb.path; to = [ bindtoOb, // 'exprOb' for this expression and view-binding. So bindtoOb.ob is current object returned by expression. targetPath ]; } else if (obsCtxPrm.tag && obsCtxPrm.path === _ocp) { to = obsCtxPrm; } } else { // Two-way binding to a helper - e.g. ~address.street, or computed, e.g. ~fullName(), or view property e.g. #data.foo to = l>1 ? [to[l-2], to[l-1]] // With path: [object, path] : [to[l-1]]; // No path, (e.g. [function] for computed with setter) } source = to[0]; targetPath = to[1]; } to = to || [source, path]; to._cxp = topCp; return to; } function mergeCtxs(tag, newCtxs, replace) { // Merge updated tagCtxs into tag.tagCtxs var tagCtx, newTagCtx, latePath, view = tag.tagCtx.view, tagCtxs = tag.tagCtxs || [tag.tagCtx], l = tagCtxs.length, refresh = !newCtxs; if (refresh) { newCtxs = tag._.bnd.call(view.tmpl, (tag.linkCtx || view).data, view, $sub); if (newCtxs.lt) { return; // We are calling tag.refresh() but a late path (@a.b.c) has not yet returned an object (@a) so cancel the refresh() } tag._.lt = undefined; // All late paths are now resolved, so this is no longer a late tag newCtxs = $isArray(newCtxs) ? newCtxs : [newCtxs]; } if (replace) { // Replace previous tagCtxs by new ones, rather than merging tagCtxs = tag.tagCtxs = newCtxs; tag.tagCtx = tagCtxs[0]; addLinkMethods(tag); } else { while (l--) { tagCtx = tagCtxs[l]; newTagCtx = newCtxs[l]; $extend(tagCtx.ctx, newTagCtx.ctx); // We don't support propagating ctx variables, ~foo, observably, to nested views. So extend, not setProperty... tagCtx.args = newTagCtx.args; if (refresh) { tagCtx.tmpl = newTagCtx.tmpl; } $observable(tagCtx.props).setProperty(newTagCtx.props); } } $sub._thp(tag, tagCtxs[0]); // tagHandlersFromProps return tagCtxs; } //========= // Disposal //========= function clean(elems) { // Remove data-link bindings, or contained views var l, elem, bindings, elemArray = [], len = elems.length, i = len; while (i--) { // Copy into an array, so that deletion of nodes from DOM will not cause our 'i' counter to get shifted // (Note: This seems as fast or faster than elemArray = [].slice.call(elems); ...) elemArray.push(elems[i]); } i = len; while (i--) { elem = elemArray[i]; if (elem.parentNode) { // Has not already been removed from the DOM if (bindings = elem._jsvBnd) { // Get propertyChange bindings for this element // This may be an element with data-link, or the opening script marker node for a data-linked tag {^{...}} // bindings is a string with the syntax: "(&bindingId)*" bindings = bindings.slice(1).split("&"); elem._jsvBnd = ""; l = bindings.length; while (l--) { // Remove associated bindings removeViewBinding(bindings[l], elem._jsvLkEl, elem); // unbind bindings with this bindingId on this view } } disposeTokens(markerNodeInfo(elem) + (elem._df || ""), elem); } } } function removeViewBinding(bindId, linkedElemTag, elem) { // Unbind var objId, linkCtx, tag, object, obsId, tagCtxs, l, map, linkedElem, trigger, view, tagCtx, linkedElems, allLinkedElems, binding = bindingStore[bindId]; if (linkedElemTag) { elem._jsvLkEl = undefined; } else if (binding && (!elem || elem === binding.elem)) { // Test that elem is actually binding.elem, since cloned elements can have inappropriate markerNode info delete bindingStore[bindId]; // Delete already, so call to onDispose handler below cannot trigger recursive deletion (through recursive call to jQuery cleanData) for (objId in binding.bnd) { if (object = binding.bnd[objId]) { obsId = binding.cbId; if ($isArray(object)) { $([object]).off(arrayChangeStr + obsId).off(propertyChangeStr + obsId); // There may be either or both of arrayChange and propertyChange } else { $(object).off(propertyChangeStr + obsId); } delete binding.bnd[objId]; } } if (linkCtx = binding.linkCtx) { if (tag = linkCtx.tag) { if (tagCtxs = tag.tagCtxs) { l = tagCtxs.length; while (l--) { tagCtx = tagCtxs[l]; if (map = tagCtx.map) { map.unmap(); //unobserve } // Copy linkedElems in case tag.linkedElem or tag.linkedElems are undefined in onUnbind if (linkedElems = tagCtx.linkedElems) { allLinkedElems = (allLinkedElems || []).concat(linkedElems); } } } if (tag.onUnbind) { tag.onUnbind(tag.tagCtx, linkCtx, tag.ctx); } if (tag.onDispose) { tag.onDispose(); } if (!tag._elCnt) { if (tag._prv) { tag._prv.parentNode.removeChild(tag._prv); } if (tag._nxt) { tag._nxt.parentNode.removeChild(tag._nxt); } } } linkedElems = allLinkedElems || [$(linkCtx.elem)]; l = linkedElems.length; while (l--) { linkedElem = linkedElems[l]; if (trigger = linkedElem && linkedElem[0] && linkedElem[0]._jsvTr) { bindTriggerEvent(linkedElem, trigger, "off"); linkedElem[0]._jsvTr = undefined; } } view = linkCtx.view; if (view.type === "link") { view.parent.removeViews(view._.key, undefined, true); // A "link" view is associated with the binding, so should be disposed with binding. } else { delete view._.bnds[bindId]; } } delete binding.s[binding.cbId]; } } function $unlink(to) { if (to) { to = to.jquery ? to : $(to); to.each(function() { var innerView; //TODO fix this for better perf. Rather that calling inner view multiple times which does querySelectorAll each time, consider a single querySelectorAll // or simply call view.removeViews() on the top-level views under the target 'to' node, then clean(...) // And/or replace each() by for() or while() while ((innerView = $view(this, true)) && innerView.parent) { innerView.parent.removeViews(innerView._.key, undefined, true); } clean(this.getElementsByTagName("*")); }); clean(to); } else { // Call to $.unlink() is equivalent to $.unlink(true, "body") if (activeBody) { $(activeBody) .off(elementChangeStr, onElemChange) .off('blur.jsv', '[contenteditable]', onElemChange); activeBody = undefined; } topView.removeViews(); clean(document.body.getElementsByTagName("*")); } } //======== // Helpers //======== function inputAttrib(elem) { return elem.type === CHECKBOX ? elem[CHECKED] : elem.value; } function changeHandler(view, name, tag) { // Get onBeforeChange, onAfterChange, onAfterCreate handler - if there is one; return tag && tag[name] || view.ctx[name] && view.ctxPrm(name) || $views.helpers[name]; } //========================== Initialize ========================== //===================== // JsRender integration //===================== addLinkMethods($sub.View.prototype); // Modify the View prototype to include link methods $sub.onStore.template = function(name, item, parentTmpl) { if (item === null) { delete $.link[name]; delete $.render[name]; } else { item.link = tmplLink; if (name && !parentTmpl && name !== "jsvTmpl") { $.render[name] = item; $.link[name] = function() { return tmplLink.apply(item, arguments); }; } } }; $sub.viewInfos = viewInfos; // Expose viewInfos() as public helper method // Define JsViews version of delimiters(), and initialize ($viewsSettings.delimiters = function() { // Run delimiters initialization in context of jsrender.js var ret = oldJsvDelimiters.apply(0, arguments), // Now set also delimOpenChar0 etc. in context of jquery.views.js... delimChars = $subSettings.delimiters; delimOpenChar0 = delimChars[0].charAt(0); delimOpenChar1 = delimChars[0].charAt(1); delimCloseChar0 = delimChars[1].charAt(0); delimCloseChar1 = delimChars[1].charAt(1); linkChar = delimChars[2]; // Data-linking must use new delimiters rTagDatalink = new RegExp("(?:^|\\s*)([\\w-]*)(\\" + linkChar + ")?(\\" + delimOpenChar1 + $sub.rTag + "(:\\w*)?\\" + delimCloseChar0 + ")", "g"); return ret; })(); // jshint ignore:line $sub.addSetting("trigger"); //==================================== // 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 // view marker tokens: #m_...VIEW.../m_ // tag marker tokens: #m^...TAG..../m^ 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] // A view: "#m_" or "/m_" : bindingStore[viewId].linkCtx.tag; // A tag "#m^" or "/m^" if (viewOrTag) { if (vwInfo.open) { // A "#m_" or "#m^" token viewOrTag._prv = nextNode; } else if (vwInfo.close) { // A "/m_" or "/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, elem) { 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 && (!elem || vwItem._prv === elem || vwItem.parentElem === elem )) { // 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 // (or unless the elem is not related - e.g. a cloned element which 'accidentally' picked up the data-jsv atttribute of the ._df expando) vwItem.parent.removeViews(vwItem._.key, undefined, true); } } else { removeViewBinding(vwItem.id, undefined, elem); // unbind bindings with this bindingId on this view } } } } //============================================ // Add link methods to data-linked view or tag //============================================ function updateValue(val, index, tagElse, async, bindId, ev) { // async, bindId and ev not documented - used internally, e.g. for paged and sorted arrays on tags with dataMap, such as {{for}} // Observably update a data value targeted by the binding.to binding of a 2way data-link binding. Called when elem changes // Called when linkedElem of a tag control changes: as updateValue(val, index, tagElse, bindId, ev) - this: undefined // Called directly as tag.updateValue(val, index, tagElse) - this: tag var self = this, values = []; if (self && self._tgId) { bindId = self; } if (arguments.length < 4) { if (+index !== index) { async = index; tagElse = index = 0; } else if (+tagElse !== tagElse) { async = tagElse; tagElse = 0; } } values[index||0] = val; updateValues(values, tagElse, async, bindId, ev); return self; } function setValues() { // tagCtx.setValues() calls tag.setValue() on that tagCtx for each bindTo target var m = this.tag.bindTo.length, ev = arguments[m], eventArgs = arguments[m+1]; while (m--) { this.tag.setValue(arguments[m], m, this.index, ev, eventArgs); } } function addLinkMethods(tagOrView) { // tagOrView is View prototype or tag instance var l, m, tagCtx, boundProps, bindFrom, key, theTag, theView; tagOrView.contents = function(deep, select, select2) { // For a view, a tag or a tagCtx, return jQuery object with the content nodes, if (deep !== !!deep) { // deep not boolean, so this is contents(selector) select = deep; deep = undefined; } var filtered, nodes = $(this.nodes()); if (nodes[0]) { select = deep ? select || "*" : select; filtered = select ? nodes.filter(select) : nodes; nodes = deep ? filtered.add(nodes.find(select)) : filtered; } return select2 // select2 is optional selector for an additional filtering. e.g checkboxgroup with ? nodes.filter(select2) // selector for binding to subset of checkbox elements : nodes; }; tagOrView.nodes = function(withMarkers, prevNode, nextNode) { // For a view, a tag or a tagCtx, return top-level nodes // Do not return any script marker nodes, unless withMarkers is true // Optionally limit range, by passing in prevNode or nextNode parameters var node, self = this.contentView || this, // If tagCtx, use tagCtx.contentView elCnt = self._elCnt, prevIsFirstNode = !prevNode && elCnt, nodes = []; if (!self.args) { // If tagCtx with no content (so no contentView) self is tagCtx: return empty []; prevNode = prevNode || self._prv; nextNode = nextNode || self._nxt; node = prevIsFirstNode ? (prevNode === self._nxt ? self.parentElem.lastSibling : prevNode) : (self.inline === false ? prevNode || self.linkCtx.elem.firstChild : prevNode && prevNode.nextSibling); while (node && (!nextNode || node !== nextNode)) { if (withMarkers || elCnt || node.tagName !== SCRIPT) { // All the top-level nodes in the view // (except script marker nodes, unless withMarkers = true) // (Note: If a script marker node, viewInfo.elCnt undefined) nodes.push(node); } node = node.nextSibling; } } return nodes; }; tagOrView.childTags = function(deep, tagName) { // For a view, a tag or a tagCtx, return child tags - at any depth, or as immediate children only. if (deep !== !!deep) { // deep not boolean, so this is childTags(tagName) - which looks for top-level tags of given tagName tagName = deep; deep = undefined; } var self = this.contentView || this, // If tagCtx, use tagCtx.contentView view = self.link ? self : self.tagCtx.view, // This may be a view or a tag. If a tag, get the view from tag.tagCtx.view prevNode = self._prv, elCnt = self._elCnt, tags = []; if (!self.args) { // If tagCtx with no content (so no contentView) self is tagCtx: return empty []; view.link( undefined, self.parentElem, elCnt ? prevNode && prevNode.previousSibling : prevNode, self._nxt, undefined, {get:{ tags: tags, deep: deep, name: tagName, id: self.link ? self._.id + "_" : self._tgId + "^" }} ); } return tags; }; if (tagOrView._is === "tag") { //======================= // This is a TAG instance //======================= theTag = tagOrView; m = theTag.tagCtxs.length; while (m--) { tagCtx = theTag.tagCtxs[m]; tagCtx.setValues = setValues; tagCtx.contents = tagOrView.contents; tagCtx.childTags = tagOrView.childTags; tagCtx.nodes = tagOrView.nodes; } boundProps = theTag.boundProps = theTag.boundProps || []; if (bindFrom = theTag.bindFrom) { l = bindFrom.length; while (l--) { key = bindFrom[l]; if (typeof key === STRING) { bindFrom[key] = 1; if ($inArray(key, boundProps) < 0) { boundProps.push(key); // Add any 'bindFrom' props to boundProps array. (So two-way binding works without writing ^foo=expression) } } } } theTag.setValue = $sub._gm( // getMethod theTag.constructor.prototype.setValue || function(val) { // base method return val; }, function(val, indexFrom, tagElse, ev, eventArgs) { indexFrom = indexFrom || 0; tagElse = tagElse || 0; var linkedElem, linkedEl, linkedCtxParam, indexTo, linkedElems, newVal, tagCtx = theTag.tagCtxs[tagElse]; if (tagCtx._bdArgs && (eventArgs || val !== undefined) && tagCtx._bdArgs[indexFrom]===val && (!eventArgs || eventArgs.change !== "set" || ev.target !== val && eventArgs.value !== val)) { if (tagCtx._bdVals) { // If val is not undefined (or is coming from an observable change event), and is a value that was already returned, use stored value and don't call tag.setValue() val = tagCtx._bdVals[indexFrom]; } } else { // Call tag method tag.setValue(), if implemented tagCtx._bdArgs = tagCtx._bdArgs || []; tagCtx._bdArgs[indexFrom] = val; newVal = theTag.base.call(theTag, val, indexFrom, tagElse, ev, eventArgs); if (newVal !== undefined) { tagCtx._bdVals = tagCtx._bdVals || []; tagCtx._bdVals[indexFrom] = newVal; // store value, so that if same value is called later we'll use the stored value, and won't call tag.setValue() val = newVal; } } if (val !== undefined && (linkedCtxParam = theTag.linkedCtxParam) && linkedCtxParam[indexFrom]) { // If this setValue call corresponds to a tag contextual parameter and the tag has a converter, then we need to set the // value of this contextual parameter (since it is not directly bound to the tag argument/property when there is a converter). tagCtx.ctxPrm(linkedCtxParam[indexFrom], val); } indexTo = theTag._.toIndex[indexFrom]; if (indexTo !== undefined) { if (linkedElems = tagCtx.linkedElems || theTag.linkedElem && [theTag.linkedElem]) { if ((linkedElem = linkedElems[indexTo]) && (l = linkedElem.length)) { while (l--) { linkedEl = linkedElem[l]; if (val !== undefined && !linkedEl._jsvChg && theTag.linkCtx._val !== val) { if (linkedEl.value !== undefined) { if (linkedEl.type === CHECKBOX) { linkedEl[CHECKED] = $.isArray(val) ? $.inArray(linkedEl.value, val) > -1 : val && val !== "false"; } else if (linkedEl.type === RADIO) { linkedEl[CHECKED] = (linkedEl.value === val); } else { $(linkedEl).val(val); // Use jQuery for attrHooks - can't just set value (on select, for example) } } else { linkedEl[linkedEl.contentEditable === TRUE ? "innerHTML" : TEXTCONTENT] = val; } } if (tagCtx.props.name) { linkedEl.name = linkedEl.name || tagCtx.props.name; } } } } } return theTag; } ); theTag.updateValue = updateValue; theTag.updateValues = function() { var tagElse, async, tag = this, bindToLength = tag.bindTo ? tag.bindTo.length : 1, extra = arguments.length - bindToLength; if (extra) { tagElse = arguments[bindToLength]; if (extra > 1) { async = extra > 1 ? arguments[bindToLength + 1] : undefined; } else if (+tagElse !== tagElse) { async = tagElse; tagElse = 0; } } return updateValues(arguments, tagElse, async, this); }; theTag.setValues = function() { // tag.setValues(a, b, c) calls tagCtx.setValues(a, b, c) on the first tagCtx setValues.apply(theTag.tagCtx, arguments); return theTag; }; theTag.refresh = function() { var attr, sourceValue, linkCtx = theTag.linkCtx, view = theTag.tagCtx.view; if (!(sourceValue = mergeCtxs(theTag))) { return; } if (theTag.onUnbind) { theTag.onUnbind(theTag.tagCtx, linkCtx, theTag.ctx); theTag._.unlinked = true; } attr = theTag.inline ? HTML : (linkCtx.attr || defaultAttr(theTag.parentElem, true)); sourceValue = theTag.tagName === ":" ? $sub._cnvt(theTag.convert, view, theTag.tagCtx) : $sub._tag(theTag, view, view.tmpl, sourceValue, true); // Get rendered HTML for tag, based on refreshed tagCtxs observeAndBind(linkCtx, linkCtx.data, linkCtx.elem); updateContent(sourceValue, linkCtx, attr, theTag); callAfterLink(theTag); return theTag; }; theTag.domChange = function(forOrIfTagCtx, linkCtx, eventArgs, ev) { // domChange notification support, called by {^{for}}, onArrayChange, {^{if}} onAfterLink etc. var bindId, linkTag, view, elem, tag = theTag, domChangeNotification = "jsv-domchange", rSplitBindings = /&(\d+)\+?/g, tagParElem = tag.parentElem, hasListener = $._data(tagParElem).events; if (hasListener && hasListener[domChangeNotification]) { // If the parent element of this tag has a 'jsv-domchange' event handler binding, we will trigger it. // e.g.
", "
"], 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
", "
"] }, _fe: { input: { from: inputAttrib, to: VALUE }, textarea: valueBinding, select: valueBinding, optgroup: { to: "label" } } }); return $; }, window));