Update Fork (#2)

* Cleanup OpenPgpImportPopupView code

* update polish translation

* small fix

* Added Import S/MIME certificate popup
And much better handling of the sign and encrypt options

* bugfix: store in Passphrases

* Resolve #1448

* pre-verify S/MIME opaque signed messages so we have a body to view

* Fix timestampToString() for future dates

* Move php8.php to /app/libraries/polyfill/

* Improved Settings handling to prevent bugs in outer code

* Changed AbstractProvider::IsActive() to be abstract

* Example for #1449

* bugfix: previous IsActive() commit

* OpenSSL required due to S/MIME

* Use get_debug_type() instead of gettype()

* update polish translation

* Make all Enumerations classes abstract

* Added search functionality in Admin -> Config
And removed the unused ['capa']['quota']

* Cleanup Quota handling

* OPEN_PGP should be OPENPGP as it is one word

* Improve Capa handling

* Resolve #1451

* Bugfix TypeError: b64Encode(...).match(...) is null

* Small StorageType change

* Bugfix: mailvelope editor failed

* Bugfix: undefined getMailvelopePrivateKeyFor()

* Bugfix: MIME parser RegExp didn't escape `boundary` which caused issues

* Return detailed info on PgpImportKey

* Show GnuPG verify error

* Sort PGP keys by email and id

* Sort S/MIME certificates on emailAddress else validTo

* S/MIME import from signature use `BEGIN PKCS7`

* Optionally use existing private key to generate S/MIME certificate

* Chaned some error_log() to MailSo Logger()

* Force reload of S/MIME certificates list on import

* Make better use of SnappyMail\SensitiveString

* Fix view PGP key button

* Mask all POST data that has a key which contains `pass`

* v2.35.1

* Resolve #1455

* Improved GnuPG error handling

* Update pt/pt-PT translation

* update Polish translation

* Drop support for gnupg pecl extension as it fails with "no passphrase" issues

* Resolve #1456

* Resolve #1458

* v2.35.2

* fix changelog

* Resolve #1461

* Update pt/pt-PT translation

* compact-composer plugin v1.0.0

* Resolve #1462

* Fix decrypt error message

* `new Error()` to `Error()`

* Resolve #1463

* Show url for #1466

* Simplify SignMe/Remember me code

* Simplify language Notifications

* Bugfix: SetPassword expects \SnappyMail\SensitiveString

* https://github.com/the-djmaze/snappymail/issues/1450#issuecomment-1972147950

* improve: fire the 'squire2-toolbar' event after more props are added

* improve: add dark theme support and use 'button' element as menu trigger for consistent styling

* fix: use compact template in non-destructive way (do not replace the PopupsCompose template if a different wysiwyg is used)

* Update admin.json

* Update user.json

* CSS rainloopErrorTip location

* Improved error handling on PGP and S/MIME decrypt

* KnockoutJS remove unused `beforeRemove`

* KnockoutJS drop unused `as`

* KnockoutJS simplify renderMode because only 1 option is used

* KnoutJS cleanup templating.js a bit

* KnockoutJS drop unused `bindingRewriteValidators`

* KnockoutJS drop the twoWayBindings code

* KnockoutJS simplify virtualElements binding check

* KnockoutJS simplify applyBindingsToNodeInternal

* KnockoutJS use Array.isArray

* KnockoutJS drop alias `textinput` for `textInput`

* KnockoutJS scramble `createChildContext`

* KnockoutJS scramble `controlsDescendantBindings`

* KnockoutJS scramble `exportDependencies`

* KnockoutJS drop unused `throttleEvaluation`

* KnockoutJS drop unused `valueAllowUnset`

* KnockoutJS drop unused `templateNodes`

* KnockoutJS drop unused `optionsCaption`

* KnockoutJS drop unused `dontLimitMoves`

* KnockoutJS drop unused `uniqueName`

* KnockoutJS drop IE leftovers

* KnockoutJS drop unused `preprocess`

* KnockoutJS drop unused "disposeWhenNodeIsRemoved" and "disposeWhen"

* KnockoutJS don't scramble exportDependencies. controlsDescendantBindings, createChildContext

* KnockoutJS drop unused `$parentContext` and `$parents`

* KnockoutJS drop unused `$rawData`

* Knockoutjs built latest

* KnockoutJS drop unused template options `nodes`, `if`, `ifnot`

* KnockoutJS use more Array.isArray

* KnockoutJS cleanup code a bit

* KnockoutJS primitiveTypes can just be checked with Object()

* KnockoutJS rebuilt

* Verify S/MIME signed automatically and log Exception

* Automatically verify PGP and S/MIME signed messages

* `new Error` to `Error`

* By default throw AccountNotAllowed as confused in #1478

* GPG use pinentries for decrypt, sign and export

* Better GPG error handling

* GPG show error on view/export

* OpenPGP fix handling of importing keys

* Make "verify signatures automatically" optional, as it requires more IMAP fetching

* S/MIME don't post identity key and certificate, just fetch from server

* Show error to old browsers, instead of crashing

* Automatically verify S/MIME decrypted signed message

---------

Co-authored-by: the-djmaze <>
Co-authored-by: tinola <tinola@poczta.onet.pl>
Co-authored-by: Maarten <3752035+the-djmaze@users.noreply.github.com>
Co-authored-by: lmperfis <joint.striker@gmail.com>
Co-authored-by: Sergey Mosin <sergey@srgdev.com>
Co-authored-by: hguilbert <51283484+hguilbert@users.noreply.github.com>
This commit is contained in:
S-A-L13 2024-03-04 17:00:27 +01:00 • committed by GitHub
parent 3bd073a38b
commit 876ed221c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
218 changed files with 10266 additions and 4267 deletions

View file

@ -13,7 +13,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,
@ -42,7 +42,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
@ -56,13 +55,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.
@ -107,21 +102,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);
}
@ -129,7 +114,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);
}
@ -202,7 +187,7 @@ ko.bindingEvent = {
} 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");
throw Error("descendantsComplete event not supported for bindings on this node");
}
}
}
@ -226,12 +211,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);
@ -252,7 +231,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.
@ -384,8 +363,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 Error("The binding '" + bindingKey + "' cannot be used with comment nodes");
}
try {
@ -397,7 +377,7 @@ function applyBindingsToNodeInternal(node, sourceBindings, bindingContext) {
// If this binding handler claims to control descendant bindings, make a note of this
if (initResult && initResult['controlsDescendantBindings']) {
if (bindingHandlerThatControlsDescendantBindings !== undefined)
throw new Error("Multiple bindings (" + bindingHandlerThatControlsDescendantBindings + " and " + bindingKey + ") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");
throw Error("Multiple bindings (" + bindingHandlerThatControlsDescendantBindings + " and " + bindingKey + ") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");
bindingHandlerThatControlsDescendantBindings = bindingKey;
}
});
@ -417,11 +397,7 @@ function applyBindingsToNodeInternal(node, sourceBindings, bindingContext) {
});
}
var shouldBindDescendants = bindingHandlerThatControlsDescendantBindings === undefined;
return {
'shouldBindDescendants': shouldBindDescendants,
'bindingContextForDescendants': shouldBindDescendants && contextToExtend
};
return bindingHandlerThatControlsDescendantBindings === undefined && contextToExtend;
}
ko.storedBindingContextForNode = node => {
@ -432,7 +408,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) =>

