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
JavaScript
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
;