diff --git a/src/components/NumericInput/index.ts b/src/components/NumericInput/index.ts index c4e772e3..e2f47e19 100644 --- a/src/components/NumericInput/index.ts +++ b/src/components/NumericInput/index.ts @@ -254,7 +254,16 @@ class NumericInput extends InputElement { // remove spaces value = value.replace(/\s/g, ''); - // sanitize input to only allow short mathematical expressions to be evaluated + const currentValue = this._oldValue || 0; + + // handle percentages with a simple, non-backtracking regex + value = value.replace(/(\d+(?:\.\d+)?%)/g, (match: string) => { + const percent = parseFloat(match.slice(0, -1)); + const calculatedValue = (percent / 100) * currentValue; + return calculatedValue.toString(); + }); + + // sanitize input to only allow short mathematical expressions value = value.match(/^[*/+\-0-9().]+$/); if (value !== null && value[0].length < 20) { let expression = value[0]; @@ -267,35 +276,46 @@ class NumericInput extends InputElement { expression = expressionArr.join(operator); }); // eslint-disable-next-line - value = Function('"use strict";return (' + expression + ')')(); + value = Function(`"use strict";return (${expression})`)(); } } - } catch (error) { - value = null; - } - if (value === null || isNaN(value)) { - if (this._allowNull) { - return null; + if (value === null || value === undefined || value === '') { + if (this._allowNull) { + return null; + } + value = 0; } - value = 0; - } + value = Number(value); - // clamp between min max - if (this.min !== null && value < this.min) { - value = this.min; - } - if (this.max !== null && value > this.max) { - value = this.max; - } + if (isNaN(value)) { + if (this._allowNull) { + return null; + } + value = 0; + } - // fix precision - if (this.precision !== null) { - value = parseFloat(Number(value).toFixed(this.precision)); - } + // clamp between min max + if (this.min !== null && value < this.min) { + value = this.min; + } + if (this.max !== null && value > this.max) { + value = this.max; + } + + // fix precision + if (this.precision !== null) { + value = parseFloat(Number(value).toFixed(this.precision)); + } - return value; + return value; + } catch (error) { + if (this._allowNull) { + return null; + } + return 0; + } } protected _updateValue(value: number, force?: boolean) { diff --git a/test/components/numericinput.mjs b/test/components/numericinput.mjs index 179efff5..db1585a1 100644 --- a/test/components/numericinput.mjs +++ b/test/components/numericinput.mjs @@ -116,5 +116,83 @@ describe('NumericInput', () => { numericInput.value = "1+1+1+1+1+1+1+1+1+10"; strictEqual(numericInput.value, 0); }); + + describe('percentages', () => { + it('basic percentage', () => { + const numericInput = new NumericInput(); + + numericInput.value = 200; + numericInput.value = "50%"; + strictEqual(numericInput.value, 100); // 50% of 200 + + numericInput.value = 200; + numericInput.value = "150%"; + strictEqual(numericInput.value, 300); // 150% of 200 + }); + + it('percentage in expressions', () => { + const numericInput = new NumericInput(); + + numericInput.value = 100; + numericInput.value = "50% + 10"; + strictEqual(numericInput.value, 60); // (50% of 100) + 10 + + numericInput.value = 100; + numericInput.value = "25% * 2"; + strictEqual(numericInput.value, 50); // (25% of 100) * 2 + }); + + it('multiple percentages', () => { + const numericInput = new NumericInput(); + + numericInput.value = 100; + numericInput.value = "25% + 50%"; + strictEqual(numericInput.value, 75); // (25% of 100) + (50% of 100) + }); + + it('invalid percentages', () => { + const numericInput = new NumericInput(); + + numericInput.value = 100; + numericInput.value = "abc%"; + strictEqual(numericInput.value, 0); + + numericInput.value = 100; + numericInput.value = "%50"; + strictEqual(numericInput.value, 0); + + numericInput.value = 100; + numericInput.value = "50%%"; + strictEqual(numericInput.value, 0); + }); + + it('percentage with zero base value', () => { + const numericInput = new NumericInput(); + + numericInput.value = 0; + numericInput.value = "50%"; + strictEqual(numericInput.value, 0); // 50% of 0 is 0 + }); + + it('percentage with negative base value', () => { + const numericInput = new NumericInput(); + + numericInput.value = -100; + numericInput.value = "50%"; + strictEqual(numericInput.value, -50); // 50% of -100 + + numericInput.value = -100; + numericInput.value = "150%"; + strictEqual(numericInput.value, -150); // 150% of -100 + }); + + it('percentage with decimal base value', () => { + const numericInput = new NumericInput(); + + numericInput.value = 0.5; + numericInput.value = "200%"; + strictEqual(numericInput.value, 1); // 200% of 0.5 + }); + }); }); });