View file

@ -18,14 +18,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;
}
});
}
};

View file

@ -12,7 +12,8 @@ ko.bindingHandlers['checked'] = {
// Treat "value" like "checkedValue" when it is included with "checked" binding
if (allBindings['has']('checkedValue')) {
return ko.utils.unwrapObservable(allBindings.get('checkedValue'));
} else if (useElementValue) {
}
if (useElementValue) {
return allBindings['has']('value')
? ko.utils.unwrapObservable(allBindings.get('value'))
: element.value;
@ -78,10 +79,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
@ -113,7 +110,6 @@ ko.bindingHandlers['checked'] = {
}
}
};
//ko.expressionRewriting.twoWayBindings['checked'] = true;
ko.bindingHandlers['checkedValue'] = {
'update'(element, valueAccessor) {

View file

@ -1,31 +1,28 @@
// "foreach: someExpression" is equivalent to "template: { foreach: someExpression }"
// "foreach: { data: someExpression, afterAdd: myfn }" is equivalent to "template: { foreach: someExpression, afterAdd: myfn }"
// "foreach: { data: someExpression }" is equivalent to "template: { foreach: someExpression }"
const makeTemplateValueAccessor = valueAccessor =>
() => {
var modelValue = valueAccessor(),
// Unwrap without setting a dependency here
unwrappedValue = ko.isObservable(modelValue) ? modelValue.peek() : modelValue;
// If unwrappedValue is the array, pass in the wrapped value on its own
// The value will be unwrapped and tracked within the template binding
// (See https://github.com/SteveSanderson/knockout/issues/523)
if ((!unwrappedValue) || Array.isArray(unwrappedValue))
return { 'foreach': modelValue };
// 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']
};
};
ko.bindingHandlers['foreach'] = {
makeTemplateValueAccessor: valueAccessor =>
() => {
var modelValue = valueAccessor(),
// Unwrap without setting a dependency here
unwrappedValue = ko.isObservable(modelValue) ? modelValue.peek() : modelValue;
// If unwrappedValue is the array, pass in the wrapped value on its own
// The value will be unwrapped and tracked within the template binding
// (See https://github.com/SteveSanderson/knockout/issues/523)
if ((!unwrappedValue) || typeof unwrappedValue.length == "number")
return { 'foreach': modelValue };
// 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']
};
},
'init': (element, valueAccessor) =>
ko.bindingHandlers['template']['init'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor))
ko.bindingHandlers['template']['init'](element, makeTemplateValueAccessor(valueAccessor))
,
'update': (element, valueAccessor, allBindings, viewModel, bindingContext) =>
ko.bindingHandlers['template']['update'](element, ko.bindingHandlers['foreach'].makeTemplateValueAccessor(valueAccessor), allBindings, viewModel, bindingContext)
ko.bindingHandlers['template']['update'](element, makeTemplateValueAccessor(valueAccessor), allBindings, viewModel, bindingContext)
};
ko.expressionRewriting.bindingRewriteValidators['foreach'] = false; // Can't rewrite control flow bindings
ko.virtualElements.allowedBindings['foreach'] = true;

