UNPKG

imask

Version:

vanilla javascript input mask

315 lines (295 loc) 11.3 kB
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 };