More Optional chaining in Knockout.js

This commit is contained in:
the-djmaze 2022-09-13 22:00:31 +02:00
parent d18f93d87f
commit a566f6ed5a
10 changed files with 391 additions and 476 deletions

View file

@ -18,7 +18,7 @@
realDataItemOrAccessor = shouldInheritData ? undefined : dataItemOrAccessor,
isFunc = typeof(realDataItemOrAccessor) == "function" && !ko.isObservable(realDataItemOrAccessor),
subscribable,
dataDependency = options && options['dataDependency'],
dataDependency = options?.['dataDependency'],
// The binding context object includes static properties for the current, parent, and root view models.
// If a view model is actually stored in an observable, the corresponding binding context object, and
@ -64,12 +64,11 @@
// 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.
if (extendCallback)
extendCallback(self, parentContext, dataItem);
extendCallback?.(self, parentContext, dataItem);
// When a "parent" context is given and we don't already have a dependency on its context, register a dependency on it.
// Thus whenever the parent context is updated, this context will also be updated.
if (parentContext && parentContext[contextSubscribable] && !ko.dependencyDetection.computed().hasAncestorDependency(parentContext[contextSubscribable])) {
if (parentContext?.[contextSubscribable] && !ko.dependencyDetection.computed().hasAncestorDependency(parentContext[contextSubscribable])) {
parentContext[contextSubscribable]();
}
@ -80,7 +79,7 @@
return self['$data'];
};
if (options && options['exportDependencies']) {
if (options?.['exportDependencies']) {
// The "exportDependencies" option means that the calling code will track any dependencies and re-create
// the binding context when they change.
updateContext();
@ -138,7 +137,7 @@
function asyncContextDispose(node) {
var bindingInfo = ko.utils.domData.get(node, boundElementDomDataKey),
asyncContext = bindingInfo && bindingInfo.asyncContext;
asyncContext = bindingInfo?.asyncContext;
if (asyncContext) {
bindingInfo.asyncContext = null;
asyncContext.notifyAncestor();
@ -152,25 +151,19 @@
this.asyncDescendants = new Set;
this.childrenComplete = false;
if (!bindingInfo.asyncContext) {
ko.utils.domNodeDisposal.addDisposeCallback(node, asyncContextDispose);
}
bindingInfo.asyncContext || ko.utils.domNodeDisposal.addDisposeCallback(node, asyncContextDispose);
if (ancestorBindingInfo && ancestorBindingInfo.asyncContext) {
if (ancestorBindingInfo?.asyncContext) {
ancestorBindingInfo.asyncContext.asyncDescendants.add(node);
this.ancestorBindingInfo = ancestorBindingInfo;
}
}
notifyAncestor() {
if (this.ancestorBindingInfo && this.ancestorBindingInfo.asyncContext) {
this.ancestorBindingInfo.asyncContext.descendantComplete(this.node);
}
this.ancestorBindingInfo?.asyncContext?.descendantComplete(this.node);
}
descendantComplete(node) {
this.asyncDescendants.delete(node);
if (!this.asyncDescendants.size && this.childrenComplete) {
this.completeChildren();
}
this.asyncDescendants.size || this.completeChildren?.();
}
completeChildren() {
this.childrenComplete = true;
@ -192,7 +185,7 @@
if (!bindingInfo.eventSubscribable) {
bindingInfo.eventSubscribable = new ko.subscribable;
}
if (options && options['notifyImmediately'] && bindingInfo.notifiedEvents[event]) {
if (options?.['notifyImmediately'] && bindingInfo.notifiedEvents[event]) {
ko.dependencyDetection.ignore(callback, context, [node]);
}
return bindingInfo.eventSubscribable.subscribe(callback, context, event);
@ -202,13 +195,11 @@
var bindingInfo = ko.utils.domData.get(node, boundElementDomDataKey);
if (bindingInfo) {
bindingInfo.notifiedEvents[event] = true;
if (bindingInfo.eventSubscribable) {
bindingInfo.eventSubscribable.notifySubscribers(node, event);
}
bindingInfo.eventSubscribable?.notifySubscribers(node, event);
if (event == ko.bindingEvent.childrenComplete) {
if (bindingInfo.asyncContext) {
bindingInfo.asyncContext.completeChildren();
} else if (bindingInfo.asyncContext === undefined && bindingInfo.eventSubscribable && bindingInfo.eventSubscribable.hasSubscriptionsForEvent(ko.bindingEvent.descendantsComplete)) {
} else if (bindingInfo.asyncContext === undefined && bindingInfo.eventSubscribable?.hasSubscriptionsForEvent(ko.bindingEvent.descendantsComplete)) {
// It's currently an error to register a descendantsComplete handler for a node that was never registered as completing asynchronously.
// That's because without the asyncContext, we don't have a way to know that all descendants have completed.
throw new Error("descendantsComplete event not supported for bindings on this node");
@ -260,8 +251,7 @@
// Perf optimisation: Apply bindings only if...
// (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)
var shouldApplyBindings = isElement || ko.bindingProvider.nodeHasBindings(nodeVerified);
if (shouldApplyBindings)
if (isElement || ko.bindingProvider.nodeHasBindings(nodeVerified))
bindingContextForDescendants = applyBindingsToNodeInternal(nodeVerified, null, bindingContext)['bindingContextForDescendants'];
// Don't want bindings that operate on text nodes to mutate <script> and <textarea> contents,
@ -269,7 +259,7 @@
// Also bindings should not operate on <template> elements since this breaks in Internet Explorer
// and because such elements' contents are always intended to be bound in a different context
// from where they appear in the document.
if (bindingContextForDescendants && nodeVerified.matches && !nodeVerified.matches('SCRIPT,TEXTAREA,TEMPLATE')) {
if (bindingContextForDescendants && !nodeVerified.matches?.('SCRIPT,TEXTAREA,TEMPLATE')) {
applyBindingsToDescendantsInternal(bindingContextForDescendants, nodeVerified);
}
}
@ -336,12 +326,8 @@
bindings = sourceBindings ? sourceBindings(bindingContext, node) : ko.bindingProvider.getBindingAccessors(node, bindingContext);
// Register a dependency on the binding context to support observable view models.
if (bindings) {
if (bindingContext[contextSubscribable]) {
bindingContext[contextSubscribable]();
}
if (bindingContext[contextDataDependency]) {
bindingContext[contextDataDependency]();
}
bindingContext[contextSubscribable]?.();
bindingContext[contextDataDependency]?.();
}
return bindings;
},
@ -374,9 +360,7 @@
var callback = bindings[ko.bindingEvent.childrenComplete]();
if (callback) {
var nodes = ko.virtualElements.childNodes(node);
if (nodes.length) {
callback(nodes, ko.dataFor(nodes[0]));
}
nodes.length && callback(nodes, ko.dataFor(nodes[0]));
}
});
}

View file

@ -49,55 +49,57 @@
array = [array];
options = options || {};
var lastMappingResult = ko.utils.domData.get(domNode, lastMappingResultDomDataKey);
var isFirstExecution = !lastMappingResult;
var lastMappingResult = ko.utils.domData.get(domNode, lastMappingResultDomDataKey),
isFirstExecution = !lastMappingResult,
// Build the new mapping result
var newMappingResult = [];
var lastMappingResultIndex = 0;
var currentArrayIndex = 0;
// Build the new mapping result
newMappingResult = [],
lastMappingResultIndex = 0,
currentArrayIndex = 0,
var nodesToDelete = [];
var itemsToMoveFirstIndexes = [];
var itemsForBeforeRemoveCallbacks = [];
var mapData;
var countWaitingForRemove = 0;
nodesToDelete = [],
itemsToMoveFirstIndexes = [],
itemsForBeforeRemoveCallbacks = [],
mapData,
countWaitingForRemove = 0,
function itemAdded(value) {
mapData = { arrayEntry: value, indexObservable: ko.observable(currentArrayIndex++) };
newMappingResult.push(mapData);
}
itemAdded = value => {
mapData = { arrayEntry: value, indexObservable: ko.observable(currentArrayIndex++) };
newMappingResult.push(mapData);
},
function itemMovedOrRetained(oldPosition) {
mapData = lastMappingResult[oldPosition];
// Since updating the index might change the nodes, do so before calling fixUpContinuousNodeArray
mapData.indexObservable(currentArrayIndex++);
ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode);
newMappingResult.push(mapData);
}
itemMovedOrRetained = oldPosition => {
mapData = lastMappingResult[oldPosition];
// Since updating the index might change the nodes, do so before calling fixUpContinuousNodeArray
mapData.indexObservable(currentArrayIndex++);
ko.utils.fixUpContinuousNodeArray(mapData.mappedNodes, domNode);
newMappingResult.push(mapData);
},
function callCallback(callback, items) {
if (callback) {
for (var i = 0, n = items.length; i < n; i++) {
items[i] && items[i].mappedNodes.forEach(node => callback(node, i, items[i].arrayEntry));
callCallback = (callback, items) => {
if (callback) {
for (var i = 0, n = items.length; i < n; i++) {
items[i] && items[i].mappedNodes.forEach(node => callback(node, i, items[i].arrayEntry));
}
}
}
}
};
if (isFirstExecution) {
array.forEach(itemAdded);
} else {
if (!editScript || (lastMappingResult && lastMappingResult['_countWaitingForRemove'])) {
// Compare the provided array against the previous one
var lastArray = Array.prototype.map.call(lastMappingResult, x => x.arrayEntry),
compareOptions = {
editScript = ko.utils.compareArrays(
Array.prototype.map.call(lastMappingResult, x => x.arrayEntry),
array,
{
'dontLimitMoves': options['dontLimitMoves'],
'sparse': true
};
editScript = ko.utils.compareArrays(lastArray, array, compareOptions);
});
}
for (let i = 0, editScriptItem, movedIndex, itemIndex; editScriptItem = editScript[i]; i++) {
let movedIndex, itemIndex;
editScript.forEach(editScriptItem => {
movedIndex = editScriptItem['moved'];
itemIndex = editScriptItem['index'];
switch (editScriptItem['status']) {
@ -145,7 +147,7 @@
}
break;
}
}
});
while (currentArrayIndex < array.length) {
itemMovedOrRetained(lastMappingResultIndex++);
@ -162,7 +164,11 @@
// Next remove nodes for deleted items (or just clean if there's a beforeRemove callback)
nodesToDelete.forEach(options['beforeRemove'] ? ko.cleanNode : ko.removeNode);
var i, j, lastNode, nodeToInsert, mappedNodes, activeElement;
var i, lastNode, mappedNodes, activeElement,
insertNode = nodeToInsert => {
ko.virtualElements.insertAfter(domNode, nodeToInsert, lastNode);
lastNode = nodeToInsert;
};
// Since most browsers remove the focus from an element when it's moved to another location,
// save the focused element and try to restore it later.
@ -173,27 +179,24 @@
while ((i = itemsToMoveFirstIndexes.shift()) != undefined) {
mapData = newMappingResult[i];
for (lastNode = undefined; i; ) {
if ((mappedNodes = newMappingResult[--i].mappedNodes) && mappedNodes.length) {
mappedNodes = newMappingResult[--i].mappedNodes;
if (mappedNodes?.length) {
lastNode = mappedNodes[mappedNodes.length - 1];
break;
}
}
for (j = 0; nodeToInsert = mapData.mappedNodes[j]; lastNode = nodeToInsert, j++) {
ko.virtualElements.insertAfter(domNode, nodeToInsert, lastNode);
}
mapData.mappedNodes.forEach(insertNode);
}
}
// Next add/reorder the remaining items (will include deleted items if there's a beforeRemove callback)
for (i = 0; mapData = newMappingResult[i]; i++) {
newMappingResult.forEach(mapData => {
// Get nodes for newly added items
if (!mapData.mappedNodes)
ko.utils.extend(mapData, mapNodeAndRefreshWhenChanged(domNode, mapping, mapData.arrayEntry, callbackAfterAddingNodes, mapData.indexObservable));
mapData.mappedNodes
|| ko.utils.extend(mapData, mapNodeAndRefreshWhenChanged(domNode, mapping, mapData.arrayEntry, callbackAfterAddingNodes, mapData.indexObservable));
// Put nodes in the right place if they aren't there already
for (j = 0; nodeToInsert = mapData.mappedNodes[j]; lastNode = nodeToInsert, j++) {
ko.virtualElements.insertAfter(domNode, nodeToInsert, lastNode);
}
mapData.mappedNodes.forEach(insertNode);
// Run the callbacks for newly added nodes (for example, to apply bindings, etc.)
if (!mapData.initialized && callbackAfterAddingNodes) {
@ -201,11 +204,11 @@
mapData.initialized = true;
lastNode = mapData.mappedNodes[mapData.mappedNodes.length - 1]; // get the last node again since it may have been changed by a preprocessor
}
}
});
// Restore the focused element if it had lost focus
if (activeElement && domNode.ownerDocument.activeElement != activeElement) {
activeElement.focus();
if (domNode.ownerDocument.activeElement != activeElement) {
activeElement?.focus();
}
// If there's a beforeRemove callback, call it after reordering.
@ -218,10 +221,6 @@
// 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
// with an actual item in the array and appear as "retained" or "moved".
for (i = 0; i < itemsForBeforeRemoveCallbacks.length; ++i) {
if (itemsForBeforeRemoveCallbacks[i]) {
itemsForBeforeRemoveCallbacks[i].arrayEntry = deletedItemDummyValue;
}
}
itemsForBeforeRemoveCallbacks.forEach(callback => callback && (callback.arrayEntry = deletedItemDummyValue));
}
})();

View file

@ -1,20 +1,19 @@
// Go through the items that have been added and deleted and try to find matches between them.
ko.utils.findMovesInArrayComparison = (left, right, limitFailedCompares) => {
if (left.length && right.length) {
var failedCompares, l, r, leftItem, rightItem;
for (failedCompares = l = 0; (!limitFailedCompares || failedCompares < limitFailedCompares) && (leftItem = left[l]); ++l) {
for (r = 0; rightItem = right[r]; ++r) {
if (leftItem['value'] === rightItem['value']) {
leftItem['moved'] = rightItem['index'];
rightItem['moved'] = leftItem['index'];
right.splice(r, 1); // This item is marked as moved; so remove it from right list
failedCompares = r = 0; // Reset failed compares count because we're checking for consecutive failures
break;
}
}
failedCompares += r;
var failedCompares = 0, r, l = right.length;
l && left.every(leftItem => {
r = right.findIndex(rightItem => leftItem['value'] === rightItem['value']);
if (r >= 0) {
leftItem['moved'] = right[r]['index'];
right[r]['moved'] = leftItem['index'];
right.splice(r, 1); // This item is marked as moved; so remove it from right list
// right[r] = null;
failedCompares = r = 0; // Reset failed compares count because we're checking for consecutive failures
--l;
}
}
failedCompares += l;
return l && (!limitFailedCompares || failedCompares < limitFailedCompares);
});
};
ko.utils.compareArrays = (() => {
@ -24,15 +23,15 @@ ko.utils.compareArrays = (() => {
var myMin = Math.min,
myMax = Math.max,
editDistanceMatrix = [],
smlIndex, smlIndexMax = smlArray.length,
smlIndex = -1, smlIndexMax = smlArray.length,
bigIndex, bigIndexMax = bigArray.length,
compareRange = (bigIndexMax - smlIndexMax) || 1,
maxDistance = smlIndexMax + bigIndexMax + 1,
thisRow, lastRow,
thisRow, prevRow,
bigIndexMaxForRow, bigIndexMinForRow;
for (smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) {
lastRow = thisRow;
while (++smlIndex <= smlIndexMax) {
prevRow = thisRow;
editDistanceMatrix.push(thisRow = []);
bigIndexMaxForRow = myMin(bigIndexMax, smlIndex + compareRange);
bigIndexMinForRow = myMax(0, smlIndex - 1);
@ -42,9 +41,9 @@ ko.utils.compareArrays = (() => {
else if (!smlIndex) // Top row - transform empty array into new array via additions
thisRow[bigIndex] = bigIndex + 1;
else if (smlArray[smlIndex - 1] === bigArray[bigIndex - 1])
thisRow[bigIndex] = lastRow[bigIndex - 1]; // copy value (no edit)
thisRow[bigIndex] = prevRow[bigIndex - 1]; // copy value (no edit)
else {
var northDistance = lastRow[bigIndex] || maxDistance; // not in big (deletion)
var northDistance = prevRow[bigIndex] || maxDistance; // not in big (deletion)
var westDistance = thisRow[bigIndex - 1] || maxDistance; // not in small (addition)
thisRow[bigIndex] = myMin(northDistance, westDistance) + 1;
}
@ -52,7 +51,9 @@ ko.utils.compareArrays = (() => {
}
var editScript = [], meMinusOne, notInSml = [], notInBig = [];
for (smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex;) {
smlIndex = smlIndexMax;
bigIndex = bigIndexMax
while (smlIndex || bigIndex) {
meMinusOne = editDistanceMatrix[smlIndex][bigIndex] - 1;
if (bigIndex && meMinusOne === editDistanceMatrix[smlIndex][bigIndex-1]) {
notInSml.push(editScript[editScript.length] = { // added

View file

@ -176,9 +176,7 @@ ko.expressionRewriting = (() => {
// it is !== existing value on that writable observable
writeValueToProperty: (property, allBindings, key, value, checkIfDifferent) => {
if (!property || !ko.isObservable(property)) {
var propWriters = allBindings.get('_ko_property_writers');
if (propWriters && propWriters[key])
propWriters[key](value);
allBindings.get('_ko_property_writers')?.[key]?.(value);
} else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) {
property(value);
}

View file

@ -8,11 +8,13 @@
readValue : element => {
switch (element.nodeName) {
case 'OPTION':
if (element[hasDomDataExpandoProperty] === true)
return ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey);
return element.value;
return (element[hasDomDataExpandoProperty] === true)
? ko.utils.domData.get(element, ko.bindingHandlers.options.optionValueDomDataKey)
: element.value;
case 'SELECT':
return element.selectedIndex >= 0 ? ko.selectExtensions.readValue(element.options[element.selectedIndex]) : undefined;
return element.selectedIndex >= 0
? ko.selectExtensions.readValue(element.options[element.selectedIndex])
: undefined;
default:
return element.value;
}
@ -37,8 +39,9 @@
break;
case 'SELECT':
// A blank string or null value will select the caption
var selection = -1, noValue = ("" === value || null == value);
for (var i = 0, n = element.options.length, optionValue; i < n; ++i) {
var selection = -1, noValue = ("" === value || null == value),
i = element.options.length, optionValue;
while (i--) {
optionValue = ko.selectExtensions.readValue(element.options[i]);
// Include special check to handle selecting a caption with a blank string value
if (optionValue == value || (optionValue === "" && noValue)) {