View file

@ -36,4 +36,3 @@ ko.bindingHandlers['hasfocus'] = {
}
}
};
//ko.expressionRewriting.twoWayBindings.add('hasfocus');

View file

@ -7,7 +7,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);
@ -53,7 +53,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;
}

View file

@ -2,7 +2,7 @@ var captionPlaceholder = {};
ko.bindingHandlers['options'] = {
'init': element => {
if (!element.matches("SELECT"))
throw new Error("options binding applies only to SELECT elements");
throw Error("options binding applies only to SELECT elements");
// Remove all existing <option>s.
let l = element.length;
@ -19,7 +19,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,
@ -38,10 +37,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]));
@ -54,29 +50,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
}
@ -88,12 +73,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
@ -107,10 +92,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) => {
@ -121,28 +102,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);
}

View file

@ -1,7 +1,7 @@
ko.bindingHandlers['submit'] = {
'init': (element, valueAccessor, allBindings, viewModel, bindingContext) => {
if (typeof valueAccessor() != "function")
throw new Error("The value for a submit binding must be a function");
throw Error("The value for a submit binding must be a function");
element.addEventListener("submit", event => {
var handlerReturnValue;
var value = valueAccessor();

View file

@ -45,16 +45,6 @@ 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)
};

View file

@ -79,9 +79,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);
@ -112,4 +111,3 @@ ko.bindingHandlers['value'] = {
},
'update': () => {} // Keep for backwards compatibility with code that may have wrapped value binding
};
//ko.expressionRewriting.twoWayBindings.add('value');

View file

@ -32,8 +32,14 @@
// 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(),
@ -41,7 +47,7 @@
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 || {};
@ -70,12 +76,6 @@
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) {
@ -87,7 +87,6 @@
Array.prototype.map.call(lastMappingResult, x => x.arrayEntry),
array,
{
'dontLimitMoves': options['dontLimitMoves'],
'sparse': true
});
}
@ -112,15 +111,6 @@
// 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);
}
@ -155,8 +145,8 @@
// 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 => {
@ -183,7 +173,7 @@
}
}
// 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
@ -205,15 +195,8 @@
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));
}

View file

@ -78,16 +78,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 || [];

View file

@ -112,9 +112,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 = [],
@ -122,16 +119,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);
@ -148,10 +135,6 @@ ko.expressionRewriting = (() => {
};
return {
bindingRewriteValidators: [],
// twoWayBindings: twoWayBindings,
parseObjectLiteral: parseObjectLiteral,
preProcessBindings: preProcessBindings,

View file

@ -20,7 +20,7 @@ ko.selectExtensions = {
}
},
writeValue: (element, value, allowUnset) => {
writeValue: (element, value) => {
switch (element.nodeName) {
case 'OPTION':
if (typeof value === "string") {
@ -49,7 +49,7 @@ ko.selectExtensions = {
break;
}
}
if (allowUnset || selection >= 0 || (noValue && element.size > 1)) {
if (selection >= 0 || (noValue && element.size > 1)) {
element.selectedIndex = selection;
}
break;