imask
Version:
vanilla javascript input mask
315 lines (295 loc) • 11.3 kB
JavaScript
import { escapeRegExp, DIRECTION } from '../core/utils.js';
import ChangeDetails from '../core/change-details.js';
import Masked from './base.js';
import IMask from '../core/holder.js';
import '../core/continuous-tail-details.js';
var _MaskedNumber;
/** Number mask */
class MaskedNumber extends Masked {
/** Single char */
/** Single char */
/** Array of single chars */
/** */
/** */
/** Digits after point */
/** Flag to remove leading and trailing zeros in the end of editing */
/** Flag to pad trailing zeros after point in the end of editing */
/** Enable characters overwriting */
/** */
/** */
/** */
/** Format typed value to string */
/** Parse string to get typed value */
constructor(opts) {
super({
...MaskedNumber.DEFAULTS,
...opts
});
}
updateOptions(opts) {
super.updateOptions(opts);
}
_update(opts) {
super._update(opts);
this._updateRegExps();
}
_updateRegExps() {
const start = '^' + (this.allowNegative ? '[+|\\-]?' : '');
const mid = '\\d*';
const end = (this.scale ? "(" + escapeRegExp(this.radix) + "\\d{0," + this.scale + "})?" : '') + '$';
this._numberRegExp = new RegExp(start + mid + end);
this._mapToRadixRegExp = new RegExp("[" + this.mapToRadix.map(escapeRegExp).join('') + "]", 'g');
this._thousandsSeparatorRegExp = new RegExp(escapeRegExp(this.thousandsSeparator), 'g');
}
_removeThousandsSeparators(value) {
return value.replace(this._thousandsSeparatorRegExp, '');
}
_insertThousandsSeparators(value) {
// https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
const parts = value.split(this.radix);
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.thousandsSeparator);
return parts.join(this.radix);
}
doPrepareChar(ch, flags) {
if (flags === void 0) {
flags = {};
}
const [prepCh, details] = super.doPrepareChar(this._removeThousandsSeparators(this.scale && this.mapToRadix.length && (
/*
radix should be mapped when
1) input is done from keyboard = flags.input && flags.raw
2) unmasked value is set = !flags.input && !flags.raw
and should not be mapped when
1) value is set = flags.input && !flags.raw
2) raw value is set = !flags.input && flags.raw
*/
flags.input && flags.raw || !flags.input && !flags.raw) ? ch.replace(this._mapToRadixRegExp, this.radix) : ch), flags);
if (ch && !prepCh) details.skip = true;
if (prepCh && !this.allowPositive && !this.value && prepCh !== '-') details.aggregate(this._appendChar('-'));
return [prepCh, details];
}
_separatorsCount(to, extendOnSeparators) {
if (extendOnSeparators === void 0) {
extendOnSeparators = false;
}
let count = 0;
for (let pos = 0; pos < to; ++pos) {
if (this._value.indexOf(this.thousandsSeparator, pos) === pos) {
++count;
if (extendOnSeparators) to += this.thousandsSeparator.length;
}
}
return count;
}
_separatorsCountFromSlice(slice) {
if (slice === void 0) {
slice = this._value;
}
return this._separatorsCount(this._removeThousandsSeparators(slice).length, true);
}
extractInput(fromPos, toPos, flags) {
if (fromPos === void 0) {
fromPos = 0;
}
if (toPos === void 0) {
toPos = this.displayValue.length;
}
[fromPos, toPos] = this._adjustRangeWithSeparators(fromPos, toPos);
return this._removeThousandsSeparators(super.extractInput(fromPos, toPos, flags));
}
_appendCharRaw(ch, flags) {
if (flags === void 0) {
flags = {};
}
const prevBeforeTailValue = flags.tail && flags._beforeTailState ? flags._beforeTailState._value : this._value;
const prevBeforeTailSeparatorsCount = this._separatorsCountFromSlice(prevBeforeTailValue);
this._value = this._removeThousandsSeparators(this.value);
const oldValue = this._value;
this._value += ch;
const num = this.number;
let accepted = !isNaN(num);
let skip = false;
if (accepted) {
let fixedNum;
if (this.min != null && this.min < 0 && this.number < this.min) fixedNum = this.min;
if (this.max != null && this.max > 0 && this.number > this.max) fixedNum = this.max;
if (fixedNum != null) {
if (this.autofix) {
this._value = this.format(fixedNum, this).replace(MaskedNumber.UNMASKED_RADIX, this.radix);
skip || (skip = oldValue === this._value && !flags.tail); // if not changed on tail it's still ok to proceed
} else {
accepted = false;
}
}
accepted && (accepted = Boolean(this._value.match(this._numberRegExp)));
}
let appendDetails;
if (!accepted) {
this._value = oldValue;
appendDetails = new ChangeDetails();
} else {
appendDetails = new ChangeDetails({
inserted: this._value.slice(oldValue.length),
rawInserted: skip ? '' : ch,
skip
});
}
this._value = this._insertThousandsSeparators(this._value);
const beforeTailValue = flags.tail && flags._beforeTailState ? flags._beforeTailState._value : this._value;
const beforeTailSeparatorsCount = this._separatorsCountFromSlice(beforeTailValue);
appendDetails.tailShift += (beforeTailSeparatorsCount - prevBeforeTailSeparatorsCount) * this.thousandsSeparator.length;
return appendDetails;
}
_findSeparatorAround(pos) {
if (this.thousandsSeparator) {
const searchFrom = pos - this.thousandsSeparator.length + 1;
const separatorPos = this.value.indexOf(this.thousandsSeparator, searchFrom);
if (separatorPos <= pos) return separatorPos;
}
return -1;
}
_adjustRangeWithSeparators(from, to) {
const separatorAroundFromPos = this._findSeparatorAround(from);
if (separatorAroundFromPos >= 0) from = separatorAroundFromPos;
const separatorAroundToPos = this._findSeparatorAround(to);
if (separatorAroundToPos >= 0) to = separatorAroundToPos + this.thousandsSeparator.length;
return [from, to];
}
remove(fromPos, toPos) {
if (fromPos === void 0) {
fromPos = 0;
}
if (toPos === void 0) {
toPos = this.displayValue.length;
}
[fromPos, toPos] = this._adjustRangeWithSeparators(fromPos, toPos);
const valueBeforePos = this.value.slice(0, fromPos);
const valueAfterPos = this.value.slice(toPos);
const prevBeforeTailSeparatorsCount = this._separatorsCount(valueBeforePos.length);
this._value = this._insertThousandsSeparators(this._removeThousandsSeparators(valueBeforePos + valueAfterPos));
const beforeTailSeparatorsCount = this._separatorsCountFromSlice(valueBeforePos);
return new ChangeDetails({
tailShift: (beforeTailSeparatorsCount - prevBeforeTailSeparatorsCount) * this.thousandsSeparator.length
});
}
nearestInputPos(cursorPos, direction) {
if (!this.thousandsSeparator) return cursorPos;
switch (direction) {
case DIRECTION.NONE:
case DIRECTION.LEFT:
case DIRECTION.FORCE_LEFT:
{
const separatorAtLeftPos = this._findSeparatorAround(cursorPos - 1);
if (separatorAtLeftPos >= 0) {
const separatorAtLeftEndPos = separatorAtLeftPos + this.thousandsSeparator.length;
if (cursorPos < separatorAtLeftEndPos || this.value.length <= separatorAtLeftEndPos || direction === DIRECTION.FORCE_LEFT) {
return separatorAtLeftPos;
}
}
break;
}
case DIRECTION.RIGHT:
case DIRECTION.FORCE_RIGHT:
{
const separatorAtRightPos = this._findSeparatorAround(cursorPos);
if (separatorAtRightPos >= 0) {
return separatorAtRightPos + this.thousandsSeparator.length;
}
}
}
return cursorPos;
}
doCommit() {
if (this.value) {
const number = this.number;
let validnum = number;
// check bounds
if (this.min != null) validnum = Math.max(validnum, this.min);
if (this.max != null) validnum = Math.min(validnum, this.max);
if (validnum !== number) this.unmaskedValue = this.format(validnum, this);
let formatted = this.value;
if (this.normalizeZeros) formatted = this._normalizeZeros(formatted);
if (this.padFractionalZeros && this.scale > 0) formatted = this._padFractionalZeros(formatted);
this._value = formatted;
}
super.doCommit();
}
_normalizeZeros(value) {
const parts = this._removeThousandsSeparators(value).split(this.radix);
// remove leading zeros
parts[0] = parts[0].replace(/^(\D*)(0*)(\d*)/, (match, sign, zeros, num) => sign + num);
// add leading zero
if (value.length && !/\d$/.test(parts[0])) parts[0] = parts[0] + '0';
if (parts.length > 1) {
parts[1] = parts[1].replace(/0*$/, ''); // remove trailing zeros
if (!parts[1].length) parts.length = 1; // remove fractional
}
return this._insertThousandsSeparators(parts.join(this.radix));
}
_padFractionalZeros(value) {
if (!value) return value;
const parts = value.split(this.radix);
if (parts.length < 2) parts.push('');
parts[1] = parts[1].padEnd(this.scale, '0');
return parts.join(this.radix);
}
doSkipInvalid(ch, flags, checkTail) {
if (flags === void 0) {
flags = {};
}
const dropFractional = this.scale === 0 && ch !== this.thousandsSeparator && (ch === this.radix || ch === MaskedNumber.UNMASKED_RADIX || this.mapToRadix.includes(ch));
return super.doSkipInvalid(ch, flags, checkTail) && !dropFractional;
}
get unmaskedValue() {
return this._removeThousandsSeparators(this._normalizeZeros(this.value)).replace(this.radix, MaskedNumber.UNMASKED_RADIX);
}
set unmaskedValue(unmaskedValue) {
super.unmaskedValue = unmaskedValue;
}
get typedValue() {
return this.parse(this.unmaskedValue, this);
}
set typedValue(n) {
this.rawInputValue = this.format(n, this).replace(MaskedNumber.UNMASKED_RADIX, this.radix);
}
/** Parsed Number */
get number() {
return this.typedValue;
}
set number(number) {
this.typedValue = number;
}
get allowNegative() {
return this.min != null && this.min < 0 || this.max != null && this.max < 0;
}
get allowPositive() {
return this.min != null && this.min > 0 || this.max != null && this.max > 0;
}
typedValueEquals(value) {
// handle 0 -> '' case (typed = 0 even if value = '')
// for details see https://github.com/uNmAnNeR/imaskjs/issues/134
return (super.typedValueEquals(value) || MaskedNumber.EMPTY_VALUES.includes(value) && MaskedNumber.EMPTY_VALUES.includes(this.typedValue)) && !(value === 0 && this.value === '');
}
}
_MaskedNumber = MaskedNumber;
MaskedNumber.UNMASKED_RADIX = '.';
MaskedNumber.EMPTY_VALUES = [...Masked.EMPTY_VALUES, 0];
MaskedNumber.DEFAULTS = {
...Masked.DEFAULTS,
mask: Number,
radix: ',',
thousandsSeparator: '',
mapToRadix: [_MaskedNumber.UNMASKED_RADIX],
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
scale: 2,
normalizeZeros: true,
padFractionalZeros: false,
parse: Number,
format: n => n.toLocaleString('en-US', {
useGrouping: false,
maximumFractionDigits: 20
})
};
IMask.MaskedNumber = MaskedNumber;
export { MaskedNumber as default };