From ebe2c0536feddf86b5ce01ae5d081da270233d09 Mon Sep 17 00:00:00 2001 From: djmaze Date: Mon, 1 Feb 2021 14:34:24 +0100 Subject: [PATCH] Bugfix: compose mail select contacts for cc/bcc failed Cleanup: Inputosaurus and Knockout Change: Knockout domData now uses WeakMap Replaced: Knockout domManipulation with a documentFragment --- README.md | 22 +- dev/External/User/ko.js | 4 +- dev/View/Popup/Compose.js | 36 +- .../templates/Views/User/PopupsCompose.html | 4 +- .../templates/Views/User/PopupsContacts.html | 12 +- .../templates/Views/User/PopupsFilter.html | 2 +- .../Views/User/PopupsSieveScript.html | 2 +- vendors/inputosaurus/inputosaurus.css | 6 - vendors/inputosaurus/inputosaurus.js | 158 +++---- .../build/fragments/source-references.js | 2 +- .../build/output/knockout-latest.debug.js | 446 +++++------------- .../knockout/build/output/knockout-latest.js | 161 +++---- .../src/binding/bindingAttributeSyntax.js | 30 +- .../knockout/src/binding/bindingProvider.js | 75 ++- .../src/binding/defaultBindings/hasfocus.js | 3 - .../src/binding/defaultBindings/html.js | 17 +- .../src/binding/expressionRewriting.js | 8 +- .../knockout/src/binding/selectExtensions.js | 13 +- .../knockout/src/components/defaultLoader.js | 5 +- .../src/templating/templateSources.js | 131 ++--- vendors/knockout/src/templating/templating.js | 44 +- vendors/knockout/src/utils.domData.js | 47 +- vendors/knockout/src/utils.domManipulation.js | 71 --- 23 files changed, 410 insertions(+), 889 deletions(-) delete mode 100644 vendors/knockout/src/utils.domManipulation.js diff --git a/README.md b/README.md index f135ea500..edb0304a5 100644 --- a/README.md +++ b/README.md @@ -116,20 +116,20 @@ RainLoop 1.15 vs SnappyMail |admin.js |2.158.025 | 119.197 | |app.js |4.215.733 | 527.415 | |boot.js | 672.433 | 4.842 | -|libs.js | 647.679 | 235.475 | +|libs.js | 647.679 | 232.283 | |polyfills.js | 325.908 | 0 | |serviceworker.js | 0 | 285 | |TOTAL |8.019.778 | 887.214 | -|js/min/* |RainLoop |Snappy |Rain gzip |S gzip |R brotli |S brotli | -|--------------- |--------: |--------: |--------: |------: |-------: |-------: | +|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | +|--------------- |--------: |--------: |------: |------: |--------: |--------: | |admin.min.js | 255.514 | 61.165 | 73.899 | 17.419 | 60.674 | 15.447 | -|app.min.js | 516.000 | 256.632 |140.430 | 73.688 |110.657 | 61.812 | +|app.min.js | 516.000 | 256.629 |140.430 | 73.667 |110.657 | 61.826 | |boot.min.js | 66.456 | 2.630 | 22.553 | 1.375 | 20.043 | 1.189 | -|libs.min.js | 574.626 | 130.930 |177.280 | 47.397 |151.855 | 42.116 | +|libs.min.js | 574.626 | 128.352 |177.280 | 46.615 |151.855 | 41.437 | |polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 | -|TOTAL |1.445.204 | 451.357 |425.477 |139.879 |353.301 |120.564 | -|TOTAL (no admin) |1.189.690 | 390.192 |351.061 |122.460 |292.627 |105.117 | +|TOTAL |1.445.204 | 448.776 |425.477 |139.076 |353.301 |119.899 | +|TOTAL (no admin) |1.189.690 | 387.611 |351.061 |121.657 |292.627 |104.452 | For a user its around 65% smaller and faster than traditional RainLoop. @@ -155,10 +155,10 @@ For a user its around 65% smaller and faster than traditional RainLoop. * Removed vendors/Progress.js/minified/progressjs.min.css -|css/* |RainLoop |Snappy |Rain gzip |gzip |brotli | -|-------------- |-------: |-------: |------: |------: |------: | -|app.css | 340.334 | 183.662 | 46,959 | 28.665 | 24.135 | -|app.min.css | 274.791 | 149.991 | 39.618 | 25.303 | 21.868 | +|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli | +|------------ |-------: |-------: |------: |------: |--------: | +|app.css | 340.334 | 183.662 | 46,959 | 28.665 | 24.135 | +|app.min.css | 274.791 | 149.991 | 39.618 | 25.303 | 21.868 | |boot.css | | 2.534 | | 837 | 668 | |boot.min.css | | 2.055 | | 732 | 560 | diff --git a/dev/External/User/ko.js b/dev/External/User/ko.js index 869fca0ad..9a173d44f 100644 --- a/dev/External/User/ko.js +++ b/dev/External/User/ko.js @@ -68,7 +68,7 @@ ko.bindingHandlers.emailsTags = { inputDelimiters = [',', ';', '\n']; element.inputosaurus = new window.Inputosaurus(element, { - focusCallback: value => fValue && fValue.focused && fValue.focused(!!value), + focusCallback: value => fValue.focused && fValue.focused(!!value), autoCompleteSource: fAllBindings.autoCompleteSource || null, splitHook: value => { const v = value.trim(); @@ -89,7 +89,7 @@ ko.bindingHandlers.emailsTags = { } }); - if (fValue && fValue.focused && fValue.focused.subscribe) { + if (fValue.focused && fValue.focused.subscribe) { fValue.focused.subscribe(value => element.inputosaurus[value ? 'focus' : 'blur']() ); diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index 13167021d..e3f191a89 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -142,16 +142,11 @@ class ComposePopupView extends AbstractViewPopup { identitiesDropdownTrigger: false, to: '', - toFocused: false, cc: '', - ccFocused: false, bcc: '', - bccFocused: false, replyTo: '', - replyToFocused: false, subject: '', - subjectFocused: false, isHtml: false, @@ -197,20 +192,13 @@ class ComposePopupView extends AbstractViewPopup { // this.to.subscribe((v) => console.log(v)); - ko.computed(() => { - switch (true) { - case this.toFocused(): - this.sLastFocusedField = 'to'; - break; - case this.ccFocused(): - this.sLastFocusedField = 'cc'; - break; - case this.bccFocused(): - this.sLastFocusedField = 'bcc'; - break; - // no default - } - }).extend({ notify: 'always' }); + // Used by ko.bindingHandlers.emailsTags + this.to.focused = ko.observable(false); + this.to.focused.subscribe(value => value && (this.sLastFocusedField = 'to')); + this.cc.focused = ko.observable(false); + this.cc.focused.subscribe(value => value && (this.sLastFocusedField = 'cc')); + this.bcc.focused = ko.observable(false); + this.bcc.focused.subscribe(value => value && (this.sLastFocusedField = 'bcc')); this.attachments = ko.observableArray(); @@ -688,7 +676,7 @@ class ComposePopupView extends AbstractViewPopup { this.bSkipNextHide = false; - this.toFocused(false); + this.to.focused(false); rl.route.on(); @@ -1131,11 +1119,9 @@ class ComposePopupView extends AbstractViewPopup { // rl.settings.app('mobile') || setTimeout(() => { if (!this.to()) { - this.toFocused(true); - } else if (this.oEditor) { - if (!this.toFocused()) { - this.oEditor.focus(); - } + this.to.focused(true); + } else if (this.oEditor && !this.to.focused()) { + this.oEditor.focus(); } }, 100); } diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html index abf66a4eb..9c1947974 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsCompose.html @@ -111,7 +111,7 @@
- +
@@ -143,7 +143,7 @@
- +
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsContacts.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsContacts.html index e0b18689f..be7da896b 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsContacts.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsContacts.html @@ -178,7 +178,7 @@ + data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup', attr: {'placeholder': placeholderValue}" />
@@ -187,13 +187,13 @@ + data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup', attr: {'placeholder': placeholderValue}" /> + data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup', attr: {'placeholder': placeholderValue}">
@@ -209,7 +209,7 @@ + data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup'" /> @@ -225,7 +225,7 @@ + data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup'" /> @@ -240,7 +240,7 @@ + data-bind="value: value, hasfocus: focused, valueUpdate: 'keyup'" /> diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsFilter.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsFilter.html index eb80c8c75..6db660db3 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsFilter.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsFilter.html @@ -14,7 +14,7 @@
diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsSieveScript.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsSieveScript.html index 429b16506..adf8fd5c2 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsSieveScript.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsSieveScript.html @@ -12,7 +12,7 @@
diff --git a/vendors/inputosaurus/inputosaurus.css b/vendors/inputosaurus/inputosaurus.css index a0485c8c0..a3119dc15 100644 --- a/vendors/inputosaurus/inputosaurus.css +++ b/vendors/inputosaurus/inputosaurus.css @@ -76,9 +76,3 @@ background-color: Highlight; color: HighlightText; } - -.inputosaurus-fake-span { - position: absolute; - top: 0; - left: -5000px; -} diff --git a/vendors/inputosaurus/inputosaurus.js b/vendors/inputosaurus/inputosaurus.js index 1dfbff619..dd607001b 100644 --- a/vendors/inputosaurus/inputosaurus.js +++ b/vendors/inputosaurus/inputosaurus.js @@ -35,16 +35,12 @@ this.Inputosaurus = class { // In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event // In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set validDropzone = () => dragData && dragData.li.parentNode !== self.ul, - fnDrag = e => validDropzone(e) && e.preventDefault(); + fnDrag = e => validDropzone() && e.preventDefault(); self.element = element; self.options = Object.assign({ - // while typing, the user can separate values using these delimiters - // the value tags are created on the fly when an inputDelimiter is detected - inputDelimiters : [',', ';', '\n'], - focusCallback : null, // simply passing an autoComplete source (array, string or function) will instantiate autocomplete functionality @@ -71,11 +67,10 @@ this.Inputosaurus = class { self.ul.addEventListener("dragenter", fnDrag); self.ul.addEventListener("dragover", fnDrag); self.ul.addEventListener("drop", e => { - if (validDropzone(e) && dragData.value) { + if (validDropzone() && dragData.value) { e.preventDefault(); dragData.source._removeDraggedTag(dragData.li); - self.input.value = dragData.value; - self.parseInput(); + self._parseValue(dragData.value); } }); @@ -83,13 +78,33 @@ this.Inputosaurus = class { autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"}); self.input.addEventListener('focus', () => self._focusTrigger(true)); - self.input.addEventListener('blur', () => self._focusTrigger(false)); - self.input.addEventListener('keyup', e => self._inputKeypress(e)); - self.input.addEventListener('keydown', e => self._inputKeypress(e)); - self.input.addEventListener('change', e => self._inputKeypress(e)); - self.input.addEventListener('input', e => self._inputKeypress(e)); + self.input.addEventListener('blur', () => { + // prevent autoComplete menu click from causing a false 'blur' + self._parseInput(); + self._focusTrigger(false); + }); + self.input.addEventListener('keydown', e => { + if ('Backspace' === e.key || 'ArrowLeft' === e.key) { + // if our input contains no value and backspace has been pressed, select the last tag + var lastTag = self.inputCont.previousElementSibling, + input = self.input; + if (lastTag && (!input.value + || (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0)) + ) { + e.preventDefault(); + lastTag.querySelector('a').focus(); + } + self._updateDatalist(); + } else if (e.key == 'Enter') { + e.preventDefault(); + self._parseInput(); + } + }); + self.input.addEventListener('input', () => { + self._parseInput(); + self._updateDatalist(); + }); self.input.addEventListener('focus', () => self.input.value || self._resetDatalist()); - self.input.addEventListener('blur', e => self.parseInput(e)); // define starting placeholder if (element.placeholder) { @@ -104,8 +119,7 @@ this.Inputosaurus = class { // if instantiated input already contains a value, parse that junk if (element.value.trim()) { - self.input.value = element.value; - self.parseInput(); + self._parseValue(element.value); } self._updateDatalist = self.options.autoCompleteSource @@ -134,71 +148,34 @@ this.Inputosaurus = class { datalist.textContent = ''; } - parseInput(ev) { - var self = this, - val, - hook, - delimiterFound = false, - values = []; - - val = self.input.value; - - if (val) { - hook = self.options.splitHook(val); - } - - if (hook) { - values = hook; - } else if (delimiterFound !== false) { - values = val.split(delimiterFound); - } else if (!ev || ev.key == 'Enter') { - values.push(val); - ev && ev.preventDefault(); - - // prevent autoComplete menu click from causing a false 'blur' - } else if (ev.type === 'blur') { - values.push(val); - } - - values = self.options.parseHook(values); - - if (values.length) { - self._setChosen(values); - self.input.value = ''; - self.resizeInput(); - } + _parseInput() { + this._parseValue(this.input.value) && (this.input.value = ''); + this._resizeInput(); } - _inputKeypress(ev) { - let self = this; + _parseValue(val) { + var self = this, + hook, + values = []; - switch (ev.key) { - case 'Backspace': - case 'ArrowLeft': - // if our input contains no value and backspace has been pressed, select the last tag - if (ev.type === 'keydown') { - var lastTag = self.inputCont.previousElementSibling, - input = self.input; - if (lastTag && (!input.value - || (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0)) - ) { - ev.preventDefault(); - lastTag.querySelector('a').focus(); - } + if (val) { + if ((hook = self.options.splitHook(val))) { + values = hook; + } else { + values.push(val); + } - } - break; + values = self.options.parseHook(values); - default : - self.parseInput(ev); - self.resizeInput(); + if (values.length) { + self._setChosen(values); + return true; + } } - - self._updateDatalist(); } // the input dynamically resizes based on the length of its value - resizeInput() { + _resizeInput() { let input = this.input; if (input.clientWidth < input.scrollWidth) { input.style.width = Math.min(500, Math.max(200, input.scrollWidth)) + 'px'; @@ -220,13 +197,10 @@ this.Inputosaurus = class { ; self._chosenValues.forEach(v => { - if (v.key === tagKey) - { + if (v.key === tagKey) { tagName = v.value; next = true; - } - else if (next && !oPrev) - { + } else if (next && !oPrev) { oPrev = v; } }); @@ -242,12 +216,7 @@ this.Inputosaurus = class { setTimeout(() => self.input.select(), 100); self._removeTag(ev, li); - self.resizeInput(ev); - } - - // return the inputDelimiter that was detected or false if none were found - _containsDelimiter(tagStr) { - return -1 < this.options.inputDelimiters.findIndex(v => tagStr.indexOf(v) !== -1); + self._resizeInput(ev); } _setChosen(valArr) { @@ -258,7 +227,8 @@ this.Inputosaurus = class { } valArr.forEach(a => { - var v = '', exists = false, + var v = a[0].trim(), + exists = false, lastIndex = -1, obj = { key : '', @@ -266,11 +236,8 @@ this.Inputosaurus = class { value : '' }; - v = a[0].trim(); - self._chosenValues.forEach((vv, kk) => { - if (vv.value === self._lastEdit) - { + if (vv.value === self._lastEdit) { lastIndex = kk; } @@ -283,12 +250,9 @@ this.Inputosaurus = class { obj.value = v; obj.obj = a[1]; - if (-1 < lastIndex) - { + if (-1 < lastIndex) { self._chosenValues.splice(lastIndex, 0, obj); - } - else - { + } else { self._chosenValues.push(obj); } @@ -297,8 +261,7 @@ this.Inputosaurus = class { } }); - if (valArr.length === 1 && valArr[0] === '' && self._lastEdit !== '') - { + if (valArr.length === 1 && valArr[0] === '' && self._lastEdit !== '') { self._lastEdit = ''; self._renderTags(); } @@ -419,8 +382,7 @@ this.Inputosaurus = class { self = this, indexFound = self._chosenValues.findIndex(v => key === v.key) ; - if (-1 < indexFound) - { + if (-1 < indexFound) { self._chosenValues.splice(indexFound, 1); self._setValue(self._buildValue()); } @@ -467,7 +429,7 @@ this.Inputosaurus = class { self._setChosen(values); self._renderTags(); self.input.value = ''; - self.resizeInput(); + self._resizeInput(); } } }; diff --git a/vendors/knockout/build/fragments/source-references.js b/vendors/knockout/build/fragments/source-references.js index 5da820fd7..a109ced6c 100644 --- a/vendors/knockout/build/fragments/source-references.js +++ b/vendors/knockout/build/fragments/source-references.js @@ -6,7 +6,7 @@ knockoutDebugCallback([ 'src/utils.js', 'src/utils.domData.js', 'src/utils.domNodeDisposal.js', - 'src/utils.domManipulation.js', +// 'src/utils.domManipulation.js', // 'src/memoization.js', 'src/tasks.js', 'src/subscribables/extenders.js', diff --git a/vendors/knockout/build/output/knockout-latest.debug.js b/vendors/knockout/build/output/knockout-latest.debug.js index 73875a3de..279c19275 100644 --- a/vendors/knockout/build/output/knockout-latest.debug.js +++ b/vendors/knockout/build/output/knockout-latest.debug.js @@ -190,43 +190,26 @@ ko.exportSymbol('utils', ko.utils); ko.exportSymbol('unwrap', ko.utils.unwrapObservable); // Convenient shorthand, because this is used so commonly ko.utils.domData = new (function () { - var uniqueId = 0; - var dataStoreKeyExpandoPropertyName = "__ko__" + (Date.now()); - - var - // We considered using WeakMap, but it has a problem in IE 11 and Edge that prevents using - // it cross-window, so instead we just store the data directly on the node. - // See https://github.com/knockout/knockout/issues/2141 - getDataForNode = (node, createIfNotFound) => { - var dataForNode = node[dataStoreKeyExpandoPropertyName]; - if (!dataForNode && createIfNotFound) { - dataForNode = node[dataStoreKeyExpandoPropertyName] = {}; - } - return dataForNode; - }, - clear = node => { - if (node[dataStoreKeyExpandoPropertyName]) { - delete node[dataStoreKeyExpandoPropertyName]; - return true; // Exposing "did clean" flag purely so specs can infer whether things have been cleaned up as intended - } - return false; - }; + let uniqueId = 0, + dataStoreKeyExpandoPropertyName = "__ko__" + (Date.now()), + dataStore = new WeakMap(); return { - get: (node, key) => { - var dataForNode = getDataForNode(node, false); - return dataForNode && dataForNode[key]; - }, + get: (node, key) => (dataStore.get(node) || {})[key], set: (node, key, value) => { - // Make sure we don't actually create a new domData key if we are actually deleting a value - var dataForNode = getDataForNode(node, value !== undefined /* createIfNotFound */); - dataForNode && (dataForNode[key] = value); + if (dataStore.has(node)) { + dataStore.get(node)[key] = value; + } else { + let dataForNode = {}; + dataForNode[key] = value; + dataStore.set(node, dataForNode); + } + return value; }, - getOrSet: (node, key, value) => { - var dataForNode = getDataForNode(node, true /* createIfNotFound */); - return dataForNode[key] || (dataForNode[key] = value); + getOrSet: function(node, key, value) { + return this.get(node, key) || this.set(node, key, value); }, - clear: clear, + clear: node => dataStore.delete(node), nextKey: () => (uniqueId++) + dataStoreKeyExpandoPropertyName }; @@ -323,77 +306,6 @@ ko.cleanNode = ko.utils.domNodeDisposal.cleanNode; // Shorthand name for conveni ko.removeNode = ko.utils.domNodeDisposal.removeNode; // Shorthand name for convenience ko.exportSymbol('utils.domNodeDisposal', ko.utils.domNodeDisposal); ko.exportSymbol('utils.domNodeDisposal.addDisposeCallback', ko.utils.domNodeDisposal.addDisposeCallback); -(() => { - var none = [0, "", ""], - table = [1, "", "
"], - tbody = [2, "", "
"], - tr = [3, "", "
"], - select = [1, ""], - lookup = { - 'thead': table, - 'tbody': table, - 'tfoot': table, - 'tr': tbody, - 'td': tr, - 'th': tr, - 'option': select, - 'optgroup': select - }; - - function getWrap(tags) { - var m = tags.match(/^(?:\s*?)*?<([a-z]+)[\s>]/); - return (m && lookup[m[1]]) || none; - } - - function simpleHtmlParse(html, documentContext) { - documentContext || (documentContext = document); - var windowContext = documentContext['parentWindow'] || documentContext['defaultView'] || window; - - // Based on jQuery's "clean" function, but only accounting for table-related elements. - - // Trim whitespace, otherwise indexOf won't work as expected - var tags = ko.utils.stringTrim(html).toLowerCase(), div = documentContext.createElement("div"), - wrap = getWrap(tags), - depth = wrap[0]; - - // Go to html and back, then peel off extra wrappers - div.innerHTML = "
" + wrap[1] + html + wrap[2] + "
"; - - // Move to the right depth - while (depth--) - div = div.lastChild; - - return [...div.lastChild.childNodes]; - } - - ko.utils.parseHtmlFragment = (html, documentContext) => - simpleHtmlParse(html, documentContext); // ... otherwise, this simple logic will do in most common cases. - - ko.utils.parseHtmlForTemplateNodes = (html, documentContext) => { - var nodes = ko.utils.parseHtmlFragment(html, documentContext); - return (nodes.length && nodes[0].parentElement) || ko.utils.moveCleanedNodesToContainerElement(nodes); - }; - - ko.utils.setHtml = (node, html) => { - ko.utils.emptyDomNode(node); - - // There's no legitimate reason to display a stringified observable without unwrapping it, so we'll unwrap it - html = ko.utils.unwrapObservable(html); - - if ((html !== null) && (html !== undefined)) { - if (typeof html != 'string') - html = html.toString(); - - // jQuery contains a lot of sophisticated code to parse arbitrary HTML fragments, - // for example elements which are not normally allowed to exist on their own. - // If you've referenced jQuery we'll use that rather than duplicating its code. - // ... otherwise, use KO's own parsing logic. - var parsedNodes = ko.utils.parseHtmlFragment(html, node.ownerDocument); - for (var i = 0; i < parsedNodes.length; i++) - node.appendChild(parsedNodes[i]); - } - }; -})(); ko.tasks = (() => { var taskQueue = [], taskQueueLength = 0, @@ -925,7 +837,7 @@ Object.setPrototypeOf(ko.observableArray['fn'], ko.observable['fn']); // Populate ko.observableArray.fn with native arrays functions Object.getOwnPropertyNames(Array.prototype).forEach(methodName => { - // skip property length + // skip property length if (typeof Array.prototype[methodName] === 'function' && 'constructor' != methodName) { if (["copyWithin", "fill", "pop", "push", "reverse", "shift", "sort", "splice", "unshift"].includes(methodName)) { // Mutator methods @@ -1655,25 +1567,22 @@ ko.pureComputed = (evaluatorFunctionOrOptions, evaluatorFunctionTarget) => { } break; case 'SELECT': - if (value === "" || value === null) // A blank string or null value will select the caption - value = undefined; - var selection = -1; + // 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) { optionValue = ko.selectExtensions.readValue(element.options[i]); // Include special check to handle selecting a caption with a blank string value - if (optionValue == value || (optionValue === "" && value === undefined)) { + if (optionValue == value || (optionValue === "" && noValue)) { selection = i; break; } } - if (allowUnset || selection >= 0 || (value === undefined && element.size > 1)) { + if (allowUnset || selection >= 0 || (noValue && element.size > 1)) { element.selectedIndex = selection; } break; default: - if ((value === null) || (value === undefined)) - value = ""; - element.value = value; + element.value = (value == null) ? "" : value; break; } } @@ -1843,12 +1752,8 @@ ko.expressionRewriting = (() => { preProcessBindings: preProcessBindings, - keyValueArrayContainsKey: (keyValueArray, key) => { - for (var i = 0; i < keyValueArray.length; i++) - if (keyValueArray[i]['key'] == key) - return true; - return false; - }, + keyValueArrayContainsKey: (keyValueArray, key) => + -1 < keyValueArray.findIndex(v => v['key'] == key), // Internal, private KO utility for updating model properties from within bindings // property: If the property being updated is (or might be) an observable, pass it here @@ -2013,37 +1918,29 @@ ko.expressionRewriting = (() => { } }; })(); -(function() { - var defaultBindingAttributeName = "data-bind"; +(() => { + const defaultBindingAttributeName = "data-bind", - ko.bindingProvider = function() { - this.bindingCache = {}; - }; + bindingCache = {}, - ko.utils.extend(ko.bindingProvider.prototype, { - 'nodeHasBindings': node => { - switch (node.nodeType) { - case 1: // Element - return node.getAttribute(defaultBindingAttributeName) != null; - case 8: // Comment node - return ko.virtualElements.hasBindingValue(node); - default: return false; - } + createBindingsStringEvaluatorViaCache = (bindingsString, cache, options) => { + var cacheKey = bindingsString + (options && options['valueAccessors'] || ''); + return cache[cacheKey] + || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, options)); }, - 'getBindings': function(node, bindingContext) { - var bindingsString = this['getBindingsString'](node, bindingContext); - return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null; - }, - - 'getBindingAccessors': function(node, bindingContext) { - var bindingsString = this['getBindingsString'](node, bindingContext); - return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node, { 'valueAccessors': true }) : null; + createBindingsStringEvaluator = (bindingsString, options) => { + // Build the source for a function that evaluates "expression" + // For each scope variable, add an extra level of "with" nesting + // Example result: with(sc1) { with(sc0) { return (expression) } } + var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString, options), + functionBody = "with($context){with($data||{}){return{" + rewrittenBindings + "}}}"; + return new Function("$context", "$element", functionBody); }, // The following function is only used internally by this default provider. // It's not part of the interface definition for a general binding provider. - 'getBindingsString': (node, bindingContext) => { + getBindingsString = node => { switch (node.nodeType) { case 1: return node.getAttribute(defaultBindingAttributeName); // Element case 8: return ko.virtualElements.virtualNodeBindingValue(node); // Comment node @@ -2053,33 +1950,34 @@ ko.expressionRewriting = (() => { // The following function is only used internally by this default provider. // It's not part of the interface definition for a general binding provider. - 'parseBindingsString': function(bindingsString, bindingContext, node, options) { + parseBindingsString = (bindingsString, bindingContext, node, options) => { try { - var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, this.bindingCache, options); + var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, bindingCache, options); return bindingFunction(bindingContext, node); } catch (ex) { ex.message = "Unable to parse bindings.\nBindings value: " + bindingsString + "\nMessage: " + ex.message; throw ex; } + }; + + ko.bindingProvider = new class + { + nodeHasBindings(node) { + switch (node.nodeType) { + case 1: // Element + return node.getAttribute(defaultBindingAttributeName) != null; + case 8: // Comment node + return ko.virtualElements.hasBindingValue(node); + default: return false; + } } - }); - ko.bindingProvider['instance'] = new ko.bindingProvider(); + getBindingAccessors(node, bindingContext) { + var bindingsString = getBindingsString(node, bindingContext); + return bindingsString ? parseBindingsString(bindingsString, bindingContext, node, { 'valueAccessors': true }) : null; + } + }; - function createBindingsStringEvaluatorViaCache(bindingsString, cache, options) { - var cacheKey = bindingsString + (options && options['valueAccessors'] || ''); - return cache[cacheKey] - || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, options)); - } - - function createBindingsStringEvaluator(bindingsString, options) { - // Build the source for a function that evaluates "expression" - // For each scope variable, add an extra level of "with" nesting - // Example result: with(sc1) { with(sc0) { return (expression) } } - var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString, options), - functionBody = "with($context){with($data||{}){return{" + rewrittenBindings + "}}}"; - return new Function("$context", "$element", functionBody); - } })(); (() => { // Hide or don't minify context properties, see https://github.com/knockout/knockout/issues/2294 @@ -2343,12 +2241,6 @@ ko.expressionRewriting = (() => { : ko.utils.objectMap(bindings, value => () => value); } - // This function is used if the binding provider doesn't include a getBindingAccessors function. - // It must be called with 'this' set to the provider instance. - function getBindingsAndMakeAccessors(node, context) { - return makeAccessorsFromFunction(this['getBindings'].bind(this, node, context)); - } - function validateThatBindingIsAllowedForVirtualElements(bindingName) { var validator = ko.virtualElements.allowedBindings[bindingName]; if (!validator) @@ -2359,22 +2251,7 @@ ko.expressionRewriting = (() => { var nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement); if (nextInQueue) { - var currentChild, - provider = ko.bindingProvider['instance'], - preprocessNode = provider['preprocessNode']; - - // Preprocessing allows a binding provider to mutate a node before bindings are applied to it. For example it's - // possible to insert new siblings after it, and/or replace the node with a different one. This can be used to - // implement custom binding syntaxes, such as {{ value }} for string interpolation, or custom element types that - // trigger insertion of