UNPKG

jsviews

Version:

Next-generation MVVM and MVP framework - built on top of JsRender templates. Bringing templates to life...

1,164 lines (1,054 loc) 158 kB
/*! jquery.views.js v1.0.16: 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 2025, 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 <script> tag factory(global, false); } } ( // factory (for jquery.views.js) function(global, $, jsr, jso) { "use strict"; //========================== Top-level vars ========================== // global var is the this object, which is window when running in the usual browser environment var setGlobals = $ === false; // Only set globals if script block in browser (not AMD and not CommonJS) jsr = jsr || setGlobals && global.jsrender; $ = $ || global.jQuery; var versionNumber = "v1.0.16", requiresStr = "jquery.views.js requires "; if (!$ || !$.fn) { // jQuery is not loaded. throw requiresStr + "jQuery"; // We require jQuery } if (jsr && !jsr.fn) { jsr.views.sub._jq($); // map over from jsrender namespace to jQuery namespace } var $observe, $observable, $isFunction = function(ob) { return typeof ob === "function"; }, $isArray = Array.isArray, $views = $.views; if (!$.render) { // JsRender is not loaded. throw requiresStr + "jsrender.js"; // jsrender.js must be loaded before JsViews and after jQuery } if ($views.jsviews !== versionNumber) { throw requiresStr + "query.observable.js " + versionNumber; // Wrong version number } if (!$views || !$views.map || $views.jsviews !== versionNumber) { // JsRender is not loaded. throw requiresStr + "jsrender.js " + versionNumber; // jsrender.js must be loaded before JsViews and after jQuery } var document = global.document, $viewsSettings = $views.settings, $sub = $views.sub, $subSettings = $sub.settings, $extend = $sub.extend, $expando = $.expando, $converters = $views.converters, $tags = $views.tags, $subSettingsAdvanced = $subSettings.advanced, // These two settings can be overridden on settings after loading jsRender, and prior to loading jquery.observable.js and/or JsViews propertyChangeStr = $sub.propChng = $sub.propChng || "propertyChange", arrayChangeStr = $sub.arrChng = $sub.arrChng || "arrayChange", STRING = "string", HTML = "html", _ocp = "_ocp", // Observable contextual parameter syntaxError = $sub.syntaxErr, rFirstElem = /<(?!script)(\w+)[>\s]/, error = $sub._er, onRenderError = $sub._err, delimOpenChar0, delimOpenChar1, delimCloseChar0, delimCloseChar1, linkChar, topView, rEscapeQuotes = /['"\\]/g; // Escape quotes and \ character if ($.link) { return $; } // JsViews is already loaded $subSettings.trigger = true; var activeBody, rTagDatalink, $view, $viewsLinkAttr, linkViewsSel, wrapMap, viewStore, oldAdvSet, useInput, TEXTCONTENT = document.textContent !== undefined ? "textContent" : "innerText", jsvAttrStr = "data-jsv", elementChangeStr = "change.jsv", onBeforeChangeStr = "onBeforeChange", onAfterChangeStr = "onAfterChange", onAfterCreateStr = "onAfterCreate", CHECKED = "checked", CHECKBOX = "checkbox", RADIO = "radio", RADIOINPUT = "input[type=", CHECKBOXINPUT = RADIOINPUT + CHECKBOX + "]", // input[type=checkbox] NONE = "none", VALUE = "value", SCRIPT = "SCRIPT", TRUE = "true", PLAIN = "plaintext-only", closeScript = '"></script>', openScript = '<script type="jsv', deferAttr = jsvAttrStr + "-df", bindElsSel = "script,[" + jsvAttrStr + "]", fnSetters = { value: "val", input: "val", html: HTML, text: "text" }, valueBinding = {from: VALUE, to: VALUE}, isCleanCall = 0, oldCleanData = $.cleanData, oldJsvDelimiters = $viewsSettings.delimiters, linkExprStore = {}, // Compiled functions for data-link expressions safeFragment = document.createDocumentFragment(), qsa = document.querySelector, // elContent maps tagNames which have only element content, so may not support script nodes. elContent = {ol: 1, ul: 1, table: 1, tbody: 1, thead: 1, tfoot: 1, tr: 1, colgroup: 1, dl: 1, select: 1, optgroup: 1, svg: 1, svg_ns: 1}, badParent = {tr: "table"}, voidElems = {br: 1, img: 1, input: 1, hr: 1, area: 1, base: 1, col: 1, link: 1, meta: 1, command: 1, embed: 1, keygen: 1, param: 1, source: 1, track: 1, wbr: 1}, displayStyles = {}, bindingStore = {}, bindingKey = 1, rViewPath = /^#(view\.?)?/, rConvertMarkers = /((\/>)|<\/(\w+)>|)(\s*)([#/]\d+(?:_|(\^)))`(\s*)(<\w+(?=[\s\/>]))?|\s*(?:(<\w+(?=[\s\/>]))|<\/(\w+)>(\s*)|(\/>)\s*|(>)|$)/g, rOpenViewMarkers = /(#)()(\d+)(_)/g, rOpenMarkers = /(#)()(\d+)([_^])/g, rViewMarkers = /(?:(#)|(\/))(\d+)(_)/g, rTagMarkers = /(?:(#)|(\/))(\d+)(\^)/g, rOpenTagMarkers = /(#)()(\d+)(\^)/g, rMarkerTokens = /(?:(#)|(\/))(\d+)([_^])([-+@\d]+)?/g, rShallowArrayPath = /^[^.]*$/, // No '.' in path getComputedStyle = global.getComputedStyle, $inArray = $.inArray; RADIOINPUT += RADIO + "]"; // input[type=radio] $observable = $.observable; if (!$observable) { // JsObservable is not loaded. throw requiresStr + "jquery.observable.js"; // jquery.observable.js must be loaded before JsViews } $observe = $observable.observe; $subSettings._clFns = function() { linkExprStore = {}; }; //========================== Top-level functions ========================== //=============== // Event handlers //=============== function updateValues(sourceValues, tagElse, async, bindId, ev) { // 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.updateValues(val1, val2, val3, ...) - this: tag var linkCtx, cvtBack, cnvtName, target, view, binding, sourceValue, origVals, sourceElem, sourceEl, linkedElem, linkedElems, tos, to, tcpTag, exprOb, contextCb, l, m, tag, vals, ind; if (bindId && bindId._tgId) { tag = bindId; bindId = tag._tgId; if (!tag.bindTo) { defineBindToDataTargets(bindingStore[bindId], tag); // If this tag is updating for the first time, we need to create the 'to' bindings first tag.bindTo = [0]; } } if ((binding = bindingStore[bindId]) && (tos = binding.to)) { tos = tos[tagElse||0]; // The binding has a 'to' field, which is of the form [tosForElse0, tosForElse1, ...] // where tosForElseX is of the form [[[targetObject, toPath], [targetObject, toPath], ...], cvtBack] linkCtx = binding.linkCtx; sourceElem = linkCtx.elem; view = linkCtx.view; tag = linkCtx.tag; if (!tag && tos._cxp) { tag = tos._cxp.path !== _ocp && tos._cxp.tag; sourceValue = sourceValues[0]; sourceValues = []; sourceValues[tos._cxp.ind] = sourceValue; } if (tag) { tag._.chg = 1; // Set 'changing' marker to prevent tag update from updating itself if (cnvtName = tag.convertBack) { if ($isFunction(cnvtName)) { cvtBack = cnvtName; } else { cvtBack = view.getRsc("converters", cnvtName); } } } if (sourceElem.nodeName === "SELECT") { // data-link <select> to string or (multiselect) array of strings if (sourceElem.multiple && sourceValues[0] === null) { // Case where sourceValues was undefined, and set to [null] by $source[setter]() above sourceValues = [[]]; } sourceElem._jsvSel = sourceValues; } else if (sourceElem._jsvSel) { // Checkbox group (possibly using {{checkboxgroup}} tag) data-linking to array of strings tag = sourceElem._jsvLkEl; if (tag && tag.tagCtx.params.props.linkTo) { // checkboxgroup tag with linkTo property - binds to linkTo data linkedElems = tag.linkedElem; l = linkedElems.length; vals = []; while (l--) { linkedElem = linkedElems[l]; if (linkedElem.checked) { vals.unshift(linkedElem.value); // to follow same order as checkboxes } } } else { // checkboxgroup tag with no linkTo property - binds to source data vals = sourceElem._jsvSel.slice(); ind = $inArray(sourceElem.value, vals); if (ind > -1 && !sourceElem.checked) { vals.splice(ind, 1); // Unchecking this checkbox } else if (ind < 0 && sourceElem.checked) { vals.push(sourceElem.value); // Checking this checkbox } } sourceValues = [vals]; } origVals = sourceValues; l = tos.length; if (cvtBack) { sourceValues = cvtBack.apply(tag, sourceValues); if (sourceValues === undefined) { tos = []; // If cvtBack does not return anything, do not update target. //(But cvtBack may be designed to modify observable values from code as a side effect) } if (!$isArray(sourceValues) || (sourceValues.arg0 !== false && (l === 1 || sourceValues.length !== l || sourceValues.arg0))) { sourceValues = [sourceValues]; // If there are multiple tos (e.g. multiple args on data-linked input) then cvtBack can update not only // the first arg, but all of them by returning an array. } } while (l--) { if (to = tos[l]) { to = typeof to === STRING ? [linkCtx.data, to] : to; // [object, path] target = to[0]; tcpTag = to.tag; // If this is a tag contextual parameter - the owner tag sourceValue = (target && target._ocp && !target._vw ? origVals // If to target is for tag contextual parameter set to static expression (or uninitialized) - we are // binding to tag.ctx.foo._ocp - and we use original values, without applying cvtBack converter : sourceValues // Otherwise use the converted value )[l]; if (sourceValue !== undefined && (!tag || !tag.onBeforeUpdateVal || tag.onBeforeUpdateVal(ev, { change: "change", data: target, path: to[1], index: l, tagElse: tagElse, value: sourceValue }) !== false)) { if (tcpTag) { // We are modifying a tag contextual parameter ~foo (e.g. from within block) so update 'owner' tag: tcpTag if ((m = tcpTag._.toIndex[to.ind]) !== undefined) { tcpTag.updateValue(sourceValue, m, to.tagElse, undefined, undefined, ev); // if doesn't map, don't update, or update scoped tagCtxPrm. But should initialize from outer from binding... } tcpTag.setValue(sourceValue, to.ind, to.tagElse); } else if (sourceValue !== undefined && target) { if ((tcpTag = ev && (sourceEl = ev.target)._jsvInd === l && sourceEl._jsvLkEl) && (m = tcpTag._.fromIndex[l]) !== undefined) { // The source is a tag linkedElem (linkedElement: [..., "elemSelector", ...], which is updating tcpTag.setValue(origVals[l], m, sourceEl._jsvElse); } if (target._cpfn) { contextCb = linkCtx._ctxCb; // This is the exprOb for a computed property exprOb = target; target = linkCtx.data; if (exprOb._cpCtx) { // Computed value for a contextual parameter target = exprOb.data; // The data for the contextual view (where contextual param expression evaluated/assigned) contextCb = exprOb._cpCtx; // Context callback for contextual view } while (exprOb && exprOb.sb) { // Step through chained computed values to leaf one... target = contextCb(exprOb); exprOb = exprOb.sb; } } $observable(target, async).setProperty(to[1], sourceValue, undefined, to.isCpfn); // 2way binding change event - observably updating bound object } } } } } if (tag) { tag._.chg = undefined; // Clear marker return tag; } } function onElemChange(ev) { var bindId, val, source = ev.target, fromAttr = defaultAttr(source), setter = fnSetters[fromAttr], rSplitBindings = /&(\d+)\+?/g; if (!source._jsvTr || ev.delegateTarget !== activeBody && source.type !== "number" || ev.type === "input") { // If this is an element using trigger, ignore event delegated (bubbled) to activeBody val = $isFunction(fromAttr) ? fromAttr(source) : setter ? $(source)[setter]() : $(source).attr(fromAttr); source._jsvChg = 1; // // Set 'changing' marker to prevent linkedElem change event triggering its own refresh while (bindId = rSplitBindings.exec(source._jsvBnd)) { // _jsvBnd is a string with the syntax: "&bindingId1&bindingId2" updateValue(val, source._jsvInd, source._jsvElse, undefined, bindId[1], ev); } source._jsvChg = undefined; // Clear marker } } function onDataLinkedTagChange(ev, eventArgs) { // Update or initial rendering of any tag (including {{:}}) whether inline or data-linked element. var attr, sourceValue, noUpdate, forceUpdate, hasError, onError, bindEarly, tagCtx, l, linkCtx = this, linkFn = linkCtx.fn, tag = linkCtx.tag, source = linkCtx.data, target = linkCtx.elem, cvt = linkCtx.convert, parentElem = target.parentNode, view = linkCtx.view, oldLinkCtx = view._lc, onEvent = eventArgs && changeHandler(view, onBeforeChangeStr, tag); if (parentElem && (!onEvent || onEvent.call(tag || linkCtx, ev, eventArgs) !== false) // If data changed, the ev.data is set to be the path. Use that to filter the handler action... && (!eventArgs || ev.data.prop === "*" || ev.data.prop === eventArgs.path)) { // Set linkCtx on view, dynamically, just during this handler call view._lc = linkCtx; if (eventArgs || linkCtx._toLk) { // If eventArgs are defined, this is a data update // Otherwise this is the initial data-link rendering call. Bind on this the first time it gets called linkCtx._toLk = 0; // Remove flag to skip unneccessary rebinding next time if (linkFn._er) { // data-link="exprUsingTagOrCvt with onerror=..." - e.g. {tag ... {cvt:... {:... convert='cvt' try { sourceValue = linkFn(source, view, $sub); // Compiled link expression // For data-link="{:xxx}" with no cvt or cvtBk returns value. Otherwise returns tagCtxs } catch (e) { hasError = linkFn._er; onError = onRenderError(e,view,(new Function("data,view", "return " + hasError + ";"))(source, view)); sourceValue = [{props: {}, args: [onError], tag: tag}]; } } else { sourceValue = linkFn(source, view, $sub); // Compiled link expression // For data-link="{:xxx}" with no cvt or cvtBk returns value. Otherwise returns tagCtxs } // Compiled link expression for linkTag: return value for data-link="{:xxx}" with no cvt or cvtBk, otherwise tagCtx or tagCtxs attr = tag && tag.attr || linkCtx.attr || (linkCtx._dfAt = defaultAttr(target, true, cvt !== undefined)); if (linkCtx._dfAt === VALUE && (tag && tag.parentElem || linkCtx.elem).type === CHECKBOX) { attr = CHECKED; // This is a single checkbox (not in a checkboxgroup - which might have data-link="value{:...}" so linkCtx.attr === VALUE, but would not have _dfAt === VALUE) } // For {{: ...}} without a convert or convertBack, (tag and linkFn._tag undefined) we already have the sourceValue, and we are done if (tag) { // Existing tag instance forceUpdate = hasError || tag._er; // If the new tagCtxs hasError or the previous tagCtxs had error, then force update sourceValue = sourceValue[0] ? sourceValue : [sourceValue]; // Tag will update unless tag.onUpdate is false or is a function which returns false noUpdate = !forceUpdate && (tag.onUpdate === false || eventArgs && $isFunction(tag.onUpdate) && tag.onUpdate(ev, eventArgs, sourceValue) === false); mergeCtxs(tag, sourceValue, forceUpdate); // Merge new tagCtxs (in sourceValue var) with current tagCtxs on tag instance if (tag._.chg && (attr === HTML || attr === VALUE) || noUpdate || attr === NONE) { // This is an update coming from the tag itself (linkedElem change), or else onUpdate returned false, or attr === "none" callAfterLink(tag, ev, eventArgs); if (!tag._.chg) { // onUpdate returned false, or attr === "none" - so don't refresh the tag: we just use the new tagCtxs merged // from the sourceValue (which may optionally have been modifed in onUpdate()...) and then bind, and we are done observeAndBind(linkCtx, source, target); } // Remove dynamically added linkCtx from view view._lc = oldLinkCtx; if (eventArgs && (onEvent = changeHandler(view, onAfterChangeStr, tag))) { onEvent.call(tag || linkCtx, ev, eventArgs); } if (tag.tagCtx.props.dataMap) { tag.tagCtx.props.dataMap.map(tag.tagCtx.args[0], tag.tagCtx, tag.tagCtx.map, isRenderCall || !tag._.bnd); } return; } if (tag.onUnbind) { tag.onUnbind(tag.tagCtx, linkCtx, tag.ctx, ev, eventArgs); } tag.linkedElems = tag.linkedElem = tag.mainElem = tag.displayElem = undefined; l = tag.tagCtxs.length; while (l--) { tagCtx = tag.tagCtxs[l]; tagCtx.linkedElems = tagCtx.mainElem = tagCtx.displayElem = undefined; } sourceValue = tag.tagName === ":" // Call convertVal if it is a {{cvt:...}} - otherwise call renderTag ? $sub._cnvt(tag.convert, view, sourceValue[0]) // convertVal(converter, view, tagCtx, onError) : $sub._tag(tag, view, view.tmpl, sourceValue, true, onError); // renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) } else if (linkFn._tag) { // For {{: ...}} with either cvt or cvtBack we call convertVal to get the sourceValue and instantiate the tag // If cvt is undefined then this is a tag, and we call renderTag to get the rendered content and instantiate the tag cvt = cvt === "" ? TRUE : cvt; // If there is a cvtBack but no cvt, set cvt to "true" sourceValue = cvt // Call convertVal if it is a {{cvt:...}} - otherwise call renderTag ? $sub._cnvt(cvt, view, sourceValue[0] || sourceValue) // convertVal(converter, view, tagCtx, onError) : $sub._tag(linkFn._tag, view, view.tmpl, sourceValue, true, onError); // renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) addLinkMethods(tag = linkCtx.tag); // In both convertVal and renderTag we have instantiated a tag attr = linkCtx.attr || attr; // linkCtx.attr may have been set to tag.attr during tag instantiation in renderTag } if (bindEarly = tag && (!tag.inline || linkCtx.fn._lr) && tag.template) { // Data-linked tags with templated contents need to be data-linked before their contents, so that observable updates // will trigger the parent tags before the child tags. observeAndBind(linkCtx, source, target); } updateContent(sourceValue, linkCtx, attr, tag); linkCtx._noUpd = 0; // For data-link="^{...}" remove _noUpd flag so updates on subsequent calls if (tag) { tag._er = hasError; callAfterLink(tag, ev, eventArgs); } } if (!bindEarly) { observeAndBind(linkCtx, source, target); } if (tag && tag._.ths) { // Tag has a this=expr binding for which we have created an additional 'to' (defineBindToDataTargets) target (at index bindTo.length) // We now have the this pointer, so we push it to the binding, using updateValue(index) tag.updateValue(tag, tag.bindTo ? tag.bindTo.length : 1); // If bindTo not defined yet, it will be [0], so length 1 } if (eventArgs && (onEvent = changeHandler(view, onAfterChangeStr, tag))) { onEvent.call(tag || linkCtx, ev, eventArgs); } // Remove dynamically added linkCtx from view view._lc = oldLinkCtx; } } function setDefer(elem, value) { elem._df = value; // Use both an expando and an attribute to track deferred tokens. Attribute is needed for querySelectorAll for getViewInfos (childTags) elem[(value ? "set" : "remove") + "Attribute"](deferAttr, ""); } function updateContent(sourceValue, linkCtx, attr, tag) { // When called for a tag, either in tag.refresh() or onDataLinkedTagChange(), returns tag // When called (in onDataLinkedTagChange) for target HTML returns true // When called (in onDataLinkedTagChange) for other targets returns boolean for "change" var setter, prevNode, nextNode, late, nodesToRemove, useProp, tokens, id, openIndex, closeIndex, testElem, nodeName, cStyle, jsvSel, renders = attr !== NONE && sourceValue !== undefined && !linkCtx._noUpd && !((attr === VALUE || attr === HTML) && (!tag && linkCtx.elem._jsvChg)), // For data-link="^{...}", don't update the first time (no initial render) - e.g. to leave server rendered values. source = linkCtx.data, target = tag && tag.parentElem || linkCtx.elem, targetParent = target.parentNode, $target = $(target), view = linkCtx.view, targetVal = linkCtx._val, change = tag; if (tag) { // Initialize the tag with element references tag._.unlinked = true; // Set to unlinked, so initialization is triggered after re-rendering, e.g. for setting linkedElem, and calling onBind tag.parentElem = tag.parentElem || (linkCtx.expr || tag._elCnt) ? target : targetParent; prevNode = tag._prv; nextNode = tag._nxt; } if (!renders) { linkCtx._val = sourceValue; return; } if (attr === "visible") { attr = "css-display"; } if (/^css-/.test(attr)) { if (linkCtx.attr === "visible") { // Get the current display style cStyle = (target.currentStyle || getComputedStyle.call(global, target, "")).display; if (sourceValue) { // We are showing the element. // Get the cached 'visible' display value from the -jsvd expando sourceValue = target._jsvd // Or, if not yet cached, get the current display value || cStyle; if (sourceValue === NONE && !(sourceValue = displayStyles[nodeName = target.nodeName])) { // Currently display value is 'none', and the 'visible' style has not been cached. // We create an element to find the correct 'visible' display style for this nodeName testElem = document.createElement(nodeName); document.body.appendChild(testElem); // Get the default style for this HTML tag to use as 'visible' style sourceValue // and cache it as a hash against nodeName = displayStyles[nodeName] = (testElem.currentStyle || getComputedStyle.call(global, testElem, "")).display; document.body.removeChild(testElem); } } else { // We are hiding the element. // Cache the current display value as 'visible' style, on _jsvd expando, for when we show the element again target._jsvd = cStyle; sourceValue = NONE; // Hide the element } } if (change = change || targetVal !== sourceValue) { $.style(target, attr.slice(4), sourceValue); } } else if (attr !== "link") { // attr === "link" is for tag controls which do data binding but have no rendered output or target if (/^data-/.test(attr)) { $.data(target, attr.slice(5), sourceValue); // Support for binding to data attributes: data-foo{:expr}: data-foo attribute will be // expr.toString(), but $.data(element, "foo") and $(element).data("foo") will actually return value of expr, even if of type object } else if (/^prop-/.test(attr)) { useProp = true; attr = attr.slice(5); } else if (attr === CHECKED) { useProp = true; if (target.name && sourceValue == undefined && target._jsvLkEl && target._jsvLkEl.tagName === "checkboxgroup") { sourceValue = []; // If checkboxgroup bound to non-initialized array, we initialize as empty array. } if (target.name && $isArray(sourceValue)) { target._jsvSel = sourceValue; // Checkbox group (possibly using {{checkboxgroup}} tag) data-linking to array of strings sourceValue = $inArray(target.value, sourceValue) > -1; } else { sourceValue = sourceValue && sourceValue !== "false"; } // The string value "false" can occur with data-link="checked{attr:expr}" - as a result of attr, and hence using convertVal() // We will set the "checked" property // We will compare this with the current value } else if (attr === RADIO) { // This is a special binding attribute for radio buttons, which corresponds to the default 'to' binding. // This allows binding both to value (for each input) and to the default checked radio button (for each input in named group, // e.g. binding to parent data). // Place value binding first: <input type="radio" data-link="value{:name} {:#get('data').data.currency:} " .../> // or (allowing any order for the binding expressions): // <input type="radio" value="{{:name}}" data-link="{:#get('data').data.currency:} value^{:name}" .../> useProp = true; attr = CHECKED; sourceValue = target.value === sourceValue; // If the data value corresponds to the value attribute of this radio button input, set the checked property to true // Otherwise set the checked property to false } else if (attr === "selected" || attr === "disabled" || attr === "multiple" || attr === "readonly" || attr === "required") { sourceValue = (sourceValue && sourceValue !== "false") ? attr : null; // Use attr, not prop, so when the options (for example) are changed dynamically, but include the previously selected value, // they will still be selected after the change } else if (attr === VALUE && target.nodeName === "SELECT") { target._jsvSel = $isArray(sourceValue) ? sourceValue : "" + sourceValue; // If not array, coerce to string } if (setter = fnSetters[attr]) { if (attr === HTML) { if (tag && tag.inline) { nodesToRemove = tag.nodes(true); if (tag._elCnt) { if (prevNode && prevNode !== nextNode) { // nextNode !== prevNode // This prevNode will be removed from the DOM, so transfer the view tokens on prevNode to nextNode of this 'viewToRefresh' transferViewTokens(prevNode, nextNode, target, tag._tgId, "^", true); } else { // nextNode === prevNode, or there is no nextNode and so the target._df may have tokens tokens = prevNode ? prevNode.getAttribute(jsvAttrStr) : target._df; id = tag._tgId + "^"; openIndex = tokens.indexOf("#" + id) + 1; closeIndex = tokens.indexOf("/" + id); if (openIndex && closeIndex > 0) { // If prevNode, or target._df, include tokens referencing view and tag bindings contained within the open and close tokens // of the updated tag control, they need to be processed (disposed) openIndex += id.length; if (closeIndex > openIndex) { disposeTokens(tokens.slice(openIndex, closeIndex)); // Dispose view and tag bindings tokens = tokens.slice(0, openIndex) + tokens.slice(closeIndex); if (prevNode) { prevNode.setAttribute(jsvAttrStr, tokens); // Remove tokens of replaced content } else if (target._df) { // Remove tokens of replaced content setDefer(target, tokens); } } } } prevNode = prevNode ? prevNode.previousSibling : nextNode ? nextNode.previousSibling : target.lastChild; } // Remove HTML nodes $(nodesToRemove).remove(); // Note if !tag._elCnt removing the nodesToRemove will process and dispose view and tag bindings contained within the updated tag control // Insert and link new content late = view.link(view.data, target, prevNode, nextNode, sourceValue, tag && {tag: tag._tgId}); } else { // data-linked value targeting innerHTML: data-link="html{:expr}" or is contenteditable renders = renders && targetVal !== sourceValue; if (renders) { $target.empty(); late = view.link(source, target, prevNode, nextNode, sourceValue, tag && {tag: tag._tgId}); } } } else if (target._jsvSel) { $target[setter](sourceValue); // <select> (or multiselect) } else { if (change = change || targetVal !== sourceValue) { if (attr === "text" && target.children && !target.children[0]) { // This code is faster then $target.text() target[TEXTCONTENT] = sourceValue === null ? "" : sourceValue; } else { $target[setter](sourceValue); } } if ((jsvSel = targetParent._jsvSel) !== undefined // Setting value of <option> element && (attr === VALUE || $target.attr(VALUE) === undefined)) { // Setting value attribute, or setting textContent if attribute is null // Set/unselect selection based on value set on parent <select>. Works for multiselect too target.selected = $inArray("" + sourceValue, $isArray(jsvSel) ? jsvSel : [jsvSel]) > -1; } } } else if (change = change || targetVal !== sourceValue) { // Setting an attribute to undefined should remove the attribute $target[useProp ? "prop" : "attr"](attr, sourceValue === undefined && !useProp ? null : sourceValue); } } linkCtx._val = sourceValue; lateLink(late); // Do any deferred linking (lateRender) return change; } function arrayChangeHandler(ev, eventArgs) { // array change handler for 'array' views var self = this, onBeforeChange = changeHandler(self, onBeforeChangeStr, self.tag), onAfterChange = changeHandler(self, onAfterChangeStr, self.tag); if (!onBeforeChange || onBeforeChange.call(self, ev, eventArgs) !== false) { if (eventArgs) { // This is an observable action (not a trigger/handler call from pushValues, or similar, for which eventArgs will be null) var action = eventArgs.change, index = eventArgs.index, items = eventArgs.items; self._.srt = eventArgs.refresh; // true if part of a 'sort' on refresh switch (action) { case "insert": self.addViews(index, items, eventArgs._dly); break; case "remove": self.removeViews(index, items.length, undefined, eventArgs._dly); break; case "move": self.moveViews(eventArgs.oldIndex, index, items.length); break; case "refresh": self._.srt = undefined; self.fixIndex(0); // Other cases: (e.g.undefined, for setProperty on observable object) etc. do nothing } } if (onAfterChange) { onAfterChange.call(self, ev, eventArgs); } } } //============================= // Utilities for event handlers //============================= function setArrayChangeLink(view) { // Add/remove arrayChange handler on view var handler, arrayBinding, type = view.type, // undefined if view is being removed data = view.data, bound = view._.bnd; // true for top-level link() or data-link="{for}", or the for tag instance for {^{for}} (or for any custom tag that has an onArrayChange handler) if (!view._.useKey && bound) { // This is an array view. (view._.useKey not defined => data is array), and is data-bound to collection change events if (arrayBinding = view._.bndArr) { // First remove the current handler if there is one $([arrayBinding[1]]).off(arrayChangeStr, arrayBinding[0]); view._.bndArr = undefined; } if (bound !== !!bound) { // bound is not a boolean, so it is the data-linked tag that 'owns' this array binding - e.g. {^{for...}} if (type) { bound._.arrVws[view._.id] = view; } else { delete bound._.arrVws[view._.id]; // if view.type is undefined, view is being removed } } else if (type && data) { // If this view is not being removed, but the data array has been replaced, then bind to the new data array handler = function(ev) { if (!(ev.data && ev.data.off)) { // Skip if !!ev.data.off: - a handler that has already been removed (maybe was on handler collection at call time - then removed by another handler) // If view.type is undefined, do nothing. (Corresponds to case where there is another handler on the same data whose // effect was to remove this view, and which happened to precede this event in the trigger sequence. So although this // event has been removed now, it is still called since already on the trigger sequence) arrayChangeHandler.apply(view, arguments); } }; $([data]).on(arrayChangeStr, handler); view._.bndArr = [handler, data]; } } } function defaultAttr(elem, to, linkGetVal) { // to: true - default attribute for setting data value on HTML element; false: default attribute for getting value from HTML element // Merge in the default attribute bindings for this target element var nodeName = elem.nodeName.toLowerCase(), attr = $subSettingsAdvanced._fe[nodeName] // get form element binding settings for input textarea select or optgroup || (elem.contentEditable === TRUE || elem.contentEditable === PLAIN) && { to: HTML, from: HTML }; // Or if is contentEditable set attr to "html" return attr ? (to ? ((nodeName === "input" && elem.type === RADIO) // For radio buttons, bind from value, but bind to 'radio' - special value. ? RADIO : attr.to) : attr.from) : to ? linkGetVal ? "text" : HTML // Default innerText for data-link="a.b.c" or data-link="{:a.b.c}" (with or without converters)- otherwise innerHTML : ""; // Default is not to bind from } //============================== // Rendering and DOM insertion //============================== function renderAndLink(view, index, tmpl, views, data, context, refresh) { var html, linkToNode, prevView, nodesToRemove, bindId, parentNode = view.parentElem, prevNode = view._prv, nextNode = view._nxt, elCnt = view._elCnt; if (prevNode && prevNode.parentNode !== parentNode) { error("Missing parentNode"); // Abandon, since node has already been removed, or wrapper element has been inserted between prevNode and parentNode } if (refresh) { nodesToRemove = view.nodes(); if (elCnt && prevNode && prevNode !== nextNode) { // This prevNode will be removed from the DOM, so transfer the view tokens on prevNode to nextNode of this 'viewToRefresh' transferViewTokens(prevNode, nextNode, parentNode, view._.id, "_", true); } // Remove child views view.removeViews(undefined, undefined, true); linkToNode = nextNode; if (elCnt) { prevNode = prevNode ? prevNode.previousSibling : nextNode ? nextNode.previousSibling : parentNode.lastChild; } // Remove HTML nodes $(nodesToRemove).remove(); for (bindId in view._.bnds) { // The view bindings may have already been removed above in: $(nodesToRemove).remove(); // If not, remove them here: removeViewBinding(bindId); } } else { // addViews. Only called if view is of type "array" if (index) { // index is a number, so indexed view in view array prevView = views[index - 1]; if (!prevView) { return false; // If subview for provided index does not exist, do nothing } prevNode = prevView._nxt; } if (elCnt) { linkToNode = prevNode; prevNode = linkToNode ? linkToNode.previousSibling // There is a linkToNode, so insert after previousSibling, or at the beginning : parentNode.lastChild; // If no prevView and no prevNode, index is 0 and the container is empty, // so prevNode = linkToNode = null. But if prevView._nxt is null then we set prevNode to parentNode.lastChild // (which must be before the prevView) so we insert after that node - and only link the inserted nodes } else { linkToNode = prevNode.nextSibling; } } html = tmpl.render(data, context, view._.useKey && refresh, view, refresh || index, true); // Pass in view._.useKey as test for noIteration (which corresponds to when self._.useKey > 0 and self.data is an array) // Link the new HTML nodes to the data lateLink(view.link(data, parentNode, prevNode, linkToNode, html, prevView)); } //===================== // addBindingMarkers //===================== function addBindingMarkers(value, view, tag) { // Insert binding markers into the rendered template output, which will get converted to appropriate // data-jsv attributes (element-only content) or script marker nodes (phrasing or flow content), in convertMarkers, // within view.link, prior to inserting into the DOM. Linking will then bind based on these markers in the DOM. // Added view markers: #m_...VIEW.../m_ // Added tag markers: #m^...TAG..../m^ var id, end; if (tag) { // This is a binding marker for a data-linked tag {^{...}} end = "^`"; addLinkMethods(tag); // This is {^{>...}} or {^{tag ...}}, {{cvt:...} or {^{:...}}, and tag was defined in convertVal or renderTag id = tag._tgId; if (!id) { bindingStore[id = bindingKey++] = tag; // Store the tag temporarily, ready for databinding. // During linking, in addDataBinding, the tag will be attached to the linkCtx, // and then in observeAndBind, bindingStore[bindId] will be replaced by binding info. tag._tgId = "" + id; } } else { // This is a binding marker for a view // Add the view to the store of current linked views end = "_`"; viewStore[id = view._.id] = view; } // Example: "#23^TheValue/23^" return "#" + id + end + (value != undefined ? value : "") // For {^{:name}} this gives the equivalent semantics to compiled // (v=data.name)!=null?v:""; used in {{:name}} or data-link="name" + "/" + id + end; } //============================== // Data-linking and data binding //============================== //--------------- // observeAndBind //--------------- function observeAndBind(linkCtx, source, target) { var binding, l, k, linkedElem, exprFnDeps, exprOb, prop, propDeps, depends, tagDepends, bindId, linkedElems, tag = linkCtx.tag, allowArray = !tag, cvtBk = linkCtx.convertBack, handler = linkCtx._hdl; source = typeof source === "object" && source; // If not an object set to false if (tag) { // Use the 'depends' paths set on linkCtx.tag, or on the converter // - which may have been set on declaration or in events: init, render, onAfterLink etc. if (depends = tag.convert) { depends = depends === TRUE ? tag.tagCtx.props.convert : depends; depends = linkCtx.view.getRsc("converters", depends) || depends; depends = depends && depends.depends; depends = depends && $sub._dp(depends, source, handler); // dependsPaths } if (tagDepends = tag.tagCtx.props.depends || tag.depends) { tagDepends = $sub._dp(tagDepends, tag, handler); depends = depends ? depends.concat(tagDepends) : tagDepends; } linkedElems = tag.linkedElems; } depends = depends || []; if (!linkCtx._depends || ("" + linkCtx._depends !== "" + depends)) { // Only bind the first time, or if the new depends (toString) has changed from when last bound exprFnDeps = linkCtx.fn.deps.slice(); // Make a copy of the dependency paths for the compiled linkCtx expression - to pass to observe(). In getInnerCb(), // (and whenever the object is updated, in innerCb), we will set exprOb.ob to the current object returned by that computed expression, for this view. if (linkCtx._depends) { bindId = linkCtx._depends.bdId; // Unobserve previous binding $observable._apply(1, [source], exprFnDeps, linkCtx._depends, handler, linkCtx._ctxCb, true); } if (tag) { // Add dependency paths for declared boundProps (so no need to write ^myprop=... to get binding) and for linkedProp too if there is one l = tag.boundProps.length; while (l--) { prop = tag.boundProps[l]; k = tag._.bnd.paths.length; while (k--) { // Iterate across tagCtxs propDeps = tag._.bnd.paths[k]["_" + prop]; if (propDeps && propDeps.length && propDeps.skp) { // Not already a bound prop ^prop=expression; exprFnDeps = exprFnDeps.concat(propDeps); // Add dependencies for this prop expression } } } allowArray = tag.onArrayChange === undefined || tag.onArrayChange === true; } l = exprFnDeps.length; while (l--) { exprOb = exprFnDeps[l]; if (exprOb._cpfn) { // This path is an 'exprOb', corresponding to a computed property returning an object. We replace the exprOb by // a view-binding-specific exprOb instance. The current object will be stored as exprOb.ob. exprFnDeps[l] = $extend({}, exprOb); } } binding = $observable._apply( allowArray ? 0 : 1, // 'this' pointer for observeAndBind, used to set allowArray to 1 or 0. [source], exprFnDeps, // flatten the paths - to gather all the dependencies across args and bound params depends, handler, linkCtx._ctxCb); // The binding returned by $observe has a bnd array with the source objects of the individual bindings. if (!bindId) { bindId = linkCtx._bndId || "" + bindingKey++; linkCtx._bndId = undefined; // Store the binding key on the view and on the element, for disposal when the view is removed target._jsvBnd = (target._jsvBnd || "") + "&" + bindId; linkCtx.view._.bnds[bindId] = bindId; } binding.elem = target; // The target of all the individual bindings binding.linkCtx = linkCtx; binding._tgId = bindId; depends.bdId = bindId; linkCtx._depends = depends; // Store the binding. bindingStore[bindId] = binding; // Note: If this corresponds to a data-linked tag, we are replacing the // temporarily stored tag by the stored binding. The tag will now be at binding.linkCtx.tag if (linkedElems || cvtBk !== undefined || tag && tag.bindTo) { defineBindToDataTargets(binding, tag, cvtBk); } if (linkedElems) { l = linkedElems.length; while (l--) { linkedElem = linkedElems[l]; k = linkedElem && linkedElem.length; while (k--) { if (linkedElem[k]._jsvLkEl) { if (!linkedElem[k]._jsvBnd) { linkedElem[k]._jsvBnd = "&" + bindId + "+"; // Add a "+" for cloned binding - so removing // elems with cloned bindings will not remove the 'parent' binding from the bindingStore. } } else { linkedElem[k]._jsvLkEl = tag; bindLinkedElChange(tag, linkedElem[k]); linkedElem[k]._jsvBnd = "&" + bindId + "+"; } } } } else if (cvtBk !== undefined) { bindLinkedElChange(tag, target); } if (tag && !tag.inline) { if (!tag.flow) { target.setAttribute(jsvAttrStr, (target.getAttribute(jsvAttrStr)||"") + "#" + bindId + "^/" + bindId + "^"); } tag._tgId = "" + bindId; } } } //------- // $.link //------- function lateLink(late) { // Do any deferred linking (lateRender) var lnkCtx; if (late) { while (lnkCtx = late.pop()) { lnkCtx._hdl(); } } } function tmplLink(to, from, context, noIteration, parentView, prevNode, nextNode) { return $link(this, to, from, context, noIteration, parentView, prevNode, nextNode); } function $link(tmplOrLinkExpr, to, from, context, noIteration, parentView, prevNode, nextNode) { // When linking from a template, prevNode and nextNode parameters are ignored if (context === true) { noIteration = context; // passing boolean as third param - noIteration context = undefined; } else if (typeof context !== "object") { context = undefined; // context must be a boolean (noIteration) or a plain object } else { context = $extend({}, context); } if (tmplOrLinkExpr && to) { to = to.jquery ? to : $(to); // to is a jquery object or an element or selector if (!activeBody) { activeBody = document.body; useInput = "oninput" in activeBody; $(activeBody) .on(elementChangeStr, onElemChange) .on('blur.jsv', '[contenteditable]', onElemChange); } var i, k, html, vwInfos, view, placeholderParent, targetEl, refresh, late, prntView, onRender = addBindingMarkers, replaceMode = context && context.target === "replace", l = to.length; while (l--) { // iterate over 'to' targets. (Usually one, but can be multiple) targetEl = to[l]; prntView = parentView || $view(targetEl); if (typeof tmplOrLinkExpr === STRING) { // tmplOrLinkExpr is a string: treat as data-link expression. addDataBinding(late = [], tmplOrLinkExpr, targetEl, prntView, undefined, "expr", from, context); } else { if (tmplOrLinkExpr.markup !== undefined) { // This is a call to template.link() if (replaceMode) { placeholderParent = targetEl.parentNode; } prntView._.scp = true; // Set scope flag on prntView for link() call - used to set view.isTop for outermost view of created linked content html = tmplOrLinkExpr.render(from, context, noIteration, prntView, undefined, onRender, true); prntView._.scp = undefined; // TODO Consider finding a way to bind data (link) within template without html being different for each view, the HTML can // be evaluated once outside the while (l--), and pushed into a document fragment, then cloned and inserted at each target. if (placeholderParent) { // This is target="replace" mode prevNode = targetEl.previousSibling; nextNode = targetEl.nextSibling; $.cleanData([targetEl], true); placeholderParent.removeChild(targetEl); targetEl = placeholderParent; } else { prevNode = nextNode = undefined; // When linking from a template, prevNode and nextNode parameters are ignored $(targetEl).empty(); } } else if (tmplOrLinkExpr === true && prntView === topView) { // $.link(true, selector, data, ctx) - where selector points to elem in top-level content. (If not top-level content, no-op) refresh = {lnk: "top"}; } else { break; // no-op - $.link(true, selector, data, ctx) targeting within previously linked rendered template } // TODO Consider deferred linking API feature on per-template basis - {@{ instead of {^{ which allows the user to see the rendered content // before that content is linked, with better perceived perf. Have view.link return a deferred, and pass that to onAfterLink... // or something along those lines. // setTimeout(function() { if (targetEl._df && !nextNode) { // We are inserting new content and the target element has some deferred binding annotations,and there is no nextNode. // Those views may be stale views (that will be recreated in this new linking action) so we will first remove them // (if not already removed). vwInfos = viewInfos(targetEl._df, true, rOpenViewMarkers); for (i = 0, k = vwInfos.length; i < k; i++) { view = vwInfos[i]; if ((view = viewStore[view.id]) && view.data !== undefined) { // If this is the _prv (prevNode) for a view, remove the view // - unless view.data is undefined, in which case it is already being removed view.parent.removeViews(view._.key, undefined, true); } } setDefer(targetEl); // remove defer tokens } // Link the content of the element, since this is a call to template.link(), or to $(el).link(true, ...), late = prntView.link(from, targetEl, prevNode, nextNode, html, refresh, context); //}); } lateLink(late); // Do any deferred linking (lateRender) } } return to; // Allow chaining, to attach event handlers, etc. } //---------- // view.link //---------- function viewLink(outerData, parentNode, prevNode, nextNode, html, refresh, context, validateOnly) { // Optionally insert HTML into DOM using documentFragments (and wrapping HTML appropriately). // Data-link existing contents of parentNode, or the inserted HTML, if provided // Depending on the content model for the HTML elements, the standard data-linking markers inserted in the HTML by addBindingMarkers during // template rendering will be converted either to script marker nodes or, for element-only content sections, to data-jsv element annotations. // Data-linking will then add _prv and _nxt to views, where: // _prv: References the previous node (script element of type "jsv123"), or (for elCnt=true), the first element node in the view (or if none, set _prv = _nxt) // _nxt: References the last node (script element of type "jsv/123"), or (for elCnt=true), the next element node after the view. //==== nested functions ==== function convertMarkers(all, preceding, selfClose, closeTag, spaceBefore, id, boundId, spaceAfter, tag1, tag2, closeTag2, spaceAfterClose, selfClose2, endOpenTag) { // rConvertMarkers = /(^|(\/>)|<\/(\w+)>|)(\s*)([#/]\d+(?:_|(\^)))`(\s*)(<\w+(?=[\s\/>]))?|\s*(?:(<\w+(?=[\s\/>]))|<\/(\w+)>(\s*)|(\/>)\s*|(>))/g, // prec, slfCl, clsTag, spBefore, id, bndId spAfter,tag1, tag2, clTag2,sac slfCl2, endOpenTag // Convert the markers that were included by addBindingMarkers in template output, to appropriate DOM annotations: // data-jsv attributes (for element-only content) or script marker nodes (within phrasing or flow content). // TODO consider detecting 'quoted' contexts (attribute strings) so that attribute encoding does not need to encode > // Currently rAttrEncode = /[><"'&]/g includes '>' encoding in order to avoid erroneous parsing of <span title="&lt;a/>"></span>"> var errorMsg, bndId, endOfElCnt = ""; if (endOpenTag) { inTag = 0; return all; } tag = (tag1 || tag2 || "").toLowerCase(); closeTag = closeTag || closeTag2; selfClose = selfClose || selfClose2; if (isVoid && !selfClose && (!all || closeTag || tag || id && !inTag)) { // !all = end of string isVoid = undefined; parentTag = tagStack.shift(); // preceding tag was a void element, with no closing slash, such as <br>. } closeTag = closeTag || selfClose; if (closeTag) { closeTag = closeTag.toLowerCase(); inTag = 0; isVoid = undefined; // TODO: smart insertion of <tbody> - to be completed for robust insertion of deferred bindings etc. //if (closeTag === "table" && parentTag === "tbody") { // preceding = "</tbody>" + preceding; // parentTag = "table"; // tagStack.shift(); //} if (validate) { if (selfClose || selfClose2) { if (!voidElems[parentTag] && !/;svg;|;math