Knockoutjs built latest

This commit is contained in:
the-djmaze 2024-03-03 19:20:51 +01:00
parent e3f781cffb
commit e97cf8fa6b
2 changed files with 204 additions and 365 deletions

View file

@ -824,8 +824,8 @@ ko.computed = (evaluatorFunctionOrOptions, options) => {
pure: false,
isSleeping: false,
readFunction: options["read"],
disposeWhenNodeIsRemoved: options["disposeWhenNodeIsRemoved"] || options.disposeWhenNodeIsRemoved || null,
disposeWhen: options["disposeWhen"] || options.disposeWhen,
disposeWhenNodeIsRemoved: options.disposeWhenNodeIsRemoved || null,
disposeWhen: options.disposeWhen,
domNodeDisposalCallback: null,
dependencyTracking: {},
dependenciesCount: 0,
@ -1004,14 +1004,8 @@ var computedFn = {
return target['subscribe'](this.evaluatePossiblyAsync, this);
},
evaluatePossiblyAsync() {
var computedObservable = this,
throttleEvaluationTimeout = computedObservable['throttleEvaluation'];
if (throttleEvaluationTimeout >= 0) {
clearTimeout(this[computedState].evaluationTimeoutInstance);
this[computedState].evaluationTimeoutInstance = setTimeout(() =>
computedObservable.evaluateImmediate(true /*notifyChange*/)
, throttleEvaluationTimeout);
} else if (computedObservable._evalDelayed) {
var computedObservable = this;
if (computedObservable._evalDelayed) {
computedObservable._evalDelayed(true /*isChange*/);
} else {
computedObservable.evaluateImmediate(true /*notifyChange*/);
@ -1285,7 +1279,7 @@ ko.selectExtensions = {
}
},
writeValue: (element, value, allowUnset) => {
writeValue: (element, value) => {
switch (element.nodeName) {
case 'OPTION':
if (typeof value === "string") {
@ -1314,7 +1308,7 @@ ko.selectExtensions = {
break;
}
}
if (allowUnset || selection >= 0 || (noValue && element.size > 1)) {
if (selection >= 0 || (noValue && element.size > 1)) {
element.selectedIndex = selection;
}
break;
@ -1438,9 +1432,6 @@ ko.expressionRewriting = (() => {
return result;
},
// Two-way bindings include a write function that allow the handler to update the value even if it's not an observable.
// twoWayBindings = new Set,
preProcessBindings = (bindingsStringOrKeyValueArray) => {
var resultStrings = [],
@ -1448,16 +1439,6 @@ ko.expressionRewriting = (() => {
keyValueArray = parseObjectLiteral(bindingsStringOrKeyValueArray),
processKeyValue = (key, val) => {
var /*writableVal,*/ obj = ko.bindingHandlers[key];
if (obj?.['preprocess'] && !obj['preprocess'](val, key, processKeyValue))
return;
/*
if (twoWayBindings.has(key) && (writableVal = getWriteableValue(val))) {
// For two-way bindings, provide a write method in case the value
// isn't a writable observable.
propertyAccessorResultStrings.push("'" + key + "':function(_z){" + writableVal + "=_z}");
}
*/
// Values are wrapped in a function so that each value can be accessed independently
val = 'function(){return ' + val + ' }';
resultStrings.push("'" + key + "':" + val);
@ -1474,10 +1455,6 @@ ko.expressionRewriting = (() => {
};
return {
bindingRewriteValidators: [],
// twoWayBindings: twoWayBindings,
parseObjectLiteral: parseObjectLiteral,
preProcessBindings: preProcessBindings,
@ -1696,7 +1673,7 @@ ko.bindingHandlers = {};
// The ko.bindingContext constructor is only called directly to create the root context. For child
// contexts, use bindingContext.createChildContext or bindingContext.extend.
ko.bindingContext = class {
constructor(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback, options)
constructor(dataItemOrAccessor, parentContext, extendCallback, options)
{
var self = this,
shouldInheritData = dataItemOrAccessor === inheritParentVm,
@ -1725,7 +1702,6 @@ ko.bindingContext = class {
self[contextAncestorBindingInfo] = parentContext[contextAncestorBindingInfo];
}
} else {
self['$parents'] = [];
self['$root'] = dataItem;
// Export 'ko' in the binding context so it will be available in bindings and templates
@ -1739,13 +1715,9 @@ ko.bindingContext = class {
if (shouldInheritData) {
dataItem = self['$data'];
} else {
self['$rawData'] = dataItemOrObservable;
self['$data'] = dataItem;
}
if (dataItemAlias)
self[dataItemAlias] = dataItem;
// The extendCallback function is provided when creating a child context or extending a context.
// It handles the specific actions needed to finish setting up the binding context. Actions in this
// function could also add dependencies to this binding context.
@ -1790,21 +1762,11 @@ ko.bindingContext = class {
// But this does not mean that the $data value of the child context will also get updated. If the child
// view model also depends on the parent view model, you must provide a function that returns the correct
// view model on each update.
'createChildContext'(dataItemOrAccessor, dataItemAlias, extendCallback, options) {
if (!options && dataItemAlias && typeof dataItemAlias == "object") {
options = dataItemAlias;
dataItemAlias = options['as'];
extendCallback = options['extend'];
}
return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, (self, parentContext) => {
'createChildContext'(dataItemOrAccessor, options) {
return new ko.bindingContext(dataItemOrAccessor, this, (self, parentContext) => {
// Extend the context hierarchy by setting the appropriate pointers
self['$parentContext'] = parentContext;
self['$parent'] = parentContext['$data'];
self['$parents'] = (parentContext['$parents'] || []).slice(0);
self['$parents'].unshift(self['$parent']);
if (extendCallback)
extendCallback(self);
options['extend']?.(self);
}, options);
}
@ -1812,7 +1774,7 @@ ko.bindingContext = class {
// Similarly to "child" contexts, provide a function here to make sure that the correct values are set
// when an observable view model is updated.
'extend'(properties, options) {
return new ko.bindingContext(inheritParentVm, this, null, self =>
return new ko.bindingContext(inheritParentVm, this, self =>
ko.utils.extend(self, typeof(properties) == "function" ? properties(self) : properties)
, options);
}
@ -1909,12 +1871,6 @@ ko.bindingEvent = {
}
};
function validateThatBindingIsAllowedForVirtualElements(bindingName) {
var validator = ko.virtualElements.allowedBindings[bindingName];
if (!validator)
throw new Error("The binding '" + bindingName + "' cannot be used with virtual elements")
}
function applyBindingsToDescendantsInternal(bindingContext, elementOrVirtualElement) {
var currentChild, nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement);
@ -1935,7 +1891,7 @@ function applyBindingsToNodeAndDescendantsInternal(bindingContext, nodeVerified)
// (1) We need to store the binding info for the node (all element nodes)
// (2) It might have bindings (e.g., it has a data-bind attribute, or it's a marker for a containerless template)
if (isElement || ko.bindingProvider.nodeHasBindings(nodeVerified))
bindingContextForDescendants = applyBindingsToNodeInternal(nodeVerified, null, bindingContext)['bindingContextForDescendants'];
bindingContextForDescendants = applyBindingsToNodeInternal(nodeVerified, null, bindingContext);
// Don't want bindings that operate on text nodes to mutate <script> and <textarea> contents,
// because it's unexpected and a potential XSS issue.
@ -2067,8 +2023,9 @@ function applyBindingsToNodeInternal(node, sourceBindings, bindingContext) {
handlerUpdateFn = bindingKeyAndHandler.handler["update"],
bindingKey = bindingKeyAndHandler.key;
if (node.nodeType === 8) {
validateThatBindingIsAllowedForVirtualElements(bindingKey);
// COMMENT_NODE
if (node.nodeType === 8 && !ko.virtualElements.allowedBindings[bindingKey]) {
throw new Error("The binding '" + bindingKey + "' cannot be used with comment nodes");
}
try {
@ -2100,11 +2057,7 @@ function applyBindingsToNodeInternal(node, sourceBindings, bindingContext) {
});
}
var shouldBindDescendants = bindingHandlerThatControlsDescendantBindings === undefined;
return {
'shouldBindDescendants': shouldBindDescendants,
'bindingContextForDescendants': shouldBindDescendants && contextToExtend
};
return bindingHandlerThatControlsDescendantBindings === undefined && contextToExtend;
}
ko.storedBindingContextForNode = node => {
@ -2115,7 +2068,7 @@ ko.storedBindingContextForNode = node => {
function getBindingContext(viewModelOrBindingContext, extendContextCallback) {
return viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext)
? viewModelOrBindingContext
: new ko.bindingContext(viewModelOrBindingContext, undefined, undefined, extendContextCallback);
: new ko.bindingContext(viewModelOrBindingContext, null, extendContextCallback);
}
ko['applyBindingAccessorsToNode'] = (node, bindings, viewModelOrBindingContext) =>
@ -2273,8 +2226,7 @@ ko.exportSymbol('applyBindings', ko.applyBindings);
currentViewModel = null;
// Any in-flight loading operation is no longer relevant, so make sure we ignore its completion
currentLoadingOperationId = null;
},
originalChildNodes = [...ko.virtualElements.childNodes(element)];
};
ko.virtualElements.emptyNode(element);
ko.utils.domNodeDisposal['addDisposeCallback'](element, disposeAssociatedComponentViewModel);
@ -2313,15 +2265,9 @@ ko.exportSymbol('applyBindings', ko.applyBindings);
ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(template));
currentViewModel = componentDefinition['createViewModel'](componentParams, {
'element': element,
'templateNodes': originalChildNodes
'element': element
});
ko.applyBindingsToDescendants(asyncContext['createChildContext'](currentViewModel, {
'extend': ctx => {
ctx['$component'] = currentViewModel;
ctx['$componentTemplateNodes'] = originalChildNodes;
}
}), element);
ko.applyBindingsToDescendants(asyncContext['createChildContext'](currentViewModel, {}), element);
}
});
}, { disposeWhenNodeIsRemoved: element });
@ -2353,14 +2299,6 @@ ko.bindingHandlers['attr'] = {
attrValue = attrValue.toString();
namespace ? element.setAttributeNS(namespace, attrName, attrValue) : element.setAttribute(attrName, attrValue);
}
// Treat "name" specially - although you can think of it as an attribute, it also needs
// special handling on older versions of IE (https://github.com/SteveSanderson/knockout/pull/333)
// Deliberately being case-sensitive here because XHTML would regard "Name" as a different thing
// entirely, and there's no strong reason to allow for such casing in HTML.
if (attrName === "name") {
element.name = toRemove ? "" : attrValue;
}
});
}
};
@ -2444,10 +2382,6 @@ ko.bindingHandlers['checked'] = {
useElementValue = isRadio || valueIsArray,
oldElemValue = valueIsArray ? checkedValue() : undefined;
// IE 6 won't allow radio buttons to be selected unless they have a name
if (isRadio && !element.name)
ko.bindingHandlers['uniqueName']['init'](element, function() { return true });
// Set up two computeds to update the binding:
// The first responds to changes in the checkedValue value and to element clicks
@ -2479,7 +2413,6 @@ ko.bindingHandlers['checked'] = {
}
}
};
//ko.expressionRewriting.twoWayBindings['checked'] = true;
ko.bindingHandlers['checkedValue'] = {
'update'(element, valueAccessor) {
@ -2577,9 +2510,7 @@ ko.bindingHandlers['foreach'] = {
// If unwrappedValue.data is the array, preserve all relevant options and unwrap again value so we get updates
ko.utils.unwrapObservable(modelValue);
return {
'foreach': unwrappedValue['data'],
'as': unwrappedValue['as'],
'beforeRemove': unwrappedValue['beforeRemove']
'foreach': unwrappedValue['data']
};
},
'init': (element, valueAccessor) =>
@ -2588,7 +2519,6 @@ ko.bindingHandlers['foreach'] = {
'update': (element, valueAccessor, allBindings, viewModel, bindingContext) =>
ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindings, viewModel, bindingContext)
};
ko.expressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
ko.virtualElements.allowedBindings['foreach'] = true;
const hasfocusUpdatingProperty = '__ko_hasfocusUpdating',
hasfocusLastValue = '__ko_hasfocusLastValue';
@ -2628,7 +2558,6 @@ ko.bindingHandlers['hasfocus'] = {
}
}
};
//ko.expressionRewriting.twoWayBindings.add('hasfocus');
ko.bindingHandlers['html'] = {
'init': () => (
// Prevent binding on the dynamically-injected HTML (as developers are unlikely to expect that, and it has security implications)
@ -2657,7 +2586,7 @@ function makeWithIfBinding(bindingKey, isWith, isNot) {
var savedNodes, contextOptions = {}, needAsyncContext;
if (isWith) {
contextOptions = { 'as': allBindings.get('as'), 'exportDependencies': true };
contextOptions = { 'exportDependencies': true };
}
needAsyncContext = allBindings['has'](ko.bindingEvent.descendantsComplete);
@ -2703,7 +2632,6 @@ function makeWithIfBinding(bindingKey, isWith, isNot) {
return { 'controlsDescendantBindings': true };
}
};
ko.expressionRewriting.bindingRewriteValidators[bindingKey] = false; // Can't rewrite control flow bindings
ko.virtualElements.allowedBindings[bindingKey] = true;
}
@ -2734,7 +2662,6 @@ ko.bindingHandlers['options'] = {
multiple = element.multiple,
previousScrollTop = (!selectWasPreviouslyEmpty && multiple) ? element.scrollTop : null,
unwrappedArray = ko.utils.unwrapObservable(valueAccessor()),
valueAllowUnset = allBindings.get('valueAllowUnset') && allBindings['has']('value'),
arrayToDomNodeChildrenOptions = {},
captionValue,
filteredArray,
@ -2753,10 +2680,7 @@ ko.bindingHandlers['options'] = {
},
setSelectionCallback = (arrayEntry, newOptions) => {
if (itemUpdate && valueAllowUnset) {
// The model value is authoritative, so make sure its value is the one selected
ko.bindingEvent.notify(element, ko.bindingEvent.childrenComplete);
} else if (previousSelectedValues.length) {
if (previousSelectedValues.length) {
// IE6 doesn't like us to assign selection to OPTION nodes before they're added to the document.
// That's why we first added them without selection. Now it's time to set the selection.
var isSelected = previousSelectedValues.includes(ko.selectExtensions.readValue(newOptions[0]));
@ -2769,29 +2693,18 @@ ko.bindingHandlers['options'] = {
}
};
if (!valueAllowUnset) {
if (multiple) {
previousSelectedValues = selectedOptions().map(ko.selectExtensions.readValue);
} else if (element.selectedIndex >= 0) {
previousSelectedValues.push(ko.selectExtensions.readValue(element.options[element.selectedIndex]));
}
if (multiple) {
previousSelectedValues = selectedOptions().map(ko.selectExtensions.readValue);
} else if (element.selectedIndex >= 0) {
previousSelectedValues.push(ko.selectExtensions.readValue(element.options[element.selectedIndex]));
}
if (unwrappedArray) {
if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
if (!Array.isArray(unwrappedArray)) // Coerce single value into array
unwrappedArray = [unwrappedArray];
// Filter out any entries marked as destroyed
filteredArray = unwrappedArray.filter(item => item || item == null);
// If caption is included, add it to the array
if (allBindings['has']('optionsCaption')) {
captionValue = ko.utils.unwrapObservable(allBindings.get('optionsCaption'));
// If caption value is null or undefined, don't show a caption
if (captionValue != null) {
filteredArray.unshift(captionPlaceholder);
}
}
} else {
// If a falsy value is provided (e.g. null), we'll simply empty the select element
}
@ -2803,12 +2716,12 @@ ko.bindingHandlers['options'] = {
var itemUpdate = false,
optionForArrayItem = (arrayEntry, index, oldOptions) => {
if (oldOptions.length) {
previousSelectedValues = !valueAllowUnset && oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : [];
previousSelectedValues = oldOptions[0].selected ? [ ko.selectExtensions.readValue(oldOptions[0]) ] : [];
itemUpdate = true;
}
var option = element.ownerDocument.createElement("option");
if (arrayEntry === captionPlaceholder) {
ko.utils.setTextContent(option, allBindings.get('optionsCaption'));
ko.utils.setTextContent(option);
ko.selectExtensions.writeValue(option, undefined);
} else {
// Apply a value to the option element
@ -2822,10 +2735,6 @@ ko.bindingHandlers['options'] = {
return [option];
};
// By using a beforeRemove callback, we delay the removal until after new items are added. This fixes a selection
// problem in IE<=8 and Firefox. See https://github.com/knockout/knockout/issues/1208
arrayToDomNodeChildrenOptions['beforeRemove'] = option => element.removeChild(option);
var callback = setSelectionCallback;
if (allBindings['has']('optionsAfterRender') && typeof allBindings.get('optionsAfterRender') == "function") {
callback = (arrayEntry, newOptions) => {
@ -2836,28 +2745,26 @@ ko.bindingHandlers['options'] = {
ko.utils.setDomNodeChildrenFromArrayMapping(element, filteredArray, optionForArrayItem, arrayToDomNodeChildrenOptions, callback);
if (!valueAllowUnset) {
// Determine if the selection has changed as a result of updating the options list
var selectionChanged, prevLength = previousSelectedValues.length;
if (multiple) {
// For a multiple-select box, compare the new selection count to the previous one
// But if nothing was selected before, the selection can't have changed
selectionChanged = prevLength && selectedOptions().length < prevLength;
} else {
// For a single-select box, compare the current value to the previous value
// But if nothing was selected before or nothing is selected now, just look for a change in selection
selectionChanged = (prevLength && element.selectedIndex >= 0)
? (ko.selectExtensions.readValue(element.options[element.selectedIndex]) !== previousSelectedValues[0])
: (prevLength || element.selectedIndex >= 0);
}
// Ensure consistency between model value and selected option.
// If the dropdown was changed so that selection is no longer the same,
// notify the value or selectedOptions binding.
selectionChanged && ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
// Determine if the selection has changed as a result of updating the options list
var selectionChanged, prevLength = previousSelectedValues.length;
if (multiple) {
// For a multiple-select box, compare the new selection count to the previous one
// But if nothing was selected before, the selection can't have changed
selectionChanged = prevLength && selectedOptions().length < prevLength;
} else {
// For a single-select box, compare the current value to the previous value
// But if nothing was selected before or nothing is selected now, just look for a change in selection
selectionChanged = (prevLength && element.selectedIndex >= 0)
? (ko.selectExtensions.readValue(element.options[element.selectedIndex]) !== previousSelectedValues[0])
: (prevLength || element.selectedIndex >= 0);
}
if (valueAllowUnset || ko.dependencyDetection.isInitial()) {
// Ensure consistency between model value and selected option.
// If the dropdown was changed so that selection is no longer the same,
// notify the value or selectedOptions binding.
selectionChanged && ko.dependencyDetection.ignore(ko.utils.triggerEvent, null, [element, "change"]);
if (ko.dependencyDetection.isInitial()) {
ko.bindingEvent.notify(element, ko.bindingEvent.childrenComplete);
}
@ -2970,19 +2877,9 @@ ko.bindingHandlers['textInput'] = {
// Bind to the change event so that we can catch programmatic updates of the value that fire this event.
onEvent('change', updateModel);
// To deal with browsers that don't notify any kind of event for some changes (IE, Safari, etc.)
onEvent('blur', updateModel);
ko.computed(updateView, { disposeWhenNodeIsRemoved: element });
}
};
//ko.expressionRewriting.twoWayBindings.add('textInput');
// textinput is an alias for textInput
ko.bindingHandlers['textinput'] = {
// preprocess is the only way to set up a full alias
'preprocess': (value, name, addBinding) => addBinding('textInput', value)
};
ko.bindingHandlers['value'] = {
'init': (element, valueAccessor, allBindings) => {
var isSelectElement = element.matches("SELECT"),
@ -3064,9 +2961,8 @@ ko.bindingHandlers['value'] = {
if (valueHasChanged || elementValue === undefined) {
if (isSelectElement) {
var allowUnset = allBindings.get('valueAllowUnset');
ko.selectExtensions.writeValue(element, newValue, allowUnset);
if (!allowUnset && newValue !== ko.selectExtensions.readValue(element)) {
ko.selectExtensions.writeValue(element, newValue);
if (newValue !== ko.selectExtensions.readValue(element)) {
// If you try to set a model value that can't be represented in an already-populated dropdown, reject that change,
// because you're not allowed to have a model value that disagrees with a visible UI selection.
ko.dependencyDetection.ignore(valueUpdateHandler);
@ -3097,7 +2993,6 @@ ko.bindingHandlers['value'] = {
},
'update': () => {} // Keep for backwards compatibility with code that may have wrapped value binding
};
//ko.expressionRewriting.twoWayBindings.add('value');
ko.bindingHandlers['visible'] = {
'update': (element, valueAccessor) => {
var value = ko.utils.unwrapObservable(valueAccessor());
@ -3174,7 +3069,7 @@ makeEventHandlerShortcut('click');
};
})();
(() => {
var renderTemplateSource = templateSource => {
const renderTemplateSource = templateSource => {
var templateNodes = templateSource.nodes ? templateSource.nodes() : null;
return templateNodes
? [...templateNodes.cloneNode(true).childNodes]
@ -3235,7 +3130,7 @@ makeEventHandlerShortcut('click');
: null;
},
executeTemplate = (targetNodeOrNodeArray, renderMode, template, bindingContext) => {
executeTemplate = (targetNodeOrNodeArray, replaceChildren, template, bindingContext) => {
var firstTargetNode = targetNodeOrNodeArray && getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
var templateDocument = (firstTargetNode || template || {}).ownerDocument;
@ -3245,22 +3140,10 @@ makeEventHandlerShortcut('click');
if ((typeof renderedNodesArray.length != "number") || (renderedNodesArray.length > 0 && typeof renderedNodesArray[0].nodeType != "number"))
throw new Error("Template engine must return an array of DOM nodes");
var haveAddedNodesToParent = false;
switch (renderMode) {
case "replaceChildren":
ko.virtualElements.setDomNodeChildren(targetNodeOrNodeArray, renderedNodesArray);
haveAddedNodesToParent = true;
break;
case "ignoreTargetNode": break;
default:
throw new Error("Unknown renderMode: " + renderMode);
}
if (haveAddedNodesToParent) {
if (replaceChildren) {
ko.virtualElements.setDomNodeChildren(targetNodeOrNodeArray, renderedNodesArray);
activateBindingsOnContinuousNodeArray(renderedNodesArray, bindingContext);
if (renderMode == "replaceChildren") {
ko.bindingEvent.notify(targetNodeOrNodeArray, ko.bindingEvent.childrenComplete);
}
ko.bindingEvent.notify(targetNodeOrNodeArray, ko.bindingEvent.childrenComplete);
}
return renderedNodesArray;
@ -3274,100 +3157,94 @@ makeEventHandlerShortcut('click');
}
// 2. A function of (data, context) returning a string ELSE 3. A string
return (typeof template === 'function') ? template(data, context) : template;
};
},
ko.renderTemplate = function (template, dataOrBindingContext, options, targetNodeOrNodeArray, renderMode) {
options = options || {};
renderMode = renderMode || "replaceChildren";
renderTemplate = (template, dataOrBindingContext, options, targetNodeOrNodeArray) => {
options = options || {};
if (targetNodeOrNodeArray) {
var firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
if (targetNodeOrNodeArray) {
var firstTargetNode = getFirstNodeFromPossibleArray(targetNodeOrNodeArray);
var whenToDispose = () => (!firstTargetNode) || !ko.utils.domNodeIsAttachedToDocument(firstTargetNode); // Passive disposal (on next evaluation)
var whenToDispose = () => (!firstTargetNode) || !ko.utils.domNodeIsAttachedToDocument(firstTargetNode); // Passive disposal (on next evaluation)
return ko.computed( // So the DOM is automatically updated when any dependency changes
() => {
// Ensure we've got a proper binding context to work with
var bindingContext = (dataOrBindingContext instanceof ko.bindingContext)
? dataOrBindingContext
: new ko.bindingContext(dataOrBindingContext, null, null, null, { "exportDependencies": true });
return ko.computed( // So the DOM is automatically updated when any dependency changes
() => {
// Ensure we've got a proper binding context to work with
var bindingContext = (dataOrBindingContext instanceof ko.bindingContext)
? dataOrBindingContext
: new ko.bindingContext(dataOrBindingContext, null, null, { "exportDependencies": true });
var templateName = resolveTemplateName(template, bindingContext['$data'], bindingContext);
executeTemplate(targetNodeOrNodeArray, renderMode, templateName, bindingContext, options);
},
{ disposeWhen: whenToDispose, disposeWhenNodeIsRemoved: firstTargetNode }
);
} else {
console.log('no targetNodeOrNodeArray');
}
};
var templateName = resolveTemplateName(template, bindingContext['$data'], bindingContext);
executeTemplate(targetNodeOrNodeArray, true, templateName, bindingContext, options);
},
{ disposeWhen: whenToDispose, disposeWhenNodeIsRemoved: firstTargetNode }
);
} else {
console.log('no targetNodeOrNodeArray');
}
},
ko.renderTemplateForEach = (template, arrayOrObservableArray, options, targetNode, parentBindingContext) => {
// Since setDomNodeChildrenFromArrayMapping always calls executeTemplateForArrayItem and then
// activateBindingsCallback for added items, we can store the binding context in the former to use in the latter.
var arrayItemContext, asName = options['as'];
renderTemplateForEach = (template, arrayOrObservableArray, options, targetNode, parentBindingContext) => {
// Since setDomNodeChildrenFromArrayMapping always calls executeTemplateForArrayItem and then
// activateBindingsCallback for added items, we can store the binding context in the former to use in the latter.
var arrayItemContext;
// This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
var executeTemplateForArrayItem = (arrayValue, index) => {
// Support selecting template as a function of the data being rendered
arrayItemContext = parentBindingContext['createChildContext'](arrayValue, {
'as': asName,
'extend': context => {
context['$index'] = index;
if (asName) {
context[asName + "Index"] = index;
}
}
});
// This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
var executeTemplateForArrayItem = (arrayValue, index) => {
// Support selecting template as a function of the data being rendered
arrayItemContext = parentBindingContext['createChildContext'](arrayValue, {
'extend': context => context['$index'] = index
});
var templateName = resolveTemplateName(template, arrayValue, arrayItemContext);
return executeTemplate(targetNode, "ignoreTargetNode", templateName, arrayItemContext, options);
};
var templateName = resolveTemplateName(template, arrayValue, arrayItemContext);
return executeTemplate(targetNode, false, templateName, arrayItemContext, options);
};
// This will be called whenever setDomNodeChildrenFromArrayMapping has added nodes to targetNode
var activateBindingsCallback = (arrayValue, addedNodesArray) => {
activateBindingsOnContinuousNodeArray(addedNodesArray, arrayItemContext);
// This will be called whenever setDomNodeChildrenFromArrayMapping has added nodes to targetNode
var activateBindingsCallback = (arrayValue, addedNodesArray) => {
activateBindingsOnContinuousNodeArray(addedNodesArray, arrayItemContext);
// release the "cache" variable, so that it can be collected by
// the GC when its value isn't used from within the bindings anymore.
arrayItemContext = null;
};
// release the "cache" variable, so that it can be collected by
// the GC when its value isn't used from within the bindings anymore.
arrayItemContext = null;
};
var setDomNodeChildrenFromArrayMapping = function (newArray, changeList) {
// Call setDomNodeChildrenFromArrayMapping, ignoring any observables unwrapped within (most likely from a callback function).
// If the array items are observables, though, they will be unwrapped in executeTemplateForArrayItem and managed within setDomNodeChildrenFromArrayMapping.
ko.dependencyDetection.ignore(ko.utils.setDomNodeChildrenFromArrayMapping, null, [targetNode, newArray, executeTemplateForArrayItem, options, activateBindingsCallback, changeList]);
ko.bindingEvent.notify(targetNode, ko.bindingEvent.childrenComplete);
};
var setDomNodeChildrenFromArrayMapping = (newArray, changeList) => {
// Call setDomNodeChildrenFromArrayMapping, ignoring any observables unwrapped within (most likely from a callback function).
// If the array items are observables, though, they will be unwrapped in executeTemplateForArrayItem and managed within setDomNodeChildrenFromArrayMapping.
ko.dependencyDetection.ignore(ko.utils.setDomNodeChildrenFromArrayMapping, null, [targetNode, newArray, executeTemplateForArrayItem, options, activateBindingsCallback, changeList]);
ko.bindingEvent.notify(targetNode, ko.bindingEvent.childrenComplete);
};
if (!options['beforeRemove'] && ko['isObservableArray'](arrayOrObservableArray)) {
setDomNodeChildrenFromArrayMapping(arrayOrObservableArray.peek());
if (ko['isObservableArray'](arrayOrObservableArray)) {
setDomNodeChildrenFromArrayMapping(arrayOrObservableArray.peek());
var subscription = arrayOrObservableArray['subscribe'](changeList => {
setDomNodeChildrenFromArrayMapping(arrayOrObservableArray(), changeList);
}, null, "arrayChange");
subscription.disposeWhenNodeIsRemoved(targetNode);
var subscription = arrayOrObservableArray['subscribe'](changeList => {
setDomNodeChildrenFromArrayMapping(arrayOrObservableArray(), changeList);
}, null, "arrayChange");
subscription.disposeWhenNodeIsRemoved(targetNode);
return subscription;
}
return ko.computed(() => {
var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray) || [];
if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
unwrappedArray = [unwrappedArray];
return subscription;
}
return ko.computed(() => {
var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray) || [];
if (!Array.isArray(unwrappedArray)) // Coerce single value into array
unwrappedArray = [unwrappedArray];
setDomNodeChildrenFromArrayMapping(unwrappedArray);
setDomNodeChildrenFromArrayMapping(unwrappedArray);
}, { disposeWhenNodeIsRemoved: targetNode });
};
}, { disposeWhenNodeIsRemoved: targetNode });
},
var templateComputedDomDataKey = ko.utils.domData.nextKey();
function disposeOldComputedAndStoreNewOne(element, newComputed) {
var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);
oldComputed?.['dispose']?.();
ko.utils.domData.set(element, templateComputedDomDataKey, (newComputed && (!newComputed.isActive || newComputed.isActive())) ? newComputed : undefined);
}
templateComputedDomDataKey = ko.utils.domData.nextKey(),
disposeOldComputedAndStoreNewOne = (element, newComputed) => {
var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);
oldComputed?.['dispose']?.();
ko.utils.domData.set(element, templateComputedDomDataKey, (newComputed && (!newComputed.isActive || newComputed.isActive())) ? newComputed : undefined);
},
cleanContainerDomDataKey = ko.utils.domData.nextKey();
var cleanContainerDomDataKey = ko.utils.domData.nextKey();
ko.bindingHandlers['template'] = {
'init': (element, valueAccessor) => {
// Support anonymous templates
@ -3433,8 +3310,7 @@ makeEventHandlerShortcut('click');
if ('foreach' in options) {
// Render once for each data point (treating data set as empty if shouldDisplay==false)
var dataArray = (shouldDisplay && options['foreach']) || [];
templateComputed = ko.renderTemplateForEach(template, dataArray, options, element, bindingContext);
templateComputed = renderTemplateForEach(template, (shouldDisplay && options['foreach']) || [], options, element, bindingContext);
} else if (!shouldDisplay) {
ko.virtualElements.emptyNode(element);
} else {
@ -3442,11 +3318,10 @@ makeEventHandlerShortcut('click');
var innerBindingContext = bindingContext;
if ('data' in options) {
innerBindingContext = bindingContext['createChildContext'](options['data'], {
'as': options['as'],
'exportDependencies': true
});
}
templateComputed = ko.renderTemplate(template, innerBindingContext, options, element);
templateComputed = renderTemplate(template, innerBindingContext, options, element);
}
// It only makes sense to have a single template computed per element (otherwise which one should have its output displayed?)
@ -3454,18 +3329,6 @@ makeEventHandlerShortcut('click');
}
};
// Anonymous templates can't be rewritten. Give a nice error message if you try to do it.
ko.expressionRewriting.bindingRewriteValidators['template'] = bindingValue => {
var parsedBindingValue = ko.expressionRewriting.parseObjectLiteral(bindingValue);
if ((parsedBindingValue.length == 1) && parsedBindingValue[0]['unknown'])
return null; // It looks like a string literal, not an object literal, so treat it as a named template (which is allowed for rewriting)
if (ko.expressionRewriting.keyValueArrayContainsKey(parsedBindingValue, "name"))
return null; // Named templates can be rewritten, so return "no error"
return "This template engine does not support anonymous templates nested within its templates";
};
ko.virtualElements.allowedBindings['template'] = true;
})();
// Go through the items that have been added and deleted and try to find matches between them.
@ -3548,16 +3411,13 @@ ko.utils.compareArrays = (() => {
// Set a limit on the number of consecutive non-matching comparisons; having it a multiple of
// smlIndexMax keeps the time complexity of this algorithm linear.
ko.utils.findMovesInArrayComparison(notInBig, notInSml, !options['dontLimitMoves'] && smlIndexMax * 10);
ko.utils.findMovesInArrayComparison(notInBig, notInSml, smlIndexMax * 10);
return editScript.reverse();
};
// Simple calculation based on Levenshtein distance.
return (oldArray, newArray, options) => {
// For backward compatibility, if the third arg is actually a bool, interpret
// it as the old parameter 'dontLimitMoves'. Newer code should use { dontLimitMoves: true }.
options = (typeof options === 'boolean') ? { 'dontLimitMoves': options } : (options || {});
oldArray = oldArray || [];
newArray = newArray || [];
@ -3600,8 +3460,14 @@ ko.utils.compareArrays = (() => {
// of which nodes would be deleted if valueToMap was itself later removed
mappedNodes.length = 0;
mappedNodes.push(...newMappedNodes);
}, { disposeWhenNodeIsRemoved: containerNode, disposeWhen: ()=>!!mappedNodes.find(ko.utils.domNodeIsAttachedToDocument) });
return { mappedNodes : mappedNodes, dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined) };
}, {
disposeWhenNodeIsRemoved: containerNode,
disposeWhen: ()=>!!mappedNodes.find(ko.utils.domNodeIsAttachedToDocument)
});
return {
mappedNodes : mappedNodes,
dependentObservable : (dependentObservable.isActive() ? dependentObservable : undefined)
};
}
var lastMappingResultDomDataKey = ko.utils.domData.nextKey(),
@ -3609,7 +3475,7 @@ ko.utils.compareArrays = (() => {
ko.utils.setDomNodeChildrenFromArrayMapping = (domNode, array, mapping, options, callbackAfterAddingNodes, editScript) => {
array = array || [];
if (typeof array.length == "undefined") // Coerce single value into array
if (!Array.isArray(array)) // Coerce single value into array
array = [array];
options = options || {};
@ -3638,12 +3504,6 @@ ko.utils.compareArrays = (() => {
mapData.indexObservable(currentArrayIndex++);
ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode);
newMappingResult.push(mapData);
},
callCallback = (callback, items) => {
if (callback) {
items.forEach(item => item?.mappedNodes.forEach(node => callback(node, i, item.arrayEntry)));
}
};
if (isFirstExecution) {
@ -3655,7 +3515,6 @@ ko.utils.compareArrays = (() => {
Array.prototype.map.call(lastMappingResult, x => x.arrayEntry),
array,
{
'dontLimitMoves': options['dontLimitMoves'],
'sparse': true
});
}
@ -3680,15 +3539,6 @@ ko.utils.compareArrays = (() => {
// Queue these nodes for later removal
if (ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode).length) {
if (options['beforeRemove']) {
newMappingResult.push(mapData);
countWaitingForRemove++;
if (mapData.arrayEntry === deletedItemDummyValue) {
mapData = null;
} else {
itemsForBeforeRemoveCallbacks[mapData.indexObservable.peek()] = mapData;
}
}
if (mapData) {
nodesToDelete.push.apply(nodesToDelete, mapData.mappedNodes);
}
@ -3723,8 +3573,8 @@ ko.utils.compareArrays = (() => {
// Store a copy of the array items we just considered so we can difference it next time
ko.utils.domData.set(domNode, lastMappingResultDomDataKey, newMappingResult);
// Next remove nodes for deleted items (or just clean if there's a beforeRemove callback)
nodesToDelete.forEach(options['beforeRemove'] ? ko.cleanNode : ko.removeNode);
// Next remove nodes for deleted items
nodesToDelete.forEach(ko.removeNode);
var i, lastNode, mappedNodes, activeElement,
insertNode = nodeToInsert => {
@ -3751,7 +3601,7 @@ ko.utils.compareArrays = (() => {
}
}
// Next add/reorder the remaining items (will include deleted items if there's a beforeRemove callback)
// Next add/reorder the remaining items
newMappingResult.forEach(mapData => {
// Get nodes for newly added items
mapData.mappedNodes
@ -3773,15 +3623,8 @@ ko.utils.compareArrays = (() => {
activeElement?.focus();
}
// If there's a beforeRemove callback, call it after reordering.
// Note that we assume that the beforeRemove callback will usually be used to remove the nodes using
// some sort of animation, which is why we first reorder the nodes that will be removed. If the
// callback instead removes the nodes right away, it would be more efficient to skip reordering them.
// Perhaps we'll make that change in the future if this scenario becomes more common.
callCallback(options['beforeRemove'], itemsForBeforeRemoveCallbacks);
// Replace the stored values of deleted items with a dummy value. This provides two benefits: it marks this item
// as already "removed" so we won't call beforeRemove for it again, and it ensures that the item won't match up
// as already "removed", and it ensures that the item won't match up
// with an actual item in the array and appear as "retained" or "moved".
itemsForBeforeRemoveCallbacks.forEach(callback => callback && (callback.arrayEntry = deletedItemDummyValue));
}