/* eslint-disable @typescript-eslint/explicit-function-return-type */ 'use strict' import styles from './styles.module.css' // const React = require('react') import React from 'react' function _interopDefault(ex: typeof React) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex.default : ex } const React__default = _interopDefault(React) function __rest(s: Record | null, e: string | string[]) { const t = {} for (var p in s) { if (Object.prototype.hasOwnProperty.call(s, p) && !e.includes(p)) { t[p] = s[p] } } if (s != null && typeof Object.getOwnPropertySymbols === 'function') { for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (!e.includes(p[i]) && Object.prototype.propertyIsEnumerable.call(s, p[i])) { t[p[i]] = s[p[i]] } } } return t } let SourceType: { props?: any, event?: any }; (function (SourceType) { SourceType.event = 'event' SourceType.props = 'prop' })(SourceType || (SourceType = {})) // basic noop function function noop() { } function memoizeOnce(cb: { (prevValue: any, newValue: any): { from: { start: number, end: number }, to: { start: number, end: number } }, apply?: any }) { let lastArgs: string | any[] let lastValue: any return function () { const args = []; let len = arguments.length while (len--) args[len] = arguments[len] if (lastArgs && args.length === lastArgs.length && args.every(function (value, index) { return value === lastArgs[index] })) { return lastValue } lastArgs = args lastValue = cb.apply(void 0, args) return lastValue } } function charIsNumber(char: any) { return !!(char || '').match(/\d/) } function isNil(val: null | undefined) { return val === null || val === undefined } function isNanValue(val: number) { return typeof val === 'number' && isNaN(val) } function isNotValidValue(val: number) { return isNil(val) || isNanValue(val) || (typeof val === 'number' && !isFinite(val)) } function escapeRegExp(str: string) { return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&') } function getThousandsGroupRegex(thousandsGroupStyle: any) { switch (thousandsGroupStyle) { case 'lakh': return /(\d+?)(?=(\d\d)+(\d)(?!\d))(\.\d+)?/g case 'wan': return /(\d)(?=(\d{4})+(?!\d))/g case 'thousand': default: return /(\d)(?=(\d{3})+(?!\d))/g } } function applyThousandSeparator(str: string, thousandSeparator: string, thousandsGroupStyle: any) { const thousandsGroupRegex = getThousandsGroupRegex(thousandsGroupStyle) let index = str.search(/[1-9]/) index = index === -1 ? str.length : index return (str.substring(0, index) + str.substring(index, str.length).replace(thousandsGroupRegex, '$1' + thousandSeparator)) } function usePersistentCallback(cb: (value: any, valueIsNumericString: any) => { formattedValue: any, numAsString: any }) { const callbackRef = React.useRef(cb) // keep the callback ref upto date callbackRef.current = cb /** * initialize a persistent callback which never changes * through out the component lifecycle */ const persistentCbRef = React.useRef(function () { const args = []; let len = arguments.length while (len--) args[len] = arguments[len] return callbackRef.current.apply(callbackRef, args) }) return persistentCbRef.current } // spilt a float number into different parts beforeDecimal, afterDecimal, and negation function splitDecimal(numStr: string | string[], allowNegative: boolean | undefined) { if (allowNegative === void 0) allowNegative = true const hasNegation = numStr[0] === '-' const addNegation = hasNegation && allowNegative numStr = numStr.replace('-', '') const parts = numStr.split('.') const beforeDecimal = parts[0] const afterDecimal = parts[1] || '' return { beforeDecimal, afterDecimal, hasNegation, addNegation } } function fixLeadingZero(numStr: string | string[]) { if (!numStr) { return numStr } const isNegative = numStr[0] === '-' if (isNegative) { numStr = numStr.substring(1, numStr.length) } const parts = numStr.split('.') const beforeDecimal = parts[0].replace(/^0+/, '') || '0' const afterDecimal = parts[1] || '' return ('' + (isNegative ? '-' : '') + beforeDecimal + (afterDecimal ? ('.' + afterDecimal) : '')) } /** * limit decimal numbers to given scale * Not used .fixedTo because that will break with big numbers */ function limitToScale(numStr: string | any[], scale: number, fixedDecimalScale: boolean) { let str = '' const filler = fixedDecimalScale ? '0' : '' for (let i = 0; i <= scale - 1; i++) { str += numStr[i] || filler } return str } function repeat(str: string | undefined, count: number) { return Array(count + 1).join(str) } function toNumericString(num: string | number) { let _num = num + '' // typecast number to string // store the sign and remove it from the number. const sign = _num[0] === '-' ? '-' : '' if (sign) { _num = _num.substring(1) } // split the number into cofficient and exponent const ref = _num.split(/[eE]/g) let coefficient = ref[0] let exponent = ref[1] // covert exponent to number; exponent = Number(exponent) // if there is no exponent part or its 0, return the coffiecient with sign if (!exponent) { return sign + coefficient } coefficient = coefficient.replace('.', '') /** * for scientific notation the current decimal index will be after first number (index 0) * So effective decimal index will always be 1 + exponent value */ const decimalIndex = 1 + exponent const coffiecientLn = coefficient.length if (decimalIndex < 0) { // if decimal index is less then 0 add preceding 0s // add 1 as join will have coefficient = '0.' + repeat('0', Math.abs(decimalIndex)) + coefficient } else if (decimalIndex >= coffiecientLn) { // if decimal index is less then 0 add leading 0s coefficient = coefficient + repeat('0', decimalIndex - coffiecientLn) } else { // else add decimal point at proper index coefficient = (coefficient.substring(0, decimalIndex) || '0') + '.' + coefficient.substring(decimalIndex) } return sign + coefficient } /** * This method is required to round prop value to given scale. * Not used .round or .fixedTo because that will break with big numbers */ function roundToPrecision(numStr: string | string[], scale: number | undefined, fixedDecimalScale: boolean) { // if number is empty don't do anything return empty string if (['', '-'].includes(numStr)) { return numStr } const shouldHaveDecimalSeparator = (numStr.includes('.') || fixedDecimalScale) && scale const ref = splitDecimal(numStr) const beforeDecimal = ref.beforeDecimal const afterDecimal = ref.afterDecimal const hasNegation = ref.hasNegation const floatValue = parseFloat(('0.' + (afterDecimal || '0'))) const floatValueStr = afterDecimal.length <= scale ? ('0.' + afterDecimal) : floatValue.toFixed(scale) const roundedDecimalParts = floatValueStr.split('.') const intPart = beforeDecimal .split('') .reverse() .reduce(function (roundedStr: string | any[], current: any, idx: number) { if (roundedStr.length > idx) { return ((Number(roundedStr[0]) + Number(current)).toString() + roundedStr.substring(1, roundedStr.length)) } return current + roundedStr }, roundedDecimalParts[0]) const decimalPart = limitToScale(roundedDecimalParts[1] || '', scale, fixedDecimalScale) const negation = hasNegation ? '-' : '' const decimalSeparator = shouldHaveDecimalSeparator ? '.' : '' return ('' + negation + intPart + decimalSeparator + decimalPart) } /** set the caret positon in an input field **/ function setCaretPosition(el: { value: any, createTextRange: () => any, selectionStart: number, focus: () => void, setSelectionRange: (arg0: any, arg1: any) => void } | null, caretPos: number) { el.value = el.value // ^ this is used to not only get 'focus', but // to make sure we don't have it everything -selected- // (it causes an issue in chrome, and having it doesn't hurt any other browser) if (el !== null) { if (el.createTextRange) { const range = el.createTextRange() range.move('character', caretPos) range.select() return true } // (el.selectionStart === 0 added for Firefox bug) if (el.selectionStart || el.selectionStart === 0) { el.focus() el.setSelectionRange(caretPos, caretPos) return true } // fail city, fortunately this never happens (as far as I've tested) :) el.focus() return false } } const findChangeRange = memoizeOnce(function (prevValue: string | any[], newValue: string | any[]) { let i = 0; let j = 0 const prevLength = prevValue.length const newLength = newValue.length while (prevValue[i] === newValue[i] && i < prevLength) { i++ } // check what has been changed from last while (prevValue[prevLength - 1 - j] === newValue[newLength - 1 - j] && newLength - j > i && prevLength - j > i) { j++ } return { from: { start: i, end: prevLength - j }, to: { start: i, end: newLength - j } } }) /* Returns a number whose value is limited to the given range */ function clamp(num: number, min: number, max: number) { return Math.min(Math.max(num, min), max) } function geInputCaretPosition(el: never) { /* Max of selectionStart and selectionEnd is taken for the patch of pixel and other mobile device caret bug */ return Math.max(el.selectionStart, el.selectionEnd) } function addInputMode() { return (typeof navigator !== 'undefined' && !(navigator.platform && /iPhone|iPod/.test(navigator.platform))) } function getDefaultChangeMeta(value: string | any[]) { return { from: { start: 0, end: 0 }, to: { start: 0, end: value.length }, lastValue: '' } } function getMaskAtIndex(mask: string | undefined, index: number) { if (mask === void 0) mask = ' ' if (typeof mask === 'string') { return mask } return mask[index] || ' ' } function defaultIsCharacterSame(ref: { currentValue: any, formattedValue: any, currentValueIndex: any, formattedValueIndex: any }) { const currentValue = ref.currentValue const formattedValue = ref.formattedValue const currentValueIndex = ref.currentValueIndex const formattedValueIndex = ref.formattedValueIndex return currentValue[currentValueIndex] === formattedValue[formattedValueIndex] } function getCaretPosition(newFormattedValue: string | any[], lastFormattedValue: any, curValue: string, curCaretPos: number, boundary: any[], isValidInputCharacter: (arg0: any) => any, /** * format function can change the character, the caret engine relies on mapping old value and new value * In such case if character is changed, parent can tell which chars are equivalent * Some example, all allowedDecimalCharacters are updated to decimalCharacters, 2nd case if user is coverting * number to different numeric system. */ isCharacterSame: ((arg0: { currentValue: any, lastValue: any, formattedValue: any, currentValueIndex: number, formattedValueIndex: number }) => any) | undefined) { if (isCharacterSame === void 0) isCharacterSame = defaultIsCharacterSame /** * if something got inserted on empty value, add the formatted character before the current value, * This is to avoid the case where typed character is present on format characters */ const firstAllowedPosition = boundary.findIndex(function (b: any) { return b }) const prefixFormat = newFormattedValue.slice(0, firstAllowedPosition) if (!lastFormattedValue && !curValue.startsWith(prefixFormat)) { lastFormattedValue = prefixFormat curValue = prefixFormat + curValue curCaretPos = curCaretPos + prefixFormat.length } const curValLn = curValue.length const formattedValueLn = newFormattedValue.length // create index map const addedIndexMap = {} const indexMap = new Array(curValLn) for (let i = 0; i < curValLn; i++) { indexMap[i] = -1 for (let j = 0, jLn = formattedValueLn; j < jLn; j++) { const isCharSame = isCharacterSame({ currentValue: curValue, lastValue: lastFormattedValue, formattedValue: newFormattedValue, currentValueIndex: i, formattedValueIndex: j }) if (isCharSame && addedIndexMap[j] !== true) { indexMap[i] = j addedIndexMap[j] = true break } } } /** * For current caret position find closest characters (left and right side) * which are properly mapped to formatted value. * The idea is that the new caret position will exist always in the boundary of * that mapped index */ let pos = curCaretPos while (pos < curValLn && (indexMap[pos] === -1 || !isValidInputCharacter(curValue[pos]))) { pos++ } // if the caret position is on last keep the endIndex as last for formatted value const endIndex = pos === curValLn || indexMap[pos] === -1 ? formattedValueLn : indexMap[pos] pos = curCaretPos - 1 while (pos > 0 && indexMap[pos] === -1) { pos-- } const startIndex = pos === -1 || indexMap[pos] === -1 ? 0 : indexMap[pos] + 1 /** * case where a char is added on suffix and removed from middle, example 2sq345 becoming $2,345 sq * there is still a mapping but the order of start index and end index is changed */ if (startIndex > endIndex) { return endIndex } /** * given the current caret position if it closer to startIndex * keep the new caret position on start index or keep it closer to endIndex */ return curCaretPos - startIndex < endIndex - curCaretPos ? startIndex : endIndex } /* This keeps the caret within typing area so people can't type in between prefix or suffix or format characters */ function getCaretPosInBoundary(value: string | any[], caretPos: number, boundary: boolean[], direction: string | undefined) { const valLn = value.length // clamp caret position to [0, value.length] caretPos = clamp(caretPos, 0, valLn) if (direction === 'left') { while (caretPos >= 0 && !boundary[caretPos]) { caretPos-- } // if we don't find any suitable caret position on left, set it on first allowed position if (caretPos === -1) { caretPos = boundary.indexOf(true) } } else { while (caretPos <= valLn && !boundary[caretPos]) { caretPos++ } // if we don't find any suitable caret position on right, set it on last allowed position if (caretPos > valLn) { caretPos = boundary.lastIndexOf(true) } } // if we still don't find caret position, set it at the end of value if (caretPos === -1) { caretPos = valLn } return caretPos } function caretUnknownFormatBoundary(formattedValue: string | any[]) { const boundaryAry = Array.from({ length: formattedValue.length + 1 }).map(function () { return true }) for (let i = 0, ln = boundaryAry.length; i < ln; i++) { // consider caret to be in boundary if it is before or after numeric value boundaryAry[i] = Boolean(charIsNumber(formattedValue[i]) || charIsNumber(formattedValue[i - 1])) } return boundaryAry } function useInternalValues(value: any, defaultValue: any, valueIsNumericString: boolean, format: { (numStr: any): any, (arg0: any): any }, removeFormatting: { (inputValue: any, changeMeta: any): any, (arg0: any, arg1: undefined): any }, onValueChange: ((arg0: any, arg1: any) => void) | undefined) { if (onValueChange === void 0) onValueChange = noop const getValues = usePersistentCallback(function (value: any, valueIsNumericString: any) { let formattedValue, numAsString if (isNotValidValue(value)) { numAsString = '' formattedValue = '' } else if (typeof value === 'number' || valueIsNumericString) { numAsString = typeof value === 'number' ? toNumericString(value) : value formattedValue = format(numAsString) } else { numAsString = removeFormatting(value, undefined) formattedValue = format(numAsString) } return { formattedValue, numAsString } }) const ref = React.useState(function () { return getValues(isNil(value) ? defaultValue : value, valueIsNumericString) }) const values = ref[0] const setValues = ref[1] const _onValueChange = function (newValues: { formattedValue: any, value: any }, sourceInfo: any) { if (newValues.formattedValue !== values.formattedValue) { setValues({ formattedValue: newValues.formattedValue, numAsString: newValues.value }) } // call parent on value change if only if formatted value is changed onValueChange(newValues, sourceInfo) } // if value is switch from controlled to uncontrolled, use the internal state's value to format with new props let _value = value let _valueIsNumericString = valueIsNumericString if (isNil(value)) { _value = values.numAsString _valueIsNumericString = true } const newValues = getValues(_value, _valueIsNumericString) React.useMemo(function () { setValues(newValues) }, [newValues.formattedValue]) return [values, _onValueChange] } function defaultRemoveFormatting(value: string) { if (value === void 0) value = '' if (value === null) { return '' } if (value === undefined) return '' return value?.replace(/[^0-9]/g, '') } function defaultFormat(value: any) { return value } const currencyFormat = (numStr: string): number | bigint | string => { if (numStr === '') return '' return new Intl.NumberFormat('es-CO', { style: 'currency', currency: 'COP', currencyDisplay: 'symbol', // Puedes cambiar esto a 'code' o 'name' segĂșn prefieras maximumFractionDigits: 0 }).format(isNaN(Number(numStr)) ? 0 : Number(numStr)) } function NumberFormatBase(props: { label: string, onChangeDefault: (args: any) => void, name: any, type: any, displayType: any, customInput: any, renderText: any, getInputRef: any, format: any, removeFormatting: any, defaultValue: any, valueIsNumericString: any, onValueChange: any, isAllowed: any, onChange: any, onKeyDown: any, onMouseUp: any, onFocus: any, onBlur: any, value: any, getCaretBoundary: any, isValidInputCharacter: any, isCharacterSame: any }) { let type = props.type; if (type === void 0) type = 'text' let displayType = props.displayType; if (displayType === void 0) displayType = 'input' const customInput = props.customInput const renderText = props.renderText const label = props.label const getInputRef = props.getInputRef let format = props.format; if (format === void 0) format = defaultFormat let removeFormatting = props.removeFormatting; if (removeFormatting === void 0) removeFormatting = defaultRemoveFormatting const defaultValue = props.defaultValue const name = props.name const valueIsNumericString = props.valueIsNumericString const onValueChange = props.onValueChange const isAllowed = props.isAllowed let onChange = props.onChange; if (onChange === void 0) onChange = noop let onChangeDefault = props.onChangeDefault; if (onChangeDefault === void 0) onChangeDefault = noop let onKeyDown = props.onKeyDown; if (onKeyDown === void 0) onKeyDown = noop let onMouseUp = props.onMouseUp; if (onMouseUp === void 0) onMouseUp = noop let onFocus = props.onFocus; if (onFocus === void 0) onFocus = noop let onBlur = props.onBlur; if (onBlur === void 0) onBlur = noop const propValue = props.value let getCaretBoundary = props.getCaretBoundary; if (getCaretBoundary === void 0) getCaretBoundary = caretUnknownFormatBoundary let isValidInputCharacter = props.isValidInputCharacter; if (isValidInputCharacter === void 0) isValidInputCharacter = charIsNumber const isCharacterSame = props.isCharacterSame const otherProps = __rest(props, ['type', 'displayType', 'customInput', 'renderText', 'getInputRef', 'format', 'removeFormatting', 'defaultValue', 'valueIsNumericString', 'onValueChange', 'isAllowed', 'onChange', 'onKeyDown', 'onMouseUp', 'onFocus', 'onBlur', 'value', 'getCaretBoundary', 'isValidInputCharacter', 'isCharacterSame']) const ref = useInternalValues(propValue, defaultValue, Boolean(valueIsNumericString), format, removeFormatting, onValueChange) const ref_0 = ref[0] const formattedValue = ref_0.formattedValue const numAsString = ref_0.numAsString const onFormattedValueChange = ref[1] const lastUpdatedValue = React.useRef({ formattedValue, numAsString }) const _onValueChange = function (values: { formattedValue: any, value: any, floatValue?: number | undefined }, source: { event: any, source: any }) { lastUpdatedValue.current = { formattedValue: values.formattedValue, numAsString: values.value } onFormattedValueChange(values, source) } const ref$1 = React.useState(false) const mounted = ref$1[0] const setMounted = ref$1[1] const focusedElm = React.useRef(null) const timeout = React.useRef({ setCaretTimeout: null, focusTimeout: null }) React.useEffect(function () { setMounted(true) return function () { clearTimeout(timeout.current.setCaretTimeout) clearTimeout(timeout.current.focusTimeout) } }, []) const _format = format const getValueObject = function (formattedValue: any, numAsString: string) { const floatValue = parseFloat(numAsString) return { formattedValue, value: numAsString, floatValue: isNaN(floatValue) ? undefined : floatValue } } const setPatchedCaretPosition = function (el: never, caretPos: any, currentValue: any) { // don't reset the caret position when the whole input content is selected if (el.selectionStart === 0 && el.selectionEnd === el.value.length) { return } /* setting caret position within timeout of 0ms is required for mobile chrome, otherwise browser resets the caret position after we set it We are also setting it without timeout so that in normal browser we don't see the flickering */ setCaretPosition(el, caretPos) timeout.current.setCaretTimeout = setTimeout(function () { if (el.value === currentValue && el.selectionStart !== el.selectionEnd) { setCaretPosition(el, caretPos) } }, 0) } /* This keeps the caret within typing area so people can't type in between prefix or suffix */ const correctCaretPosition = function (value: any, caretPos: any, direction: string | undefined) { return getCaretPosInBoundary(value, caretPos, getCaretBoundary(value), direction) } const getNewCaretPosition = function (inputValue: any, newFormattedValue: any, caretPos: number | undefined) { const caretBoundary = getCaretBoundary(newFormattedValue) let updatedCaretPos = getCaretPosition(newFormattedValue, formattedValue, inputValue, caretPos, caretBoundary, isValidInputCharacter, isCharacterSame) // correct caret position if its outside of editable area updatedCaretPos = getCaretPosInBoundary(newFormattedValue, updatedCaretPos, caretBoundary) return updatedCaretPos } const updateValueAndCaretPosition = function (params: { formattedValue: any, numAsString: any, inputValue: any, event: any, source: any, setCaretPosition: any, input: any, caretPos?: any }) { let newFormattedValue = params.formattedValue; if (newFormattedValue === void 0) newFormattedValue = '' const input = params.input let setCaretPosition = params.setCaretPosition; if (setCaretPosition === void 0) setCaretPosition = true const source = params.source const event = params.event const numAsString = params.numAsString let caretPos = params.caretPos if (input) { // calculate caret position if not defined if (caretPos === undefined && setCaretPosition) { const inputValue = params.inputValue || input.value const currentCaretPosition = geInputCaretPosition(input) /** * set the value imperatively, this is required for IE fix * This is also required as if new caret position is beyond the previous value. * Caret position will not be set correctly */ input.value = newFormattedValue // get the caret position caretPos = getNewCaretPosition(inputValue, newFormattedValue, currentCaretPosition) } /** * set the value imperatively, as we set the caret position as well imperatively. * This is to keep value and caret position in sync */ input.value = newFormattedValue // set caret position, and value imperatively when element is provided if (setCaretPosition && caretPos !== undefined) { // set caret position setPatchedCaretPosition(input, caretPos, newFormattedValue) } } if (newFormattedValue !== formattedValue) { // trigger onValueChange synchronously, so parent is updated along with the number format. Fix for #277, #287 _onValueChange(getValueObject(newFormattedValue, numAsString), { event, source }) } } /** * if the formatted value is not synced to parent, or if the formatted value is different from last synced value sync it * we also don't need to sync to the parent if no formatting is applied * if the formatting props is removed, in which case last formatted value will be different from the numeric string value * in such case we need to inform the parent. */ React.useEffect(function () { const ref = lastUpdatedValue.current const lastFormattedValue = ref.formattedValue const lastNumAsString = ref.numAsString if (formattedValue !== lastFormattedValue && (formattedValue !== numAsString || lastFormattedValue !== lastNumAsString)) { _onValueChange(getValueObject(formattedValue, numAsString), { event: undefined, source: SourceType.props }) } }, [formattedValue, numAsString]) // also if formatted value is changed from the props, we need to update the caret position // keep the last caret position if element is focused const currentCaretPosition = focusedElm.current ? geInputCaretPosition(focusedElm.current) : undefined // needed to prevent warning with useLayoutEffect on server const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect useIsomorphicLayoutEffect(function () { const input = focusedElm.current if (formattedValue !== lastUpdatedValue.current.formattedValue && input) { const caretPos = getNewCaretPosition(lastUpdatedValue.current.formattedValue, formattedValue, currentCaretPosition) /** * set the value imperatively, as we set the caret position as well imperatively. * This is to keep value and caret position in sync */ input.value = formattedValue setPatchedCaretPosition(input, caretPos, formattedValue) } }, [formattedValue]) const formatInputValue = function (inputValue: any, event: { target: any }, source: any) { const changeRange = findChangeRange(formattedValue, inputValue) const changeMeta = Object.assign(Object.assign({}, changeRange), { lastValue: formattedValue }) let _numAsString = removeFormatting(inputValue, changeMeta) const _formattedValue = _format(_numAsString) // formatting can remove some of the number chars, so we need to fine number string again _numAsString = removeFormatting(_formattedValue, undefined) if (isAllowed && !isAllowed(getValueObject(_formattedValue, _numAsString))) { // reset the caret position const input = event.target const currentCaretPosition = geInputCaretPosition(input) const caretPos = getNewCaretPosition(inputValue, formattedValue, currentCaretPosition) input.value = formattedValue setPatchedCaretPosition(input, caretPos, formattedValue) return false } updateValueAndCaretPosition({ formattedValue: _formattedValue, numAsString: _numAsString, inputValue, event, source, setCaretPosition: true, input: event.target }) return true } const _onChange = function (e: { target: any }) { const el = e.target const inputValue = el.value const changed = formatInputValue(inputValue, e, SourceType.event) onChangeDefault({ event: e, floatValue: Number(defaultRemoveFormatting(inputValue as string)), formattedValue, inputValue, inputElement: el }) if (changed) { onChange(e) } } const _onKeyDown = function (e: { target: any, key: any, preventDefault: () => void, isUnitTestRun: any }) { const el = e.target const key = e.key const selectionStart = el.selectionStart const selectionEnd = el.selectionEnd let value = el.value; if (value === void 0) value = '' let expectedCaretPosition // Handle backspace and delete against non numerical/decimal characters or arrow keys if (key === 'ArrowLeft' || key === 'Backspace') { expectedCaretPosition = Math.max(selectionStart - 1, 0) } else if (key === 'ArrowRight') { expectedCaretPosition = Math.min(selectionStart + 1, value.length) } else if (key === 'Delete') { expectedCaretPosition = selectionStart } // if expectedCaretPosition is not set it means we don't want to Handle keyDown // also if multiple characters are selected don't handle if (expectedCaretPosition === undefined || selectionStart !== selectionEnd) { onKeyDown(e) return } let newCaretPosition = expectedCaretPosition if (key === 'ArrowLeft' || key === 'ArrowRight') { const direction = key === 'ArrowLeft' ? 'left' : 'right' newCaretPosition = correctCaretPosition(value, expectedCaretPosition, direction) // arrow left or right only moves the caret, so no need to handle the event, if we are handling it manually if (newCaretPosition !== expectedCaretPosition) { e.preventDefault() } } else if (key === 'Delete' && !isValidInputCharacter(value[expectedCaretPosition])) { // in case of delete go to closest caret boundary on the right side newCaretPosition = correctCaretPosition(value, expectedCaretPosition, 'right') } else if (key === 'Backspace' && !isValidInputCharacter(value[expectedCaretPosition])) { // in case of backspace go to closest caret boundary on the left side newCaretPosition = correctCaretPosition(value, expectedCaretPosition, 'left') } if (newCaretPosition !== expectedCaretPosition) { setPatchedCaretPosition(el, newCaretPosition, value) } /* NOTE: this is just required for unit test as we need to get the newCaretPosition, Remove this when you find different solution */ /* @ts-expect-error */ if (e.isUnitTestRun) { setPatchedCaretPosition(el, newCaretPosition, value) } onKeyDown(e) } /** required to handle the caret position when click anywhere within the input **/ const _onMouseUp = function (e: { target: any }) { const el = e.target /** * NOTE: we have to give default value for value as in case when custom input is provided * value can come as undefined when nothing is provided on value prop. */ const selectionStart = el.selectionStart const selectionEnd = el.selectionEnd let value = el.value; if (value === void 0) value = '' if (selectionStart === selectionEnd) { const caretPosition = correctCaretPosition(value, selectionStart) if (caretPosition !== selectionStart) { setPatchedCaretPosition(el, caretPosition, value) } } onMouseUp(e) } const _onFocus = function (e: { persist: () => void, target: any }) { // Workaround Chrome and Safari bug https://bugs.chromium.org/p/chromium/issues/detail?id=779328 // (onFocus event target selectionStart is always 0 before setTimeout) if (e.persist) { e.persist() } const el = e.target focusedElm.current = el timeout.current.focusTimeout = setTimeout(function () { const selectionStart = el.selectionStart const selectionEnd = el.selectionEnd let value = el.value; if (value === void 0) value = '' const caretPosition = correctCaretPosition(value, selectionStart) // setPatchedCaretPosition only when everything is not selected on focus (while tabbing into the field) if (caretPosition !== selectionStart && !(selectionStart === 0 && selectionEnd === value.length)) { setPatchedCaretPosition(el, caretPosition, value) } onFocus(e) }, 0) } const _onBlur = function (e: any) { focusedElm.current = null clearTimeout(timeout.current.focusTimeout) clearTimeout(timeout.current.setCaretTimeout) onBlur(e) } // add input mode on element based on format prop and device once the component is mounted const inputMode = mounted && addInputMode() ? 'numeric' : undefined const inputProps = Object.assign({ inputMode }, otherProps, { type, name, value: formattedValue ?? currencyFormat(props.defaultValue), defaultValue: props.defaultValue, onChange: _onChange, onKeyDown: _onKeyDown, onMouseUp: _onMouseUp, onFocus: _onFocus, onBlur: _onBlur, className: styles.input }) if (displayType === 'text') { return renderText ? (React__default.createElement(React__default.Fragment, null, renderText(formattedValue, otherProps) || null)) : (React__default.createElement('span', Object.assign({}, otherProps, { ref: getInputRef }), formattedValue)) } else if (customInput) { const CustomInput = customInput /* @ts-expect-error */ return React__default.createElement(CustomInput, Object.assign({}, inputProps, { ref: getInputRef })) } return (
{label !== '' && } {React__default.createElement('input', Object.assign({}, inputProps, { ref: getInputRef }))}
) } function format(numStr: string | string[], props: { decimalScale: any, fixedDecimalScale: any, prefix: any, suffix: any, allowNegative: any, thousandsGroupStyle: any }) { const decimalScale = props.decimalScale const fixedDecimalScale = props.fixedDecimalScale let prefix = props.prefix; if (prefix === void 0) prefix = '' let suffix = props.suffix; if (suffix === void 0) suffix = '' const allowNegative = props.allowNegative let thousandsGroupStyle = props.thousandsGroupStyle; if (thousandsGroupStyle === void 0) thousandsGroupStyle = 'thousand' // don't apply formatting on empty string or '-' if (numStr === '' || numStr === '-') { return numStr } const ref = getSeparators(props) const thousandSeparator = ref.thousandSeparator const decimalSeparator = ref.decimalSeparator /** * Keep the decimal separator * when decimalScale is not defined or non zero and the numStr has decimal in it * Or if decimalScale is > 0 and fixeDecimalScale is true (even if numStr has no decimal) */ const hasDecimalSeparator = (decimalScale !== 0 && numStr.includes('.')) || (decimalScale && fixedDecimalScale) const ref$1 = splitDecimal(numStr, allowNegative) let beforeDecimal = ref$1.beforeDecimal let afterDecimal = ref$1.afterDecimal let addNegation = ref$1.addNegation // eslint-disable-line prefer-const // apply decimal precision if its defined if (decimalScale !== undefined) { afterDecimal = limitToScale(afterDecimal, decimalScale, !!fixedDecimalScale) } if (thousandSeparator) { beforeDecimal = applyThousandSeparator(beforeDecimal, thousandSeparator, thousandsGroupStyle) } // add prefix and suffix when there is a number present if (prefix) { beforeDecimal = prefix + beforeDecimal } if (suffix) { afterDecimal = afterDecimal + suffix } // restore negation sign if (addNegation) { beforeDecimal = '-' + beforeDecimal } numStr = beforeDecimal + ((hasDecimalSeparator && decimalSeparator) || '') + afterDecimal return numStr } function getSeparators(props: { decimalSeparator: any, thousandSeparator: any, allowedDecimalSeparators: any }) { let decimalSeparator = props.decimalSeparator; if (decimalSeparator === void 0) decimalSeparator = '.' let thousandSeparator = props.thousandSeparator let allowedDecimalSeparators = props.allowedDecimalSeparators if (thousandSeparator === true) { thousandSeparator = ',' } if (!allowedDecimalSeparators) { allowedDecimalSeparators = [decimalSeparator, '.'] } return { decimalSeparator, thousandSeparator, allowedDecimalSeparators } } function handleNegation(value: string | undefined, allowNegative: any) { if (value === void 0) value = '' const negationRegex = new RegExp('(-)') const doubleNegationRegex = new RegExp('(-)(.)*(-)') // Check number has '-' value const hasNegation = negationRegex.test(value) // Check number has 2 or more '-' values const removeNegation = doubleNegationRegex.test(value) // remove negation value = value.replace(/-/g, '') if (hasNegation && !removeNegation && allowNegative) { value = '-' + value } return value } function getNumberRegex(decimalSeparator: any, global: boolean) { return new RegExp(('(^-)|[0-9]|' + (escapeRegExp(decimalSeparator))), global ? 'g' : undefined) } function isNumericString(val: string, prefix: string | null | undefined, suffix: string | null | undefined) { // for empty value we can always treat it as numeric string if (val === '') { return true } return (!(prefix === null || prefix === void 0 ? void 0 : prefix.match(/\d/)) && !(suffix === null || suffix === void 0 ? void 0 : suffix.match(/\d/)) && typeof val === 'string' && !isNaN(Number(val))) } function removeFormatting(value: string, changeMeta: { from: any, to: any, lastValue: any } | undefined, props: { allowNegative: any, prefix: any, suffix: any, decimalScale: any }) { let assign if (changeMeta === void 0) changeMeta = getDefaultChangeMeta(value) const allowNegative = props.allowNegative let prefix = props.prefix; if (prefix === void 0) prefix = '' let suffix = props.suffix; if (suffix === void 0) suffix = '' const decimalScale = props.decimalScale const from = changeMeta.from const to = changeMeta.to let start = to.start let end = to.end const ref = getSeparators(props) const allowedDecimalSeparators = ref.allowedDecimalSeparators const decimalSeparator = ref.decimalSeparator const isBeforeDecimalSeparator = value[end] === decimalSeparator /** * If only a number is added on empty input which matches with the prefix or suffix, * then don't remove it, just return the same */ if (charIsNumber(value) && (value === prefix || value === suffix) && changeMeta.lastValue === '') { return value } /** Check for any allowed decimal separator is added in the numeric format and replace it with decimal separator */ if (end - start === 1 && allowedDecimalSeparators.indexOf(value[start]) !== -1) { const separator = decimalScale === 0 ? '' : decimalSeparator value = value.substring(0, start) + separator + value.substring(start + 1, value.length) } const stripNegation = function (value: string | string[], start: number, end: number) { /** * if prefix starts with - we don't allow negative number to avoid confusion * if suffix starts with - and the value length is same as suffix length, then the - sign is from the suffix * In other cases, if the value starts with - then it is a negation */ let hasNegation = false let hasDoubleNegation = false if (prefix.startsWith('-')) { hasNegation = false } else if (value.startsWith('--')) { hasNegation = false hasDoubleNegation = true } else if (suffix.startsWith('-') && value.length === suffix.length) { hasNegation = false } else if (value[0] === '-') { hasNegation = true } let charsToRemove = hasNegation ? 1 : 0 if (hasDoubleNegation) { charsToRemove = 2 } // remove negation/double negation from start to simplify prefix logic as negation comes before prefix if (charsToRemove) { value = value.substring(charsToRemove) // account for the removal of the negation for start and end index start -= charsToRemove end -= charsToRemove } return { value, start, end, hasNegation } } const toMetadata = stripNegation(value, start, end) const hasNegation = toMetadata.hasNegation; ((assign = toMetadata, value = assign.value, start = assign.start, end = assign.end)) const ref$1 = stripNegation(changeMeta.lastValue, from.start, from.end) const fromStart = ref$1.start const fromEnd = ref$1.end const lastValue = ref$1.value // if only prefix and suffix part is updated reset the value to last value // if the changed range is from suffix in the updated value, and the the suffix starts with the same characters, allow the change const updatedSuffixPart = value.substring(start, end) if (value.length && lastValue.length && (fromStart > lastValue.length - suffix.length || fromEnd < prefix.length) && !(updatedSuffixPart && suffix.startsWith(updatedSuffixPart))) { value = lastValue } /** * remove prefix * Remove whole prefix part if its present on the value * If the prefix is partially deleted (in which case change start index will be less the prefix length) * Remove only partial part of prefix. */ let startIndex = 0 if (value.startsWith(prefix)) { startIndex += prefix.length } else if (start < prefix.length) { startIndex = start } value = value.substring(startIndex) // account for deleted prefix for end end -= startIndex /** * Remove suffix * Remove whole suffix part if its present on the value * If the suffix is partially deleted (in which case change end index will be greater than the suffixStartIndex) * remove the partial part of suffix */ let endIndex = value.length const suffixStartIndex = value.length - suffix.length if (value.endsWith(suffix)) { endIndex = suffixStartIndex } // if the suffix is removed from the end else if (end > suffixStartIndex) { endIndex = end } // if the suffix is removed from start else if (end > value.length - suffix.length) { endIndex = end } value = value.substring(0, endIndex) // add the negation back and handle for double negation value = handleNegation(hasNegation ? ('-' + value) : value, allowNegative) // remove non numeric characters value = (value.match(getNumberRegex(decimalSeparator, true)) || []).join('') // replace the decimalSeparator with ., and only keep the first separator, ignore following ones const firstIndex = value.indexOf(decimalSeparator) value = value.replace(new RegExp(escapeRegExp(decimalSeparator), 'g'), function (match: any, index: any) { return index === firstIndex ? '.' : '' }) // check if beforeDecimal got deleted and there is nothing after decimal, // clear all numbers in such case while keeping the - sign const ref$2 = splitDecimal(value, allowNegative) const beforeDecimal = ref$2.beforeDecimal const afterDecimal = ref$2.afterDecimal let addNegation = ref$2.addNegation // eslint-disable-line prefer-const // clear only if something got deleted before decimal (cursor is before decimal) if (to.end - to.start < from.end - from.start && beforeDecimal === '' && isBeforeDecimalSeparator && !parseFloat(afterDecimal)) { value = addNegation ? '-' : '' } return value } function getCaretBoundary(formattedValue: string | any[], props: { prefix: any, suffix: any }) { let prefix = props.prefix; if (prefix === void 0) prefix = '' let suffix = props.suffix; if (suffix === void 0) suffix = '' const boundaryAry = Array.from({ length: formattedValue.length + 1 }).map(function () { return true }) const hasNegation = formattedValue[0] === '-' // fill for prefix and negation boundaryAry.fill(false, 0, prefix.length + (hasNegation ? 1 : 0)) // fill for suffix const valLn = formattedValue.length boundaryAry.fill(false, valLn - suffix.length + 1, valLn + 1) return boundaryAry } function validateAndUpdateProps(props: { prefix: any, allowNegative: any }) { const ref = getSeparators(props) const thousandSeparator = ref.thousandSeparator const decimalSeparator = ref.decimalSeparator // eslint-disable-next-line prefer-const let prefix = props.prefix; if (prefix === void 0) prefix = '' let allowNegative = props.allowNegative; if (allowNegative === void 0) allowNegative = true if (thousandSeparator === decimalSeparator) { throw new Error(("\n Decimal separator can't be same as thousand separator.\n thousandSeparator: " + thousandSeparator + ' (thousandSeparator = {true} is same as thousandSeparator = ",")\n decimalSeparator: ' + decimalSeparator + ' (default value for decimalSeparator is .)\n ')) } if (prefix.startsWith('-') && allowNegative) { // TODO: throw error in next major version console.error(("\n Prefix can't start with '-' when allowNegative is true.\n prefix: " + prefix + '\n allowNegative: ' + allowNegative + '\n ')) allowNegative = false } return Object.assign(Object.assign({}, props), { allowNegative }) } function useNumericFormat(props: { decimalSeparator: any, allowedDecimalSeparators: any, thousandsGroupStyle: any, suffix: any, allowNegative: any, allowLeadingZeros: any, onKeyDown: any, onBlur: any, thousandSeparator: any, decimalScale: any, fixedDecimalScale: any, prefix: any, defaultValue: any, value: any, valueIsNumericString: any, onValueChange: any }) { // validate props props = validateAndUpdateProps(props) const _decimalSeparator = props.decimalSeparator const _allowedDecimalSeparators = props.allowedDecimalSeparators const thousandsGroupStyle = props.thousandsGroupStyle const suffix = props.suffix const allowNegative = props.allowNegative const allowLeadingZeros = props.allowLeadingZeros let onKeyDown = props.onKeyDown; if (onKeyDown === void 0) onKeyDown = noop let onBlur = props.onBlur; if (onBlur === void 0) onBlur = noop const thousandSeparator = props.thousandSeparator const decimalScale = props.decimalScale const fixedDecimalScale = props.fixedDecimalScale let prefix = props.prefix; if (prefix === void 0) prefix = '' const defaultValue = props.defaultValue const value = props.value const valueIsNumericString = props.valueIsNumericString const onValueChange = props.onValueChange const restProps = __rest(props, ['decimalSeparator', 'allowedDecimalSeparators', 'thousandsGroupStyle', 'suffix', 'allowNegative', 'allowLeadingZeros', 'onKeyDown', 'onBlur', 'thousandSeparator', 'decimalScale', 'fixedDecimalScale', 'prefix', 'defaultValue', 'value', 'valueIsNumericString', 'onValueChange']) // get derived decimalSeparator and allowedDecimalSeparators const ref = getSeparators(props) const decimalSeparator = ref.decimalSeparator const allowedDecimalSeparators = ref.allowedDecimalSeparators const _format = function (numStr: any) { return format(numStr, props) } const _removeFormatting = function (inputValue: any, changeMeta: any) { return removeFormatting(inputValue, changeMeta, props) } const _value = isNil(value) ? defaultValue : value // try to figure out isValueNumericString based on format prop and value let _valueIsNumericString = valueIsNumericString !== null && valueIsNumericString !== void 0 ? valueIsNumericString : isNumericString(_value, prefix, suffix) if (!isNil(value)) { _valueIsNumericString = _valueIsNumericString || typeof value === 'number' } else if (!isNil(defaultValue)) { _valueIsNumericString = _valueIsNumericString || typeof defaultValue === 'number' } const roundIncomingValueToPrecision = function (value: string) { if (isNotValidValue(value)) { return value } if (typeof value === 'number') { value = toNumericString(value) } /** * only round numeric or float string values coming through props, * we don't need to do it for onChange events, as we want to prevent typing there */ if (_valueIsNumericString && typeof decimalScale === 'number') { return roundToPrecision(value, decimalScale, Boolean(fixedDecimalScale)) } return value } const ref$1 = useInternalValues(roundIncomingValueToPrecision(value), roundIncomingValueToPrecision(defaultValue), Boolean(_valueIsNumericString), _format, _removeFormatting, onValueChange) const ref$1_0 = ref$1[0] const numAsString = ref$1_0.numAsString const formattedValue = ref$1_0.formattedValue const _onValueChange = ref$1[1] const _onKeyDown = function (e: { target: any, key: any, preventDefault: () => void }) { const el = e.target const key = e.key const selectionStart = el.selectionStart const selectionEnd = el.selectionEnd let value = el.value; if (value === void 0) value = '' // if multiple characters are selected and user hits backspace, no need to handle anything manually if (selectionStart !== selectionEnd) { onKeyDown(e) return } // if user hits backspace, while the cursor is before prefix, and the input has negation, remove the negation if (key === 'Backspace' && value[0] === '-' && selectionStart === prefix.length + 1 && allowNegative) { // bring the cursor to after negation setCaretPosition(el, 1) } // don't allow user to delete decimal separator when decimalScale and fixedDecimalScale is set if (decimalScale && fixedDecimalScale) { if (key === 'Backspace' && value[selectionStart - 1] === decimalSeparator) { setCaretPosition(el, selectionStart - 1) e.preventDefault() } else if (key === 'Delete' && value[selectionStart] === decimalSeparator) { e.preventDefault() } } // if user presses the allowed decimal separator before the separator, move the cursor after the separator if ((allowedDecimalSeparators === null || allowedDecimalSeparators === void 0 ? void 0 : allowedDecimalSeparators.includes(key)) && value[selectionStart] === decimalSeparator) { setCaretPosition(el, selectionStart + 1) } const _thousandSeparator = thousandSeparator === true ? ',' : thousandSeparator // move cursor when delete or backspace is pressed before/after thousand separator if (key === 'Backspace' && value[selectionStart - 1] === _thousandSeparator) { setCaretPosition(el, selectionStart - 1) } if (key === 'Delete' && value[selectionStart] === _thousandSeparator) { setCaretPosition(el, selectionStart + 1) } onKeyDown(e) } const _onBlur = function (e: any) { let _value = numAsString // if there no no numeric value, clear the input if (!_value.match(/\d/g)) { _value = '' } // clear leading 0s if (!allowLeadingZeros) { _value = fixLeadingZero(_value) } // apply fixedDecimalScale on blur event if (fixedDecimalScale && decimalScale) { _value = roundToPrecision(_value, decimalScale, fixedDecimalScale) } if (_value !== numAsString) { const formattedValue = format(_value, props) _onValueChange({ formattedValue, value: _value, floatValue: parseFloat(_value) }, { event: e, source: SourceType.event }) } onBlur(e) } const isValidInputCharacter = function (inputChar: any) { if (inputChar === decimalSeparator) { return true } return charIsNumber(inputChar) } const isCharacterSame = function (ref: { currentValue: any, lastValue: any, formattedValue: any, currentValueIndex: any, formattedValueIndex: any }) { const currentValue = ref.currentValue const lastValue = ref.lastValue const formattedValue = ref.formattedValue const currentValueIndex = ref.currentValueIndex const formattedValueIndex = ref.formattedValueIndex const curChar = currentValue[currentValueIndex] const newChar = formattedValue[formattedValueIndex] /** * NOTE: as thousand separator and allowedDecimalSeparators can be same, we need to check on * typed range if we have typed any character from allowedDecimalSeparators, in that case we * consider different characters like , and . same within the range of updated value. */ const typedRange = findChangeRange(lastValue, currentValue) const to = typedRange.to if (currentValueIndex >= to.start && currentValueIndex < to.end && allowedDecimalSeparators?.includes(curChar) && newChar === decimalSeparator) { return true } return curChar === newChar } return Object.assign(Object.assign({}, restProps), { value: formattedValue, valueIsNumericString: false, isValidInputCharacter, isCharacterSame, onValueChange: _onValueChange, format: _format, removeFormatting: _removeFormatting, getCaretBoundary: function (formattedValue: any) { return getCaretBoundary(formattedValue, props) }, onKeyDown: _onKeyDown, onBlur: _onBlur }) } function NumericFormat(props: any) { const numericFormatProps = useNumericFormat(props) return React__default.createElement(NumberFormatBase, Object.assign({}, numericFormatProps)) } function format$1(numStr: string | any[], props: { format: any, allowEmptyFormatting: any, mask: any, patternChar: any }) { const format = props.format const allowEmptyFormatting = props.allowEmptyFormatting const mask = props.mask let patternChar = props.patternChar; if (patternChar === void 0) patternChar = '#' if (numStr === '' && !allowEmptyFormatting) { return '' } let hashCount = 0 const formattedNumberAry = format.split('') for (let i = 0, ln = format.length; i < ln; i++) { if (format[i] === patternChar) { formattedNumberAry[i] = numStr[hashCount] || getMaskAtIndex(mask, hashCount) hashCount += 1 } } return formattedNumberAry.join('') } function removeFormatting$1(value: string | any[], changeMeta: { from: any, to: any, lastValue: any } | undefined, props: { format: any, patternChar: any }) { if (changeMeta === void 0) changeMeta = getDefaultChangeMeta(value) const format = props.format let patternChar = props.patternChar; if (patternChar === void 0) patternChar = '#' const from = changeMeta.from const to = changeMeta.to let lastValue = changeMeta.lastValue; if (lastValue === void 0) lastValue = '' const isNumericSlot = function (caretPos: number) { return format[caretPos] === patternChar } const removeFormatChar = function (string: string | any[], startIndex: number) { let str = '' for (let i = 0; i < string.length; i++) { if (isNumericSlot(startIndex + i) && charIsNumber(string[i])) { str += string[i] } } return str } const extractNumbers = function (str: string) { return str.replace(/[^0-9]/g, '') } // if format doesn't have any number, remove all the non numeric characters if (!format.match(/\d/)) { return extractNumbers(value) } /** * if user paste the whole formatted text in an empty input, check if matches to the pattern * and remove the format characters, if there is a mismatch on the pattern, do plane number extract */ if (lastValue === '' && value.length === format.length) { let str = '' for (let i = 0; i < value.length; i++) { if (isNumericSlot(i)) { if (charIsNumber(value[i])) { str += value[i] } } else if (value[i] !== format[i]) { // if there is a mismatch on the pattern, do plane number extract return extractNumbers(value) } } return str } const firstSection = lastValue.substring(0, from.start) const middleSection = value.substring(to.start, to.end) const lastSection = lastValue.substring(from.end) return ('' + (removeFormatChar(firstSection, 0)) + (extractNumbers(middleSection)) + (removeFormatChar(lastSection, from.end))) } function getCaretBoundary$1(formattedValue: string | any[], props: { format: any, mask: any, patternChar: any }) { const format = props.format const mask = props.mask let patternChar = props.patternChar; if (patternChar === void 0) patternChar = '#' const boundaryAry = Array.from({ length: formattedValue.length + 1 }).map(function () { return true }) let hashCount = 0 let firstEmptySlot = -1 const maskAndIndexMap = {} format.split('').forEach(function (char: any, index: number) { let maskAtIndex if (char === patternChar) { hashCount++ maskAtIndex = getMaskAtIndex(mask, hashCount - 1) if (firstEmptySlot === -1 && formattedValue[index] === maskAtIndex) { firstEmptySlot = index } } maskAndIndexMap[index] = maskAtIndex }) const isPosAllowed = function (pos: number) { // the position is allowed if the position is not masked and valid number area return format[pos] === patternChar && formattedValue[pos] !== maskAndIndexMap[pos] } for (let i = 0, ln = boundaryAry.length; i < ln; i++) { // consider caret to be in boundary if it is before or after numeric value // Note: on pattern based format its denoted by patternCharacter // we should also allow user to put cursor on first empty slot boundaryAry[i] = i === firstEmptySlot || isPosAllowed(i) || isPosAllowed(i - 1) } // the first patternChar position is always allowed boundaryAry[format.indexOf(patternChar)] = true return boundaryAry } function validateProps(props: { mask: any }) { const mask = props.mask if (mask) { const maskAsStr = mask === 'string' ? mask : mask.toString() if (maskAsStr.match(/\d/g)) { throw new Error(('Mask ' + mask + ' should not contain numeric character;')) } } } function isNumericString$1(val: string, format: string | null | undefined) { // we can treat empty string as numeric string if (val === '') { return true } return !(format === null || format === void 0 ? void 0 : format.match(/\d/)) && typeof val === 'string' && (!!val.match(/^\d+$/) || val === '') } function usePatternFormat(props: { mask: any, allowEmptyFormatting: any, format: any, inputMode: any, onKeyDown: any, patternChar: any, value: any, defaultValue: any, valueIsNumericString: any }) { const mask = props.mask const allowEmptyFormatting = props.allowEmptyFormatting const formatProp = props.format let inputMode = props.inputMode; if (inputMode === void 0) inputMode = 'numeric' let onKeyDown = props.onKeyDown; if (onKeyDown === void 0) onKeyDown = noop let patternChar = props.patternChar; if (patternChar === void 0) patternChar = '#' const value = props.value const defaultValue = props.defaultValue const valueIsNumericString = props.valueIsNumericString const restProps = __rest(props, ['mask', 'allowEmptyFormatting', 'format', 'inputMode', 'onKeyDown', 'patternChar', 'value', 'defaultValue', 'valueIsNumericString']) // validate props validateProps(props) const _getCaretBoundary = function (formattedValue: any) { return getCaretBoundary$1(formattedValue, props) } const _onKeyDown = function (e: { key: any, target: any }) { const key = e.key const el = e.target const selectionStart = el.selectionStart const selectionEnd = el.selectionEnd const value = el.value // if multiple characters are selected and user hits backspace, no need to handle anything manually if (selectionStart !== selectionEnd) { onKeyDown(e) return } // bring the cursor to closest numeric section let caretPos = selectionStart // if backspace is pressed after the format characters, bring it to numeric section // if delete is pressed before the format characters, bring it to numeric section if (key === 'Backspace' || key === 'Delete') { let direction = 'right' if (key === 'Backspace') { while (caretPos > 0 && formatProp[caretPos - 1] !== patternChar) { caretPos-- } direction = 'left' } else { const formatLn = formatProp.length while (caretPos < formatLn && formatProp[caretPos] !== patternChar) { caretPos++ } direction = 'right' } caretPos = getCaretPosInBoundary(value, caretPos, _getCaretBoundary(value), direction) } else if (formatProp[caretPos] !== patternChar && key !== 'ArrowLeft' && key !== 'ArrowRight') { // if user is typing on format character position, bring user to next allowed caret position caretPos = getCaretPosInBoundary(value, caretPos + 1, _getCaretBoundary(value), 'right') } // if we changing caret position, set the caret position if (caretPos !== selectionStart) { setCaretPosition(el, caretPos) } onKeyDown(e) } // try to figure out isValueNumericString based on format prop and value const _value = isNil(value) ? defaultValue : value const isValueNumericString = valueIsNumericString !== null && valueIsNumericString !== void 0 ? valueIsNumericString : isNumericString$1(_value, formatProp) const _props = Object.assign(Object.assign({}, props), { valueIsNumericString: isValueNumericString }) return Object.assign(Object.assign({}, restProps), { value, defaultValue, valueIsNumericString: isValueNumericString, inputMode, format: function (numStr: any) { return format$1(numStr, _props) }, removeFormatting: function (inputValue: any, changeMeta: any) { return removeFormatting$1(inputValue, changeMeta, _props) }, getCaretBoundary: _getCaretBoundary, onKeyDown: _onKeyDown }) } function PatternFormat(props: any) { const patternFormatProps = usePatternFormat(props) return React__default.createElement(NumberFormatBase, Object.assign({}, patternFormatProps)) } export { NumericFormat, PatternFormat, NumberFormatBase, currencyFormat }