/** * Intl Number Input 1.4.1 * (c) 2023 Matthias Stiller * @license MIT */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const escapeRegExp = (str) => { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }; const removeLeadingZeros = (str) => { return str.replace(/^0+(0$|[^0])/, '$1'); }; const count = (str, search) => { return (str.match(new RegExp(escapeRegExp(search), 'g')) || []).length; }; const substringBefore = (str, search) => { return str.substring(0, str.indexOf(search)); }; /** * Available format styles. * * @public */ exports.NumberFormatStyle = void 0; (function (NumberFormatStyle) { /** * Use currency formatting. */ NumberFormatStyle["Currency"] = "currency"; /** * Use plain number formatting (default). */ NumberFormatStyle["Decimal"] = "decimal"; /** * Use percent formatting */ NumberFormatStyle["Percent"] = "percent"; /** * Use unit formatting. */ NumberFormatStyle["Unit"] = "unit"; })(exports.NumberFormatStyle || (exports.NumberFormatStyle = {})); /** * Available currency display options when using {@link NumberFormatStyle.Currency}. * * @public */ exports.CurrencyDisplay = void 0; (function (CurrencyDisplay) { /** * Use a localized currency symbol such as "€" (default). */ CurrencyDisplay["Symbol"] = "symbol"; /** * Use a narrow format symbol ("$100" rather than "US$100"). */ CurrencyDisplay["NarrowSymbol"] = "narrowSymbol"; /** * Use the ISO currency code. */ CurrencyDisplay["Code"] = "code"; /** * Use a localized currency name such as "dollar". */ CurrencyDisplay["Name"] = "name"; })(exports.CurrencyDisplay || (exports.CurrencyDisplay = {})); /** * Available unit display options when using {@link NumberFormatStyle.Unit}. * * @public */ exports.UnitDisplay = void 0; (function (UnitDisplay) { /** * Use a short formatting, for example "1024B" (default). */ UnitDisplay["Short"] = "short"; /** * Use a narrow formatting, for example "1024 byte". */ UnitDisplay["Narrow"] = "narrow"; /** * Use a long formatting, for example "1024 bytes". */ UnitDisplay["Long"] = "long"; })(exports.UnitDisplay || (exports.UnitDisplay = {})); const getPrefix = (parts) => parts .slice(0, parts.map((p) => p.type).indexOf('integer')) .map((p) => p.value) .join(''); const getSuffix = (parts) => { const types = parts.map((p) => p.type); return parts .slice(Math.max(types.lastIndexOf('integer'), types.indexOf('fraction')) + 1) .map((p) => p.value) .join(''); }; const DECIMAL_SEPARATORS = [',', '.', '٫']; const INTEGER_PATTERN = '(0|[1-9]\\d*)'; class NumberFormat { constructor(options) { var _a, _b, _c, _d; const createNumberFormat = (options) => new Intl.NumberFormat(locale, { currency, currencyDisplay, unit, unitDisplay, style, ...options }); const { formatStyle: style, currency, currencyDisplay, unit, unitDisplay, locale, precision } = options; const numberFormat = createNumberFormat({ minimumFractionDigits: style !== exports.NumberFormatStyle.Currency ? 1 : undefined }); const formatParts = numberFormat.formatToParts(style === exports.NumberFormatStyle.Percent ? 1234.56 : 123456); this.locale = locale; this.style = style; this.currency = currency; this.currencyDisplay = currencyDisplay; this.unit = unit; this.unitDisplay = unitDisplay; this.digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => i.toLocaleString(locale)); this.decimalSymbol = (_a = formatParts.find(({ type }) => type === 'decimal')) === null || _a === void 0 ? void 0 : _a.value; this.groupingSymbol = (_b = formatParts.find(({ type }) => type === 'group')) === null || _b === void 0 ? void 0 : _b.value; this.minusSymbol = substringBefore(Number(-1).toLocaleString(locale), this.digits[1]); if (this.decimalSymbol === undefined) { this.minimumFractionDigits = this.maximumFractionDigits = 0; } else if (typeof precision === 'number') { this.minimumFractionDigits = this.maximumFractionDigits = precision; } else if (typeof precision === 'object') { this.minimumFractionDigits = (_c = precision.min) !== null && _c !== void 0 ? _c : 0; this.maximumFractionDigits = (_d = precision.max) !== null && _d !== void 0 ? _d : 15; } else { const { maximumFractionDigits, minimumFractionDigits } = new Intl.NumberFormat(locale, { currency, unit, style }).resolvedOptions(); this.minimumFractionDigits = minimumFractionDigits; this.maximumFractionDigits = maximumFractionDigits; } this.prefix = getPrefix(numberFormat.formatToParts(1)); this.suffix = [getSuffix(createNumberFormat({ minimumFractionDigits: 0 }).formatToParts(1)), getSuffix(numberFormat.formatToParts(2))]; this.negativePrefix = getPrefix(numberFormat.formatToParts(-1)); } parse(str) { if (str) { const negative = this.isNegative(str); str = this.normalizeDigits(str); str = this.stripPrefixOrSuffix(str); str = this.stripMinusSymbol(str); const fraction = this.decimalSymbol ? `(?:${escapeRegExp(this.decimalSymbol)}(\\d*))?` : ''; const match = this.stripGroupingSeparator(str).match(new RegExp(`^${INTEGER_PATTERN}${fraction}$`)); if (match && this.isValidIntegerFormat(this.decimalSymbol ? str.split(this.decimalSymbol)[0] : str, Number(match[1]))) { const number = Number(`${negative ? '-' : ''}${this.onlyDigits(match[1])}.${this.onlyDigits(match[2] || '')}`); if (this.style === exports.NumberFormatStyle.Percent) { return Number((number / 100).toFixed(this.maximumFractionDigits + 2)); } else { return number; } } } return null; } isValidIntegerFormat(formattedNumber, integerNumber) { const options = { style: this.style, currency: this.currency, currencyDisplay: this.currencyDisplay, unit: this.unit, unitDisplay: this.unitDisplay, minimumFractionDigits: 0 }; if (this.style === exports.NumberFormatStyle.Percent) { integerNumber /= 100; } return [ this.stripPrefixOrSuffix(this.normalizeDigits(integerNumber.toLocaleString(this.locale, { ...options, useGrouping: true }))), this.stripPrefixOrSuffix(this.normalizeDigits(integerNumber.toLocaleString(this.locale, { ...options, useGrouping: false }))) ].includes(formattedNumber); } format(value, options = { minimumFractionDigits: this.minimumFractionDigits, maximumFractionDigits: this.maximumFractionDigits }) { return value != null ? new Intl.NumberFormat(this.locale, { style: this.style, currency: this.currency, currencyDisplay: this.currencyDisplay, unit: this.unit, unitDisplay: this.unitDisplay, ...options }).format(this.style === exports.NumberFormatStyle.Percent ? value / 100 : value) : ''; } toFraction(str) { return `${this.digits[0]}${this.decimalSymbol}${this.onlyLocaleDigits(str.substr(1)).substr(0, this.maximumFractionDigits)}`; } isFractionIncomplete(str) { return !!this.normalizeDigits(this.stripGroupingSeparator(str)).match(new RegExp(`^${INTEGER_PATTERN}${escapeRegExp(this.decimalSymbol)}$`)); } isNegative(str) { return str.startsWith(this.negativePrefix) || str.replace('-', this.minusSymbol).startsWith(this.minusSymbol); } insertPrefixOrSuffix(str, negative) { return `${negative ? this.negativePrefix : this.prefix}${str}${this.suffix[1]}`; } stripGroupingSeparator(str) { return this.groupingSymbol !== undefined ? str.replace(new RegExp(escapeRegExp(this.groupingSymbol), 'g'), '') : str; } stripMinusSymbol(str) { return str.replace('-', this.minusSymbol).replace(this.minusSymbol, ''); } stripPrefixOrSuffix(str) { return str.replace(this.negativePrefix, '').replace(this.prefix, '').replace(this.suffix[1], '').replace(this.suffix[0], ''); } normalizeDecimalSeparator(str, from) { DECIMAL_SEPARATORS.forEach((s) => { str = str.substr(0, from) + str.substr(from).replace(s, this.decimalSymbol); }); return str; } normalizeDigits(str) { if (this.digits[0] !== '0') { this.digits.forEach((digit, index) => { str = str.replace(new RegExp(digit, 'g'), String(index)); }); } return str; } onlyDigits(str) { return this.normalizeDigits(str).replace(/\D+/g, ''); } onlyLocaleDigits(str) { return str.replace(new RegExp(`[^${this.digits.join('')}]*`, 'g'), ''); } } class AbstractNumberMask { constructor(numberFormat) { this.numberFormat = numberFormat; } } class DefaultNumberMask extends AbstractNumberMask { conformToMask(str, previousConformedValue = '') { const negative = this.numberFormat.isNegative(str); const checkIncompleteValue = (str) => { if (str === '' && negative && previousConformedValue !== this.numberFormat.negativePrefix) { return ''; } else if (this.numberFormat.maximumFractionDigits > 0) { if (this.numberFormat.isFractionIncomplete(str)) { return str; } else if (str.startsWith(this.numberFormat.decimalSymbol)) { return this.numberFormat.toFraction(str); } } return null; }; let value = str; value = this.numberFormat.stripPrefixOrSuffix(value); value = this.numberFormat.stripMinusSymbol(value); const incompleteValue = checkIncompleteValue(value); if (incompleteValue != null) { return this.numberFormat.insertPrefixOrSuffix(incompleteValue, negative); } const [integer, ...fraction] = value.split(this.numberFormat.decimalSymbol); const integerDigits = removeLeadingZeros(this.numberFormat.onlyDigits(integer)); const fractionDigits = this.numberFormat.onlyDigits(fraction.join('')).substr(0, this.numberFormat.maximumFractionDigits); const invalidFraction = fraction.length > 0 && fractionDigits.length === 0; const invalidNegativeValue = integerDigits === '' && negative && (previousConformedValue === str.slice(0, -1) || previousConformedValue !== this.numberFormat.negativePrefix); if (invalidFraction || invalidNegativeValue) { return previousConformedValue; } else if (integerDigits.match(/\d+/)) { return { numberValue: Number(`${negative ? '-' : ''}${integerDigits}.${fractionDigits}`), fractionDigits }; } else { return ''; } } } class AutoDecimalDigitsNumberMask extends AbstractNumberMask { conformToMask(str, previousConformedValue = '') { if (str === '' || (this.numberFormat.parse(previousConformedValue) === 0 && this.numberFormat.stripPrefixOrSuffix(previousConformedValue).slice(0, -1) === this.numberFormat.stripPrefixOrSuffix(str))) { return ''; } const negative = this.numberFormat.isNegative(str); const numberValue = this.numberFormat.stripMinusSymbol(str) === '' ? -0 : Number(`${negative ? '-' : ''}${removeLeadingZeros(this.numberFormat.onlyDigits(str))}`) / Math.pow(10, this.numberFormat.maximumFractionDigits); return { numberValue, fractionDigits: numberValue.toFixed(this.numberFormat.maximumFractionDigits).slice(-this.numberFormat.maximumFractionDigits) }; } } /** * The `NumberInput` class turns a `HTMLInputElement` into a number input field. * * @public */ class NumberInput { /** * Creates a new {@link NumberInput} instance. * * @param args - The number input settings. */ constructor(args) { this.inputEventListener = (e) => { const { value, selectionStart } = this.el; const inputEvent = e; if (selectionStart && inputEvent.data && DECIMAL_SEPARATORS.includes(inputEvent.data)) { this.decimalSymbolInsertedAt = selectionStart - 1; } this.format(value); if (this.focus && selectionStart != null) { const getCaretPositionAfterFormat = () => { const { prefix, suffix, decimalSymbol, maximumFractionDigits, groupingSymbol } = this.numberFormat; let caretPositionFromLeft = value.length - selectionStart; const newValueLength = this.formattedValue.length; if (this.formattedValue.substring(selectionStart, 1) === groupingSymbol && count(this.formattedValue, groupingSymbol) === count(value, groupingSymbol) + 1) { return newValueLength - caretPositionFromLeft - 1; } if (newValueLength < caretPositionFromLeft) { return selectionStart; } if (decimalSymbol !== undefined && value.indexOf(decimalSymbol) !== -1) { const decimalSymbolPosition = value.indexOf(decimalSymbol) + 1; if (Math.abs(newValueLength - value.length) > 1 && selectionStart <= decimalSymbolPosition) { return this.formattedValue.indexOf(decimalSymbol) + 1; } else { if (!this.options.autoDecimalDigits && selectionStart > decimalSymbolPosition) { if (this.numberFormat.onlyDigits(value.substring(decimalSymbolPosition)).length - 1 === maximumFractionDigits) { caretPositionFromLeft -= 1; } } } } if (this.options.hidePrefixOrSuffixOnFocus) { return newValueLength - caretPositionFromLeft; } else { const getSuffixLength = (str) => (str.includes(suffix[1]) ? suffix[1] : str.includes(suffix[0]) ? suffix[0] : '').length; const oldSuffixLength = getSuffixLength(value); const newSuffixLength = getSuffixLength(this.formattedValue); const suffixLengthDifference = Math.abs(newSuffixLength - oldSuffixLength); return Math.max(newValueLength - Math.max(caretPositionFromLeft - suffixLengthDifference, newSuffixLength), prefix.length); } }; this.setCaretPosition(getCaretPositionAfterFormat()); } }; this.focusEventListener = () => { this.focus = true; this.numberValueOnFocus = this.numberValue; setTimeout(() => { const { value, selectionStart, selectionEnd } = this.el; this.format(value, this.options.hideNegligibleDecimalDigitsOnFocus); if (selectionStart != null && selectionEnd != null && Math.abs(selectionStart - selectionEnd) > 0) { this.setCaretPosition(0, this.el.value.length); } else if (selectionStart != null) { const getCaretPositionOnFocus = () => { const { prefix, suffix, groupingSymbol } = this.numberFormat; if (!this.options.hidePrefixOrSuffixOnFocus) { const suffixLength = suffix[this.numberValue === 1 ? 0 : 1].length; if (selectionStart >= value.length - suffixLength) { return this.formattedValue.length - suffixLength; } else if (selectionStart < prefix.length) { return prefix.length; } } let result = selectionStart; if (this.options.hidePrefixOrSuffixOnFocus) { result -= prefix.length; } if (this.options.hideGroupingSeparatorOnFocus && groupingSymbol !== undefined) { result -= count(value.substring(0, selectionStart), groupingSymbol); } return result; }; this.setCaretPosition(getCaretPositionOnFocus()); } }); }; this.blurEventListener = () => { this.focus = false; this.applyFixedFractionFormat(this.numberValue, this.numberValueOnFocus !== this.numberValue); }; this.el = args.el; this.onInput = args.onInput; this.onChange = args.onChange; this.el.addEventListener('input', this.inputEventListener); this.el.addEventListener('focus', this.focusEventListener); this.el.addEventListener('blur', this.blurEventListener); this.init(args.options); this.setValue(this.numberFormat.parse(this.el.value)); } /** * Destroys a {@link NumberInput} instance, removing all event listeners. */ destroy() { this.el.removeEventListener('input', this.inputEventListener); this.el.removeEventListener('focus', this.focusEventListener); this.el.removeEventListener('blur', this.blurEventListener); } /** * Updates the options. * * @param options - The new options. */ setOptions(options) { this.init(options); this.applyFixedFractionFormat(this.numberValue, true); } /** * Gets the current value. */ getValue() { const numberValue = this.options.exportValueAsInteger && this.numberValue != null ? this.toInteger(this.numberValue) : this.numberValue; return { number: numberValue, formatted: this.formattedValue }; } /** * Sets a value programmatically. * * @param value - The new value. */ setValue(value) { const newValue = this.options.exportValueAsInteger && value != null ? this.toFloat(value) : value; if (newValue !== this.numberValue) { this.applyFixedFractionFormat(newValue); } } /** * Increments the value by the configured {@link NumberInputOptions.step|step}. */ increment() { var _a; this.setValue(((_a = this.numberValue) !== null && _a !== void 0 ? _a : 0) + this.step); } /** * Decrements the value by the configured {@link NumberInputOptions.step|step}. */ decrement() { var _a; this.setValue(((_a = this.numberValue) !== null && _a !== void 0 ? _a : 0) - this.step); } init(options) { var _a; this.options = { autoSign: true, hideGroupingSeparatorOnFocus: true, hidePrefixOrSuffixOnFocus: true, hideNegligibleDecimalDigitsOnFocus: true, ...options }; if (this.options.autoDecimalDigits) { this.el.setAttribute('inputmode', 'numeric'); } else { this.el.setAttribute('inputmode', 'decimal'); } this.numberFormat = new NumberFormat(this.options); this.numberMask = this.options.autoDecimalDigits ? new AutoDecimalDigitsNumberMask(this.numberFormat) : new DefaultNumberMask(this.numberFormat); this.step = Math.max((_a = options.step) !== null && _a !== void 0 ? _a : 0, this.getDefaultStep()); this.minValue = this.getMinValue(); this.maxValue = this.getMaxValue(); } getMinValue() { var _a, _b; let min = this.toFloat(-Number.MAX_SAFE_INTEGER); if (((_a = this.options.valueRange) === null || _a === void 0 ? void 0 : _a.min) !== undefined) { min = Math.max((_b = this.options.valueRange) === null || _b === void 0 ? void 0 : _b.min, this.toFloat(-Number.MAX_SAFE_INTEGER)); } if (!this.options.autoSign && min < 0) { min = 0; } min = this.getNextStep(min); return min; } getMaxValue() { var _a, _b; let max = this.toFloat(Number.MAX_SAFE_INTEGER); if (((_a = this.options.valueRange) === null || _a === void 0 ? void 0 : _a.max) !== undefined) { max = Math.min((_b = this.options.valueRange) === null || _b === void 0 ? void 0 : _b.max, this.toFloat(Number.MAX_SAFE_INTEGER)); } if (!this.options.autoSign && max < 0) { max = this.toFloat(Number.MAX_SAFE_INTEGER); } return max; } getDefaultStep() { let defaultStep = 1; if (this.options.formatStyle === exports.NumberFormatStyle.Percent) { defaultStep /= 100; } return this.toFloat(defaultStep); } validateStep(value) { return this.toInteger(value) % this.toInteger(this.step) !== 0 ? this.getNextStep(value) : value; } getNextStep(value) { return Math.ceil(value / this.step) * this.step; } toFloat(value) { return value / 10 ** this.numberFormat.maximumFractionDigits; } toInteger(value) { return Number(value.toFixed(this.numberFormat.maximumFractionDigits).split('.').join('')); } validateValueRange(value) { return Math.min(Math.max(value, this.minValue), this.maxValue); } applyFixedFractionFormat(number, forcedChange = false) { var _a; if (number != null) { number = this.validateStep(this.validateValueRange(number)); if (this.options.formatStyle === exports.NumberFormatStyle.Percent) { number *= 100; } } this.format(this.numberFormat.format(number)); if (number !== this.numberValue || forcedChange) { (_a = this.onChange) === null || _a === void 0 ? void 0 : _a.call(this, this.getValue()); } } format(value, hideNegligibleDecimalDigits = false) { var _a; if (value != null) { if (this.decimalSymbolInsertedAt !== undefined) { value = this.numberFormat.normalizeDecimalSeparator(value, this.decimalSymbolInsertedAt); this.decimalSymbolInsertedAt = undefined; } const conformedValue = this.numberMask.conformToMask(value, this.formattedValue); let formattedValue; if (typeof conformedValue === 'object') { const { numberValue, fractionDigits } = conformedValue; let { maximumFractionDigits, minimumFractionDigits } = this.numberFormat; if (this.focus) { minimumFractionDigits = hideNegligibleDecimalDigits ? fractionDigits.replace(/0+$/, '').length : Math.min(maximumFractionDigits, fractionDigits.length); } else if (Number.isInteger(numberValue) && !this.options.autoDecimalDigits && (this.options.precision === undefined || minimumFractionDigits === 0)) { minimumFractionDigits = maximumFractionDigits = 0; } formattedValue = this.toInteger(Math.abs(numberValue)) > Number.MAX_SAFE_INTEGER ? this.formattedValue : this.numberFormat.format(numberValue, { useGrouping: this.options.useGrouping && !(this.focus && this.options.hideGroupingSeparatorOnFocus), minimumFractionDigits, maximumFractionDigits }); } else { formattedValue = conformedValue; } if (this.options.autoSign) { if (this.maxValue <= 0 && !this.numberFormat.isNegative(formattedValue) && this.numberFormat.parse(formattedValue) !== 0) { formattedValue = formattedValue.replace(this.numberFormat.prefix, this.numberFormat.negativePrefix); } if (this.minValue >= 0) { formattedValue = formattedValue.replace(this.numberFormat.negativePrefix, this.numberFormat.prefix); } } if (this.focus && this.options.hidePrefixOrSuffixOnFocus) { formattedValue = formattedValue .replace(this.numberFormat.negativePrefix, this.numberFormat.minusSymbol) .replace(this.numberFormat.prefix, '') .replace(this.numberFormat.suffix[1], '') .replace(this.numberFormat.suffix[0], ''); } this.el.value = formattedValue; this.numberValue = this.numberFormat.parse(formattedValue); } else { this.el.value = ''; this.numberValue = null; } this.formattedValue = this.el.value; (_a = this.onInput) === null || _a === void 0 ? void 0 : _a.call(this, this.getValue()); } setCaretPosition(start, end = start) { this.el.setSelectionRange(start, end); } } exports.NumberInput = NumberInput;