Skip to content

Commit e5b3357

Browse files
thorn0claude
andcommitted
Fix numeric min/max validation across input, blur, isValid, and setvalue
Input / blur / isValid - Make below-min postValidation symmetric with above-max: when SetMaxOnOverflow is true and the value goes negative past min, clamp to min; otherwise reject the keystroke. - Reject a negation toggle when the resulting value would fall out of range on either side. - After revalidateMask strips the negation sign from the buffer, an overflowing positive in postValidation can be a valid negative. The max-overflow check consults the live DOM (skipped during checkVal where the DOM is stale) and falls back to opts.min < 0 in element-less contexts (Inputmask.isValid / Inputmask.format), returning currentResult instead of clamping. - Empty-field guard checks maskset.validPositions.length > 0 rather than buffer.join() !== "", so mask-literal padding and whitespace-only values no longer trigger a clamp to min on blur. setvalue - applyInputValue invokes onBeforeMask with __skipRounding on the setvalue path: the alias parser clamps before checkVal while the parseFloat round-trip that mangles bignums (#2715) is suppressed. - Internal numeric rewrites (negation-delete, radix-dance) call applyInputValue directly via setBufferAndCaret with skipOnBeforeMask=true, bypassing the parser for already-clean masked buffers without round-tripping through the public setvalue trigger. Tests added across DOM input, element-less APIs, and the setvalue path (in-range pass-through, empty, number-arg, formatted prefix/groupSeparator above and below range, European locale, onBeforeMask=null, bignum, internal-rewrite bypass). Closes #1763, #2715, #2775, #2846, #2863, #2651. Adds regression coverage for previously closed #951, #2284, #2829. Also covers the element-less-validation case from #2485 (input-path RangeError in #2485 out of scope). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aa4f229 commit e5b3357

5 files changed

Lines changed: 1037 additions & 61 deletions

File tree

lib/eventhandlers.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -523,23 +523,21 @@ const EventHandlers = {
523523
},
524524
setValueEvent: function (e) {
525525
const inputmask = this.inputmask,
526-
$ = inputmask.dependencyLib;
527-
let input = this,
528-
value = e && e.detail ? e.detail[0] : arguments[1];
529-
530-
if (value === undefined) {
531-
value = input.inputmask._valueGet(true);
532-
}
526+
$ = inputmask.dependencyLib,
527+
input = this,
528+
explicitValue = e && e.detail ? e.detail[0] : arguments[1],
529+
caretPos = e && e.detail ? e.detail[1] : arguments[2];
533530

534531
applyInputValue(
535532
input,
536-
value,
533+
explicitValue !== undefined ? explicitValue : input.inputmask._valueGet(true),
537534
new $.Event("input"),
538-
(e && e.detail ? e.detail[0] : arguments[1]) !== undefined
535+
false,
536+
explicitValue !== undefined
539537
);
540538

541-
if ((e.detail && e.detail[1] !== undefined) || arguments[2] !== undefined) {
542-
caret.call(inputmask, input, e.detail ? e.detail[1] : arguments[2]);
539+
if (caretPos !== undefined) {
540+
caret.call(inputmask, input, caretPos);
543541
}
544542
},
545543
focusEvent: function (e) {

lib/extensions/inputmask.numeric.extensions.js

Lines changed: 94 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
Licensed under the MIT license
66
*/
77
import { escapeRegex } from "../escapeRegex";
8+
import { applyInputValue } from "../inputHandling";
89
import Inputmask from "../inputmask";
910
import { keys } from "../keycode";
10-
import { seekNext } from "../positioning";
11+
import { caret, seekNext } from "../positioning";
1112

1213
const $ = Inputmask.dependencyLib;
1314

@@ -57,6 +58,30 @@ function alignDigits(buffer, digits, opts, force) {
5758
return buffer;
5859
}
5960

61+
function unmaskAsNumber(str, opts) {
62+
return opts.onUnMask(
63+
str,
64+
undefined,
65+
$.extend({}, opts, { unmaskAsNumber: true })
66+
);
67+
}
68+
69+
function boundaryBuffer(bound, opts) {
70+
return alignDigits(
71+
bound.toString().replace(".", opts.radixPoint).split(""),
72+
opts.digits,
73+
opts
74+
).reverse();
75+
}
76+
77+
// Internal numeric rewrites (negation-delete, radix-dance) — push an
78+
// already-clean masked buffer back into the input without re-running the
79+
// alias parser.
80+
function setBufferAndCaret(input, value, caretBegin) {
81+
applyInputValue(input, value, new $.Event("input"), true);
82+
caret.call(input.inputmask, input, caretBegin);
83+
}
84+
6085
function findValidator(symbol, maskset) {
6186
let posNdx = 0;
6287
if (symbol === "+") {
@@ -387,14 +412,31 @@ Inputmask.extendAliases({
387412
pos = handleRadixDance(pos, c, radixPos, maskset, opts);
388413
if (c === "-" || c === opts.negationSymbol.front) {
389414
if (opts.allowMinus !== true) return false;
390-
let isNegative = false,
391-
front = findValid("+", maskset),
415+
let isNegative = false;
416+
const front = findValid("+", maskset),
392417
back = findValid("-", maskset);
393418
if (front !== -1) {
394419
isNegative = [front];
395420
if (back !== -1) isNegative.push(back);
396421
}
397422

423+
const checkMax = isNegative !== false && opts.max !== null,
424+
checkMin = isNegative === false && opts.min !== null;
425+
// Reject typing "-" against a non-negative min — alignDigits would
426+
// pad the orphan sign to "-0". SetMaxOnOverflow=true has its own
427+
// boundary refresh in postValidation.
428+
if (!opts.SetMaxOnOverflow && checkMin && opts.min >= 0) return false;
429+
// Reject sign flips that would push the buffer out of range.
430+
// postValidation's range check doesn't fire after the validator's
431+
// `{remove: ...}` return (toggle-off path), so overflow on that
432+
// path must be caught here.
433+
if ((checkMax || checkMin) && this.maskset.validPositions.length > 0) {
434+
const absVal = Math.abs(
435+
unmaskAsNumber(buffer.slice().reverse().join(""), opts)
436+
);
437+
if (checkMax && absVal > opts.max) return false;
438+
if (checkMin && -absVal < opts.min) return false;
439+
}
398440
return isNegative !== false
399441
? {
400442
remove: isNegative,
@@ -509,37 +551,39 @@ Inputmask.extendAliases({
509551
if (currentResult === false) return currentResult;
510552
if (strict) return true;
511553
if (opts.min !== null || opts.max !== null) {
512-
const unmasked = opts.onUnMask(
554+
const unmasked = unmaskAsNumber(
513555
buffer.slice().reverse().join(""),
514-
undefined,
515-
$.extend({}, opts, {
516-
unmaskAsNumber: true
517-
})
556+
opts
518557
);
519558
if (
520559
opts.min !== null &&
521560
unmasked < opts.min &&
522-
fromAlternate !== true &&
561+
(fromAlternate !== true || unmasked < 0) &&
523562
(unmasked.toString().length > opts.min.toString().length || // > instead of >= because we want to allow to type a bigger number
524563
buffer[0] === opts.radixPoint || // disallow radixpoint when value is smaller than min
525564
unmasked < 0)
526565
) {
527-
return false;
528-
// return {
529-
// refreshFromBuffer: true,
530-
// buffer: alignDigits(opts.min.toString().replace(".", opts.radixPoint).split(""), opts.digits, opts).reverse()
531-
// };
566+
return unmasked < 0 && opts.SetMaxOnOverflow
567+
? {
568+
refreshFromBuffer: true,
569+
buffer: boundaryBuffer(opts.min, opts)
570+
}
571+
: false;
532572
}
533573

534574
if (opts.max !== null && opts.max >= 0 && unmasked > opts.max) {
575+
// #2846: revalidateMask may strip the negation sign, so an
576+
// overflowing positive here can actually be a valid negative.
577+
// Consult the DOM (stale during checkval) or, element-less,
578+
// trust min < 0.
579+
const isNegativeContext = this.el
580+
? !fromCheckval && unmaskAsNumber(this._valueGet(true), opts) < 0
581+
: opts.min !== null && opts.min < 0;
582+
if (isNegativeContext) return currentResult;
535583
return opts.SetMaxOnOverflow
536584
? {
537585
refreshFromBuffer: true,
538-
buffer: alignDigits(
539-
opts.max.toString().replace(".", opts.radixPoint).split(""),
540-
opts.digits,
541-
opts
542-
).reverse()
586+
buffer: boundaryBuffer(opts.max, opts)
543587
}
544588
: false;
545589
}
@@ -606,6 +650,10 @@ Inputmask.extendAliases({
606650
maskedValue = maskedValue.replace(escapeRegex(opts.radixPoint), ".");
607651
return isFinite(maskedValue);
608652
},
653+
// Numeric alias onBeforeMask hook — parses/normalizes a value into the
654+
// alias buffer format and clamps to min/max.
655+
// #2715: opts.__skipRounding suppresses the parseFloat round-trip so bignum
656+
// precision survives the setvalue path; clamping and other transforms remain.
609657
onBeforeMask: function (initialValue, opts) {
610658
initialValue = initialValue ?? "";
611659
const radixPoint = opts.radixPoint || ",";
@@ -637,7 +685,10 @@ Inputmask.extendAliases({
637685
: opts.digits < decimalPart.length
638686
? opts.digits
639687
: decimalPart.length;
640-
if (decimalPart !== "" || !opts.digitsOptional) {
688+
if (
689+
!opts.__skipRounding &&
690+
(decimalPart !== "" || !opts.digitsOptional)
691+
) {
641692
const digitsFactor = Math.pow(10, digits || 1);
642693

643694
// make the initialValue a valid javascript number for the parsefloat
@@ -659,16 +710,22 @@ Inputmask.extendAliases({
659710
);
660711
}
661712

713+
let clamped = false;
662714
if (initialValue !== "" && (opts.min !== null || opts.max !== null)) {
663715
const numberValue = initialValue.toString().replace(radixPoint, ".");
664716
if (opts.min !== null && numberValue < opts.min) {
665717
initialValue = opts.min.toString().replace(".", radixPoint);
718+
clamped = true;
666719
} else if (opts.max !== null && numberValue > opts.max) {
667720
initialValue = opts.max.toString().replace(".", radixPoint);
721+
clamped = true;
668722
}
669723
}
670724

671-
if (isNegative && initialValue.charAt(0) !== "-") {
725+
// After a clamp the boundary's own sign already lives in initialValue —
726+
// re-prepending the original input's "-" would invert nonneg boundaries
727+
// (setvalue("-5") with min:10 → "-10") or strand "-0" (min:0).
728+
if (isNegative && !clamped && initialValue.charAt(0) !== "-") {
672729
initialValue = "-" + initialValue;
673730
}
674731
return alignDigits(
@@ -716,35 +773,23 @@ Inputmask.extendAliases({
716773
switch (e.type) {
717774
case "blur":
718775
case "checkval":
719-
if (opts.min !== null || opts.max !== null) {
720-
const unmasked = opts.onUnMask(
776+
if (
777+
(opts.min !== null || opts.max !== null) &&
778+
this.maskset.validPositions.length > 0
779+
) {
780+
const unmasked = unmaskAsNumber(
721781
buffer.slice().reverse().join(""),
722-
undefined,
723-
$.extend({}, opts, {
724-
unmaskAsNumber: true
725-
})
782+
opts
726783
);
727-
if (
728-
opts.min !== null &&
729-
unmasked < opts.min &&
730-
buffer.join() !== ""
731-
) {
784+
if (opts.min !== null && unmasked < opts.min) {
732785
return {
733786
refreshFromBuffer: true,
734-
buffer: alignDigits(
735-
opts.min.toString().replace(".", opts.radixPoint).split(""),
736-
opts.digits,
737-
opts
738-
).reverse()
787+
buffer: boundaryBuffer(opts.min, opts)
739788
};
740789
} else if (opts.max !== null && unmasked > opts.max) {
741790
return {
742791
refreshFromBuffer: true,
743-
buffer: alignDigits(
744-
opts.max.toString().replace(".", opts.radixPoint).split(""),
745-
opts.digits,
746-
opts
747-
).reverse()
792+
buffer: boundaryBuffer(opts.max, opts)
748793
};
749794
}
750795
}
@@ -850,7 +895,7 @@ Inputmask.extendAliases({
850895
bffr = buffer.slice().reverse();
851896
if (opts.negationSymbol.front !== "") bffr.shift();
852897
if (opts.negationSymbol.back !== "") bffr.pop();
853-
$input.trigger("setvalue", [bffr.join(""), caretPos.begin]);
898+
setBufferAndCaret(this, bffr.join(""), caretPos.begin);
854899
return false;
855900
} else if (opts._radixDance === true) {
856901
const radixPos = buffer.indexOf(opts.radixPoint);
@@ -889,19 +934,21 @@ Inputmask.extendAliases({
889934
if (restoreCaretPos) {
890935
caretPos = restoreCaretPos;
891936
}
892-
$input.trigger("setvalue", [
937+
setBufferAndCaret(
938+
this,
893939
bffr,
894940
caretPos.begin >= bffr.length ? radixPos + 1 : caretPos.begin
895-
]);
941+
);
896942
return false;
897943
}
898944
} else if (radixPos === 0) {
899945
bffr = buffer.slice().reverse();
900946
bffr.pop();
901-
$input.trigger("setvalue", [
947+
setBufferAndCaret(
948+
this,
902949
bffr.join(""),
903950
caretPos.begin >= bffr.length ? bffr.length : caretPos.begin
904-
]);
951+
);
905952
return false;
906953
}
907954
}

lib/inputHandling.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@ export {
2424
writeBuffer
2525
};
2626

27-
function applyInputValue(input, value, initialEvent, strict) {
27+
function applyInputValue(input, value, initialEvent, skipOnBeforeMask, skipRounding) {
2828
const inputmask = input ? input.inputmask : this,
2929
opts = inputmask.opts;
3030

3131
input.inputmask.refreshValue = false;
32-
if (strict !== true && typeof opts.onBeforeMask === "function")
33-
value = opts.onBeforeMask.call(inputmask, value, opts) || value;
32+
// skipRounding suppresses onBeforeMask's parseFloat round-trip while
33+
// preserving its other transforms — guards bignum precision (#2715) on the
34+
// setvalue path. skipOnBeforeMask bypasses the hook entirely for internal
35+
// buffer rewrites that already produced a clean masked value (#2846).
36+
if (skipOnBeforeMask !== true && typeof opts.onBeforeMask === "function") {
37+
const callOpts = skipRounding === true ? { ...opts, __skipRounding: true } : opts;
38+
value = opts.onBeforeMask.call(inputmask, value, callOpts) || value;
39+
}
3440
value = (value || "").toString().split("");
3541
checkVal(input, true, false, value, initialEvent);
3642
inputmask.undoValue = inputmask._valueGet(true);

0 commit comments

Comments
 (0)