314 lines
11 KiB
JavaScript
314 lines
11 KiB
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 };
|