UNPKG

react-native-web-mask

Version:

A cross-platform input mask hook for React and React Native, with TypeScript and helper utilities

326 lines (320 loc) 10.7 kB
'use strict'; var react = require('react'); /** * Strips all non-digit characters from a string. */ function stripNonDigits(value) { return value.replace(/\D/g, ""); } /** * Ensures the string doesn't exceed a certain length. */ function limitLength(value, maxLength) { return value.slice(0, maxLength); } /** * Inserts a separator (e.g. '/') between chunks of specified sizes. * * Example: * insertChunks("12345", [2, 2, 1], "/") * -> "12/34/5" */ function insertChunks(value, chunkSizes, separator) { let result = ""; let startIndex = 0; chunkSizes.forEach((size) => { const chunk = value.slice(startIndex, startIndex + size); if (chunk) { if (result) { result += separator; } result += chunk; } startIndex += size; }); if (startIndex < value.length) { result += value.slice(startIndex); } return result; } /** * A flexible RegExp replace helper. * Pass in a pattern (RegExp) and a replacer string or function. * * Example: * applyRegexReplace("12345", /\d/g, "*") * -> "*****" */ function applyRegexReplace(value, pattern, replaceWith) { return value.replace(pattern, replaceWith); } /** * Clamps a numeric string (after parsing to a number) within min and max range. * Useful if you want to ensure months/days remain in valid ranges, etc. * * Example: * clampDigits("13", 1, 12) -> "12" */ function clampDigits(numericString, min, max) { if (!numericString) return numericString; const numValue = parseInt(numericString, 10); if (isNaN(numValue)) return numericString; if (numValue < min) return String(min).padStart(numericString.length, "0"); if (numValue > max) return String(max); return numericString; } /** * Takes a string that is expected to contain a currency value and extracts * the decimal equivalent from it. Any non-numeric characters are removed and the * result is parsed as a float. If the result is NaN, returns 0. * * @param {string} currency - The currency string to parse * @returns {number} The parsed number, or 0 if parsing fails */ function parseCurrencyToNumber(currency) { if (!currency) return 0; // Remove non-numeric characters except '.' and '-' const numericString = currency.replace(/[^0-9.-]/g, "").trim(); // Parse it into a float and return return parseFloat(numericString) || 0; } /** * Format a value as a phone number in the format (###) ###-#### * @param {string} value the value to format * @returns {string} the formatted value * @example * maskPhone("1234567890") -> "(123) 456-7890" */ const maskPhone = (value) => { const digits = value.replace(/\D/g, ""); const match = digits.match(/^(\d{0,3})(\d{0,3})(\d{0,4})$/); if (!match) return digits; let masked = ""; if (match[1]) masked = `(${match[1]}`; if (match[2]) masked += `) ${match[2]}`; if (match[3]) masked += `-${match[3]}`; return masked; }; /** * Format a value as a decimal number in the format 0.00 * @param {string} value the value to format * @returns {string} the formatted value * @example * maskMoney("123456") -> "1,234.56" */ const maskMoney = (value) => { const digitsOnly = value.replace(/\D/g, ""); if (!digitsOnly) { return "0.00"; } // Convert the digits to a dollar format const dollarValue = parseFloat(digitsOnly) / 100; const dollarString = dollarValue.toFixed(2); // Add commas for thousands let [integerPart, decimalPart] = dollarString.split("."); integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ","); return `${integerPart}.${decimalPart}`; }; /** * Format a value as a credit card number in the format XXXX XXXX XXXX XXXX * @param {string} value the value to format * @returns {string} the formatted value * @example * maskCard("1234567890123456") -> "1234 5678 9012 3456" */ const maskCard = (value) => { const digits = value.replace(/\D/g, ""); return digits.replace(/(\d{4})(?=\d)/g, "$1 "); }; /** * Format a value as a zip code in the format 12345-6789 * @param {string} value the value to format * @returns {string} the formatted value * @example * maskZip("123456789") -> "12345-6789" */ const maskZip = (value) => { const digits = value.replace(/\D/g, ""); if (digits.length > 5) { return digits.slice(0, 5) + "-" + digits.slice(5, 9); } return digits; }; /** * Format a value as a date in the format MM/DD/YYYY * @param {string} value the value to format * @returns {string} the formatted value * @example * maskDate("12345678") -> "01/23/4567" */ const maskDate = (value) => { const digits = value.replace(/\D/g, "").slice(0, 8); const match = digits.match(/^(\d{0,2})(\d{0,2})(\d{0,4})$/); if (!match) return digits; const [, mm, dd, yyyy] = match; let masked = ""; if (mm) masked = mm; if (dd) masked += (masked ? "/" : "") + dd; if (yyyy) masked += (masked ? "/" : "") + yyyy; return masked; }; /** * Format a value as a month/day in the format MM/DD * @param {string} value the value to format * @returns {string} the formatted value * @example * maskMonthDay("1231") -> "12/31" */ const maskMonthDay = (value) => { const digits = value.replace(/\D/g, "").slice(0, 4); const match = digits.match(/^(\d{0,2})(\d{0,2})$/); if (!match) return digits; const [, mm, dd] = match; let masked = ""; if (mm) masked = mm; if (dd) masked += (masked ? "/" : "") + dd; return masked; }; const defaultMask = (value) => value; /** * Given a mask type and a raw value, clamps the raw value to a suitable * length for the given mask type. This is useful for limiting the length * of input values when the user is typing, so that the formatting doesn't * get out of hand. * * For example, for a phone number, the raw value will be clamped to 10 * digits. For a date, it will be clamped to 8 digits (MMDDYYYY). * * Note that this function does not remove any characters from the raw * value - it simply truncates it to the desired length. This means that * if the user enters a non-digit character, it will still be included * in the final output. * * @param {MaskType} type the mask type to use for clamping * @param {string} raw the raw value to clamp * @returns {string} the clamped raw value * @example * clampRawValueByMaskType("phone", "1234567890333") -> "1234567890" */ function clampRawValueByMaskType(type, raw) { switch (type) { case "phone": // phone numbers typically max out at 10 digits return raw.replace(/\D/g, "").slice(0, 10); case "date": // date => "MMDDYYYY" => 8 digits return raw.replace(/\D/g, "").slice(0, 8); case "monthDay": // monthDay => "MMDD" => 4 digits return raw.replace(/\D/g, "").slice(0, 4); case "zip": // US ZIP => up to 9 digits (ZIP+4) return raw.replace(/\D/g, "").slice(0, 9); case "card": // typical credit card => up to 16 digits (or 19 if you want) return raw.replace(/\D/g, "").slice(0, 16); // money => no strict digit cap in many cases // custom => we won't force a clamp (the customMask can do it if desired) default: return raw; } } function useInputMask(props) { const { maskType, initialValue = "", customMask, onChange } = props || {}; // Returns the mask function based on the mask type. const getMaskFunction = react.useCallback((type) => { switch (type) { case "phone": return maskPhone; case "card": return maskCard; case "zip": return maskZip; case "date": return maskDate; case "monthDay": return maskMonthDay; case "money": return maskMoney; case "custom": return customMask || defaultMask; default: return defaultMask; } }, [customMask]); // Computes the clamped raw value and the corresponding masked value. // In the "money" case, we parse the masked value back into a fixed-decimal string. const computeValues = react.useCallback((value) => { if (!maskType) { return { raw: value, masked: value }; } const clamped = clampRawValueByMaskType(maskType, value); const maskFn = getMaskFunction(maskType); const masked = maskFn(clamped); const raw = maskType === "money" ? parseCurrencyToNumber(masked).toFixed(2) : clamped; return { raw, masked }; }, [maskType, getMaskFunction]); // Initialize state using the computed masked value. const [rawValue, setRawValue] = react.useState(initialValue); const [maskedValue, setMaskedValue] = react.useState(() => { return computeValues(initialValue).masked; }); // Handles the value change from either a text event or a string value. const handleValueChange = react.useCallback((input) => { const value = typeof input === "object" && "target" in input ? input.target.value : input; const { raw, masked } = computeValues(value); setRawValue(raw); setMaskedValue(masked); onChange === null || onChange === void 0 ? void 0 : onChange(raw); }, [computeValues, onChange]); // Allows programmatic updates of the input value. const setValue = react.useCallback((newRaw) => { const { raw, masked } = computeValues(newRaw); setRawValue(raw); setMaskedValue(masked); onChange === null || onChange === void 0 ? void 0 : onChange(raw); }, [computeValues, onChange]); return { rawValue, maskedValue, onChange: handleValueChange, onChangeText: handleValueChange, setValue, }; } exports.applyRegexReplace = applyRegexReplace; exports.clampDigits = clampDigits; exports.clampRawValueByMaskType = clampRawValueByMaskType; exports.defaultMask = defaultMask; exports.insertChunks = insertChunks; exports.limitLength = limitLength; exports.maskCard = maskCard; exports.maskDate = maskDate; exports.maskMoney = maskMoney; exports.maskMonthDay = maskMonthDay; exports.maskPhone = maskPhone; exports.maskZip = maskZip; exports.parseCurrencyToNumber = parseCurrencyToNumber; exports.stripNonDigits = stripNonDigits; exports.useInputMask = useInputMask; //# sourceMappingURL=index.js.map