From 2e6389002f489e49fd8ebb892e1d00eac8d3b6b4 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Mon, 29 Jul 2024 23:01:59 +0300 Subject: [PATCH] webui: numeric inputs and NaNs allow empty numeric input fix element setters sometimes forgetting to set the value fix NaN equality comparison for element value getters --- code/html/spec/settings.spec.mjs | 18 ++++--- code/html/src/settings.mjs | 83 +++++++++++++++++++++++--------- 2 files changed, 70 insertions(+), 31 deletions(-) diff --git a/code/html/spec/settings.spec.mjs b/code/html/spec/settings.spec.mjs index a58345d0..7cf257c0 100644 --- a/code/html/spec/settings.spec.mjs +++ b/code/html/spec/settings.spec.mjs @@ -26,7 +26,7 @@ test('select unchanged with empty value when original is missing', () => { .toEqual(getOriginalForElement(node)); node.selectedIndex = -1; - expect(getDataForElement(node)).toBe(null); + expect(getDataForElement(node)).toBeNull(); assert(!isChangedElement(node)); node.selectedIndex = 1; @@ -53,22 +53,21 @@ test('number input unchanged with empty value when original is missing', () => { expect(getDataForElement(node)) .toEqual(getOriginalForElement(node)); - const data = 12345; - expect(getDataForElement(node)).toBe(null); + expect(getDataForElement(node)).toBeNaN(); assert(!isChangedElement(node)); - node.valueAsNumber = data; + setInputValue(node, 12345); assert(!isChangedElement(node)); assert(checkAndSetElementChanged(node)); - node.value = ''; + setInputValue(node, ''); assert(isChangedElement(node)); assert(checkAndSetElementChanged(node)); assert(!isChangedElement(node)); setOriginalsFromValuesForNode(node); - node.value = ''; + setInputValue(node, ''); assert(!checkAndSetElementChanged(node)); assert(!isChangedElement(node)); @@ -255,16 +254,19 @@ test('input value update', () => { input.value = ''; setInputValue(input, null); - expect(getDataForElement(input)).toBeNull(); + expect(getDataForElement(input)).toBeNaN(); setInputValue(input, 12345); expect(getDataForElement(input)).toBe(12345); + setInputValue(input, ''); + expect(getDataForElement(input)).toBeNaN(); + setInputValue(input, '56789'); expect(getDataForElement(input)).toBe(56789); setInputValue(input, 'text'); - expect(getDataForElement(input)).toBe(null); + expect(getDataForElement(input)).toBeNaN(); input.type = 'text'; input.value = ''; diff --git a/code/html/src/settings.mjs b/code/html/src/settings.mjs index d31edbfb..f47c769a 100644 --- a/code/html/src/settings.mjs +++ b/code/html/src/settings.mjs @@ -558,8 +558,7 @@ export function getDataForElement(elem) { case "number": case "range": - return !isNaN(elem.valueAsNumber) - ? elem.valueAsNumber : null; + return elem.valueAsNumber; } } else if (elem instanceof HTMLSelectElement) { @@ -602,7 +601,7 @@ export function getOriginalForElement(elem) { case "range": return (original !== undefined) ? parseInt(original) - : null; + : NaN; } } else if (elem instanceof HTMLSelectElement) { if (original === undefined) { @@ -707,19 +706,25 @@ export function setInputValue(input, value) { case "radio": input.checked = (value === input.value); break; + case "checkbox": input.checked = (typeof value === "boolean") ? value : (typeof value === "string") ? stringToBoolean(value) : (typeof value === "number") ? (value !== 0) : false; break; + case "number": - case "password": case "range": + input.valueAsNumber = + (typeof value === "string") ? parseInt(value) : + (typeof value === "number") ? value : NaN; + break; + + case "password": case "text": - if (value !== null) { - input.value = value.toString(); - } + input.value = + (value ?? "").toString(); break; } } @@ -796,26 +801,33 @@ export function setSelectValue(select, value) { }); } +/** + * @param {InputOrSelect} elem + */ +export function setOriginalFromValue(elem) { + if (elem instanceof HTMLInputElement) { + if (elem.readOnly) { + return; + } + + if (elem.type === "checkbox") { + elem.dataset["original"] = elem.checked.toString(); + } else { + elem.dataset["original"] = elem.value; + } + } else if (elem instanceof HTMLSelectElement) { + elem.dataset["original"] = selectedValues(elem).join(","); + } + + resetChangedElement(elem); +} + /** * @param {InputOrSelect[]} elems */ export function setOriginalsFromValues(elems) { for (let elem of elems) { - if (elem instanceof HTMLInputElement) { - if (elem.readOnly) { - continue; - } - - if (elem.type === "checkbox") { - elem.dataset["original"] = elem.checked.toString(); - } else { - elem.dataset["original"] = elem.value; - } - } else if (elem instanceof HTMLSelectElement) { - elem.dataset["original"] = selectedValues(elem).join(","); - } - - resetChangedElement(elem); + setOriginalFromValue(elem); } } @@ -905,8 +917,14 @@ function onEnumerableUpdateSpan(span, enumerables) { } /** + * @callback EnumerableElemCallback * @param {HTMLElement} elem * @param {EnumerableEntry[]} enumerables + * @returns {void} + */ + +/** + * @type {EnumerableElemCallback} */ function onEnumerableUpdateElem(elem, enumerables) { if (elem instanceof HTMLSelectElement) { @@ -1136,6 +1154,25 @@ export function initInputKeyValueElement(key, value) { const Settings = new SettingsBase(); +/** + * @param {InputOrSelect} elem + * @returns {boolean} + */ +function checkElementChanged(elem) { + const lhs = getOriginalForElement(elem); + const rhs = getDataForElement(elem); + + if (typeof lhs === "number" + && typeof rhs === "number" + && isNaN(lhs) + && isNaN(rhs)) + { + return false; + } + + return lhs !== rhs; +} + /** * @param {InputOrSelect} elem * @returns {boolean} @@ -1143,7 +1180,7 @@ const Settings = new SettingsBase(); export function checkAndSetElementChanged(elem) { const changed = isChangedElement(elem); - if (getOriginalForElement(elem) !== getDataForElement(elem)) { + if (checkElementChanged(elem)) { setChangedElement(elem); } else { resetChangedElement(elem);