UNPKG

antd-phone-input

Version:

Advanced, highly customizable phone input component for Ant Design.

184 lines (183 loc) 13.2 kB
"use client"; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) 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.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import useFormInstance from "antd/es/form/hooks/useFormInstance"; import { ConfigContext } from "antd/es/config-provider"; import { FormContext } from "antd/es/form/context"; import { useWatch } from "antd/es/form/Form"; import version from "antd/es/version"; import Select from "antd/es/select"; import Space from "antd/es/space"; import Input from "antd/es/input"; import { checkValidity, cleanInput, displayFormat, getCountry, getDefaultISO2Code, getFormattedNumber, getMetadata, getRawValue, parsePhoneNumber, useMask, usePhone, } from "react-phone-hooks"; import locale from "./locale"; import { injectMergedStyles } from "./styles"; const [major, minor, _] = version.split(".").map(Number); const isV5x = major === 5; const isV6x = major === 6; const isLatest = isV6x || (isV5x && minor >= 25); const PhoneInput = forwardRef((_a, forwardedRef) => { var { value: initialValue = "", country = getDefaultISO2Code(), useSVG = false, distinct = false, disabled = false, enableArrow = false, enableSearch = false, disableDropdown = false, disableParentheses = false, onlyCountries = [], excludeCountries = [], preferredCountries = [], searchNotFound: defaultSearchNotFound = "No country found", searchPlaceholder: defaultSearchPlaceholder = "Search country", dropdownRender = (node) => node, onMount: handleMount = () => null, onInput: handleInput = () => null, onChange: handleChange = () => null, onKeyDown: handleKeyDown = () => null } = _a, antInputProps = __rest(_a, ["value", "country", "useSVG", "distinct", "disabled", "enableArrow", "enableSearch", "disableDropdown", "disableParentheses", "onlyCountries", "excludeCountries", "preferredCountries", "searchNotFound", "searchPlaceholder", "dropdownRender", "onMount", "onInput", "onChange", "onKeyDown"]); const formInstance = useFormInstance(); const { locale = {}, getPrefixCls } = useContext(ConfigContext); const formContext = useContext(FormContext); const inputRef = useRef(null); const searchRef = useRef(null); const selectedRef = useRef(false); const initiatedRef = useRef(false); const lastInternalValueRef = useRef(null); const [query, setQuery] = useState(""); const [minWidth, setMinWidth] = useState(0); const [countryCode, setCountryCode] = useState(country); const { locale: localeIdentifier, searchNotFound = defaultSearchNotFound, searchPlaceholder = defaultSearchPlaceholder, countries = new Proxy({}, ({ get: (_, prop) => prop })), } = locale.PhoneInput || {}; const prefixCls = getPrefixCls(); injectMergedStyles(prefixCls); const { value, pattern, metadata, setValue, countriesList, } = usePhone({ query, country, distinct, countryCode, initialValue, onlyCountries, excludeCountries, preferredCountries, disableParentheses, locale: localeIdentifier, }); const { onInput: onInputMaskHandler, onKeyDown: onKeyDownMaskHandler, } = useMask(pattern); const selectValue = useMemo(() => { var _a, _b; let metadata = getMetadata(getRawValue(value), countriesList); metadata = metadata || getCountry(countryCode); return ((_a = (Object.assign({}, metadata))) === null || _a === void 0 ? void 0 : _a[0]) + ((_b = (Object.assign({}, metadata))) === null || _b === void 0 ? void 0 : _b[2]); }, [countriesList, countryCode, value]); const namePath = useMemo(() => { let path = []; let formName = (formContext === null || formContext === void 0 ? void 0 : formContext.name) || ""; let fieldName = (antInputProps === null || antInputProps === void 0 ? void 0 : antInputProps.id) || ""; if (formName) { path.push(formName); fieldName = fieldName.slice(formName.length + 1); } return path.concat(fieldName.split("_")); }, [antInputProps, formContext]); const phoneValue = useWatch(namePath, formInstance); const setFieldValue = useCallback((value, forcePattern) => { setValue(getFormattedNumber(getRawValue(value), forcePattern || pattern)); if (formInstance) formInstance.setFieldValue(namePath, value); lastInternalValueRef.current = value; }, [formInstance, namePath, pattern, setValue]); const onKeyDown = useCallback((event) => { onKeyDownMaskHandler(event); handleKeyDown(event); }, [handleKeyDown, onKeyDownMaskHandler]); const onChange = useCallback((event) => { const formattedNumber = selectedRef.current ? event.target.value : getFormattedNumber(event.target.value, pattern); selectedRef.current = false; const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList); setCountryCode(phoneMetadata.isoCode); setValue(formattedNumber); setQuery(""); handleChange(Object.assign(Object.assign({}, phoneMetadata), { valid: (strict) => checkValidity(phoneMetadata, strict) }), event); }, [countriesList, handleChange, pattern, setValue]); const onInput = useCallback((event) => { onInputMaskHandler(event); handleInput(event); }, [onInputMaskHandler, handleInput]); const onMount = useCallback((value) => { setFieldValue(value); handleMount(value); }, [handleMount, setFieldValue]); const onDropdownVisibleChange = useCallback((open) => { if (open && enableSearch) setTimeout(() => searchRef.current.focus(), 100); }, [enableSearch]); const ref = useCallback((node) => { [forwardedRef, inputRef].forEach((ref) => { if (typeof ref === "function") ref(node); else if (ref != null) ref.current = node; }); }, [forwardedRef]); useEffect(() => { if (formInstance) { const currentFormValue = formInstance.getFieldValue(namePath); const currentFormRawValue = getRawValue(currentFormValue); const currentLocalRawValue = getRawValue(value); if (lastInternalValueRef.current) { if (JSON.stringify(currentFormValue) === JSON.stringify(lastInternalValueRef.current)) { const phoneValueRaw = getRawValue(phoneValue); if (phoneValueRaw === currentFormRawValue || phoneValueRaw === currentLocalRawValue) { lastInternalValueRef.current = null; } return; } } if (currentFormRawValue === currentLocalRawValue) return; } const rawValue = getRawValue(phoneValue); const metadata = getMetadata(rawValue); // Skip if value has not been updated by `setFieldValue`. if (!(metadata === null || metadata === void 0 ? void 0 : metadata[3]) || rawValue === getRawValue(value)) return; let pattern = (metadata === null || metadata === void 0 ? void 0 : metadata[3]) || ""; if (disableParentheses) pattern = pattern.replace(/[()]/g, ""); const formattedNumber = getFormattedNumber(rawValue, pattern); const phoneMetadata = parsePhoneNumber(formattedNumber); setFieldValue(Object.assign(Object.assign({}, phoneMetadata), { valid: (strict) => checkValidity(phoneMetadata, strict) }), pattern); setCountryCode(metadata === null || metadata === void 0 ? void 0 : metadata[0]); }, [phoneValue, value, disableParentheses, setFieldValue, formInstance, namePath]); useEffect(() => { if (initiatedRef.current) return; initiatedRef.current = true; let initialValue = getRawValue(value); if (!initialValue.startsWith(metadata === null || metadata === void 0 ? void 0 : metadata[2])) { initialValue = metadata === null || metadata === void 0 ? void 0 : metadata[2]; } const formattedNumber = getFormattedNumber(initialValue, pattern); const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList); onMount(Object.assign(Object.assign({}, phoneMetadata), { valid: (strict) => checkValidity(phoneMetadata, strict) })); setCountryCode(phoneMetadata.isoCode); setValue(formattedNumber); }, [countriesList, metadata, onMount, pattern, setValue, value]); const suffixIcon = useMemo(() => { return enableArrow && (_jsx("span", { role: "img", className: "anticon", style: { paddingLeft: 8 }, children: _jsx("svg", { className: "icon", viewBox: "0 0 1024 1024", focusable: "false", fill: "currentColor", width: "16", height: "18", children: _jsx("path", { d: "M848 368a48 48 0 0 0-81.312-34.544l-0.016-0.016-254.784 254.784-251.488-251.488a48 48 0 1 0-71.04 64.464l-0.128 0.128 288 288 0.016-0.016a47.84 47.84 0 0 0 34.544 14.688h0.224a47.84 47.84 0 0 0 34.544-14.688l0.016 0.016 288-288-0.016-0.016c8.32-8.624 13.44-20.368 13.44-33.312z" }) }) })); }, [enableArrow]); const countriesSelect = useMemo(() => (_jsxs(Select, Object.assign({ suffixIcon: null, value: selectValue, disabled: disabled, open: disableDropdown ? false : undefined, onSelect: (selectedOption, { key }) => { const [_, mask] = key.split("_"); const selectedCountryCode = selectedOption.slice(0, 2); const formattedNumber = displayFormat(cleanInput(mask, mask).join("")); const phoneMetadata = parsePhoneNumber(formattedNumber, countriesList, selectedCountryCode); setFieldValue(Object.assign(Object.assign({}, phoneMetadata), { valid: (strict) => checkValidity(phoneMetadata, strict) })); setCountryCode(selectedCountryCode); setValue(formattedNumber); setQuery(""); selectedRef.current = true; const nativeInputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value").set; nativeInputValueSetter.call(inputRef.current.input, formattedNumber); inputRef.current.input.dispatchEvent(new Event("change", { bubbles: true })); inputRef.current.input.focus(); }, optionLabelProp: "label", style: { width: 42 + (enableArrow ? 24 : 0) } }, ((isV5x || isV6x) ? { onOpenChange: onDropdownVisibleChange } : { onDropdownVisibleChange }), (isLatest ? { styles: { popup: { root: { minWidth } } } } : { dropdownStyle: { minWidth } }), ({ [(isV5x || isV6x) ? "popupRender" : "dropdownRender"]: (menu) => (_jsxs("div", { className: `${prefixCls}-phone-input-search-wrapper`, children: [enableSearch && (_jsx(Input, { value: query, ref: searchRef, placeholder: searchPlaceholder, onInput: ({ target }) => setQuery(target.value) })), countriesList.length ? menu : (_jsx("div", { className: "ant-select-item-empty", children: searchNotFound }))] })) }), { children: [_jsx(Select.Option, { children: null, value: selectValue, style: { display: "none" }, label: _jsxs("div", { style: { display: "flex", alignItems: "center" }, children: [_jsx("div", { className: `flag ${countryCode} ${useSVG ? "svg" : ""}` }), suffixIcon] }) }, `${countryCode}_default`), countriesList.map(([iso, name, dial, pattern]) => { const mask = disableParentheses ? pattern.replace(/[()]/g, "") : pattern; return (_jsx(Select.Option, { value: iso + dial, label: _jsxs("div", { style: { display: "flex", alignItems: "center" }, children: [_jsx("div", { className: `flag ${iso} ${useSVG ? "svg" : ""}` }), suffixIcon] }), children: _jsxs("div", { className: `${prefixCls}-phone-input-select-item`, children: [_jsx("div", { className: `flag ${iso} ${useSVG ? "svg" : ""}` }), countries[name], "\u00A0", displayFormat(mask)] }) }, `${iso}_${mask}`)); })] }))), [selectValue, suffixIcon, countryCode, query, disabled, disableParentheses, disableDropdown, onDropdownVisibleChange, minWidth, searchNotFound, countries, countriesList, setFieldValue, setValue, prefixCls, enableSearch, enableArrow, searchPlaceholder, useSVG]); return (_jsx("div", { className: `${prefixCls}-phone-input-wrapper`, ref: node => setMinWidth((node === null || node === void 0 ? void 0 : node.offsetWidth) || 0), children: _jsxs(Space.Compact, { size: (antInputProps === null || antInputProps === void 0 ? void 0 : antInputProps.size) || "middle", children: [dropdownRender(countriesSelect), _jsx(Input, Object.assign({ ref: ref, inputMode: "tel", value: value, onInput: onInput, onChange: onChange, onKeyDown: onKeyDown, disabled: disabled }, antInputProps))] }) })); }); export default PhoneInput; export { locale };