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
JavaScript
/*! 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="<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