UNPKG

v-phone-input

Version:

International phone field for Vuetify 3 and Vue 3.

1,682 lines (1,681 loc) 42.6 kB
import { h, inject, computed, unref, shallowRef, ref, watch, isRef, defineComponent, useSlots, useModel, toRef, openBlock, createElementBlock, mergeProps, createBlock, resolveDynamicComponent, normalizeProps, guardReactiveProps, createSlots, withCtx, renderSlot, createVNode, toDisplayString, createCommentVNode, createElementVNode, renderList, mergeModels, markRaw, provide } from "vue"; import { VTextField, VListItem, VAutocomplete, VSelect } from "vuetify/components"; import { getSupportedRegionCodes, getCountryCodeForRegionCode, getExample, parsePhoneNumber } from "awesome-phonenumber"; function getPhoneCountryIconAttrs(props) { return props.decorative ? {} : { role: "img", title: props.country.name, "aria-label": props.country.name }; } function makePhoneInputCountryDisplayProps() { return { /** * Country to display. */ country: { required: true, type: Object }, /** * Tells if the icon should be ignored by screen readers or if it must have * an accessible name (e.g. through `title` attribute). */ decorative: { type: Boolean } }; } const VPhoneCountryFlagSprite = /* @__PURE__ */ Object.assign( (props) => h( "span", { ...getPhoneCountryIconAttrs(props), class: ["v-phone-input__country__icon", "f32"] }, h("span", { class: ["flag", props.country.iso2.toLowerCase()] }) ), { props: makePhoneInputCountryDisplayProps() } ); const VPhoneCountryFlagSvg = /* @__PURE__ */ Object.assign( (props) => h("span", { ...getPhoneCountryIconAttrs(props), class: [ "v-phone-input__country__icon", "fi", `fi-${props.country.iso2.toLowerCase()}` ] }), { props: makePhoneInputCountryDisplayProps() } ); function formatPhoneObject(format, phone) { return phone.number?.[format ?? "input"] ?? phone.number?.input ?? ""; } const AD = "AND"; const AE = "ARE"; const AF = "AFG"; const AG = "ATG"; const AI = "AIA"; const AL = "ALB"; const AM = "ARM"; const AO = "AGO"; const AQ = "ATA"; const AR = "ARG"; const AS = "ASM"; const AT = "AUT"; const AU = "AUS"; const AW = "ABW"; const AX = "ALA"; const AZ = "AZE"; const BA = "BIH"; const BB = "BRB"; const BD = "BGD"; const BE = "BEL"; const BF = "BFA"; const BG = "BGR"; const BH = "BHR"; const BI = "BDI"; const BJ = "BEN"; const BL = "BLM"; const BM = "BMU"; const BN = "BRN"; const BO = "BOL"; const BQ = "BES"; const BR = "BRA"; const BS = "BHS"; const BT = "BTN"; const BV = "BVT"; const BW = "BWA"; const BY = "BLR"; const BZ = "BLZ"; const CA = "CAN"; const CC = "CCK"; const CD = "COD"; const CF = "CAF"; const CG = "COG"; const CH = "CHE"; const CI = "CIV"; const CK = "COK"; const CL = "CHL"; const CM = "CMR"; const CN = "CHN"; const CO = "COL"; const CR = "CRI"; const CU = "CUB"; const CV = "CPV"; const CW = "CUW"; const CX = "CXR"; const CY = "CYP"; const CZ = "CZE"; const DE = "DEU"; const DJ = "DJI"; const DK = "DNK"; const DM = "DMA"; const DO = "DOM"; const DZ = "DZA"; const EC = "ECU"; const EE = "EST"; const EG = "EGY"; const EH = "ESH"; const ER = "ERI"; const ES = "ESP"; const ET = "ETH"; const FI = "FIN"; const FJ = "FJI"; const FK = "FLK"; const FM = "FSM"; const FO = "FRO"; const FR = "FRA"; const GA = "GAB"; const GB = "GBR"; const GD = "GRD"; const GE = "GEO"; const GF = "GUF"; const GG = "GGY"; const GH = "GHA"; const GI = "GIB"; const GL = "GRL"; const GM = "GMB"; const GN = "GIN"; const GP = "GLP"; const GQ = "GNQ"; const GR = "GRC"; const GS = "SGS"; const GT = "GTM"; const GU = "GUM"; const GW = "GNB"; const GY = "GUY"; const HK = "HKG"; const HM = "HMD"; const HN = "HND"; const HR = "HRV"; const HT = "HTI"; const HU = "HUN"; const ID = "IDN"; const IE = "IRL"; const IL = "ISR"; const IM = "IMN"; const IN = "IND"; const IO = "IOT"; const IQ = "IRQ"; const IR = "IRN"; const IS = "ISL"; const IT = "ITA"; const JE = "JEY"; const JM = "JAM"; const JO = "JOR"; const JP = "JPN"; const KE = "KEN"; const KG = "KGZ"; const KH = "KHM"; const KI = "KIR"; const KM = "COM"; const KN = "KNA"; const KP = "PRK"; const KR = "KOR"; const KW = "KWT"; const KY = "CYM"; const KZ = "KAZ"; const LA = "LAO"; const LB = "LBN"; const LC = "LCA"; const LI = "LIE"; const LK = "LKA"; const LR = "LBR"; const LS = "LSO"; const LT = "LTU"; const LU = "LUX"; const LV = "LVA"; const LY = "LBY"; const MA = "MAR"; const MC = "MCO"; const MD = "MDA"; const ME = "MNE"; const MF = "MAF"; const MG = "MDG"; const MH = "MHL"; const MK = "MKD"; const ML = "MLI"; const MM = "MMR"; const MN = "MNG"; const MO = "MAC"; const MP = "MNP"; const MQ = "MTQ"; const MR = "MRT"; const MS = "MSR"; const MT = "MLT"; const MU = "MUS"; const MV = "MDV"; const MW = "MWI"; const MX = "MEX"; const MY = "MYS"; const MZ = "MOZ"; const NA = "NAM"; const NC = "NCL"; const NE = "NER"; const NF = "NFK"; const NG = "NGA"; const NI = "NIC"; const NL = "NLD"; const NO = "NOR"; const NP = "NPL"; const NR = "NRU"; const NU = "NIU"; const NZ = "NZL"; const OM = "OMN"; const PA = "PAN"; const PE = "PER"; const PF = "PYF"; const PG = "PNG"; const PH = "PHL"; const PK = "PAK"; const PL = "POL"; const PM = "SPM"; const PN = "PCN"; const PR = "PRI"; const PS = "PSE"; const PT = "PRT"; const PW = "PLW"; const PY = "PRY"; const QA = "QAT"; const RE = "REU"; const RO = "ROU"; const RS = "SRB"; const RU = "RUS"; const RW = "RWA"; const SA = "SAU"; const SB = "SLB"; const SC = "SYC"; const SD = "SDN"; const SE = "SWE"; const SG = "SGP"; const SH = "SHN"; const SI = "SVN"; const SJ = "SJM"; const SK = "SVK"; const SL = "SLE"; const SM = "SMR"; const SN = "SEN"; const SO = "SOM"; const SR = "SUR"; const SS = "SSD"; const ST = "STP"; const SV = "SLV"; const SX = "SXM"; const SY = "SYR"; const SZ = "SWZ"; const TC = "TCA"; const TD = "TCD"; const TF = "ATF"; const TG = "TGO"; const TH = "THA"; const TJ = "TJK"; const TK = "TKL"; const TL = "TLS"; const TM = "TKM"; const TN = "TUN"; const TO = "TON"; const TR = "TUR"; const TT = "TTO"; const TV = "TUV"; const TW = "TWN"; const TZ = "TZA"; const UA = "UKR"; const UG = "UGA"; const UM = "UMI"; const US = "USA"; const UY = "URY"; const UZ = "UZB"; const VA = "VAT"; const VC = "VCT"; const VE = "VEN"; const VG = "VGB"; const VI = "VIR"; const VN = "VNM"; const VU = "VUT"; const WF = "WLF"; const WS = "WSM"; const XK = "XKX"; const YE = "YEM"; const YT = "MYT"; const ZA = "ZAF"; const ZM = "ZMB"; const ZW = "ZWE"; const countriesJson = { AD, AE, AF, AG, AI, AL, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BL, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BY, BZ, CA, CC, CD, CF, CG, CH, CI, CK, CL, CM, CN, CO, CR, CU, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, ET, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IQ, IR, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KP, KR, KW, KY, KZ, LA, LB, LC, LI, LK, LR, LS, LT, LU, LV, LY, MA, MC, MD, ME, MF, MG, MH, MK, ML, MM, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NI, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PS, PT, PW, PY, QA, RE, RO, RS, RU, RW, SA, SB, SC, SD, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SO, SR, SS, ST, SV, SX, SY, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TR, TT, TV, TW, TZ, UA, UG, UM, US, UY, UZ, VA, VC, VE, VG, VI, VN, VU, WF, WS, XK, YE, YT, ZA, ZM, ZW }; const V_PHONE_INPUT_INJECTION_KEY = /* @__PURE__ */ Symbol( "VPhoneInput options injection key" ); function injectPhoneInputPluginOptions() { return inject( V_PHONE_INPUT_INJECTION_KEY, {} ); } const undefinedBoolean = /* @__PURE__ */ Symbol("VPhoneInput undefined boolean"); function usePhoneInputPluginOptions() { const injected = injectPhoneInputPluginOptions(); const getOption = (key, options, defaultValue) => { const aValue = unref(options[key]); if (aValue === void 0 || aValue === undefinedBoolean) { const bValue = unref(injected[key]); if (bValue === void 0 || bValue === undefinedBoolean) { return defaultValue; } return bValue; } return aValue; }; return { getOption, useOption: (key, options, defaultValue) => computed(() => getOption(key, options, unref(defaultValue))) }; } function normalizeCountryIso2(country) { return (country ?? "").toUpperCase(); } function normalizeCountry(country) { return normalizeCountryIso2( typeof country === "string" ? country : country?.iso2 ); } function normalizeCountriesSet(countries) { return new Set((unref(countries) ?? []).map(normalizeCountry)); } function getDefaultCountries(countryName) { return Object.keys(countriesJson).map((iso2) => ({ name: countryName(iso2), dialCode: `${getCountryCodeForRegionCode(iso2)}`, iso2 })).sort((a, b) => a.name.localeCompare(b.name)); } function usePhoneInputCountries(options) { const supportedCountries = getSupportedRegionCodes(); const { getOption, useOption } = usePhoneInputPluginOptions(); const countryLocaleOption = useOption("countryLocale", options, "en"); const countryNameOption = useOption( "countryName", options, (iso2) => countryDisplayNames.value.of(iso2) ?? iso2 ); const countriesOption = computed( () => getOption( "countries", options, getDefaultCountries(countryNameOption.value) ) ); const preferCountriesOption = computed( () => normalizeCountriesSet(getOption("preferCountries", options) ?? []) ); const includeCountriesOption = computed( () => normalizeCountriesSet(getOption("includeCountries", options) ?? []) ); const excludeCountriesOption = computed( () => normalizeCountriesSet(getOption("excludeCountries", options) ?? []) ); const defaultCountryOption = useOption("defaultCountry", options); const countryDisplayNames = computed( () => new Intl.DisplayNames([countryLocaleOption.value ?? "en"], { type: "region" }) ); const countriesByIso2 = computed(() => { const countries2 = countriesOption.value.reduce((map, country) => { const iso2 = normalizeCountry(country); if (supportedCountries.indexOf(iso2) !== -1 && (!includeCountriesOption.value.size || includeCountriesOption.value.has(iso2)) && (!excludeCountriesOption.value.size || !excludeCountriesOption.value.has(iso2))) { map.set(iso2, country); } return map; }, /* @__PURE__ */ new Map()); if (!countries2.size) { throw new Error( "[VPhoneInput] Countries list must contain at least one country." ); } return countries2; }); const findCountry = computed( () => (value) => value ? countriesByIso2.value.get(normalizeCountry(value)) ?? null : null ); const countriesPreference = computed(() => { const preferred = []; const unpreferred = new Map(countriesByIso2.value); preferCountriesOption.value.forEach((iso2) => { const unpreferredCountry = unpreferred.get(iso2); if (unpreferredCountry) { preferred.push(unpreferredCountry); unpreferred.delete(iso2); } }); return { preferred, unpreferred: [...unpreferred.values()] }; }); const preferredCountries = computed( () => countriesPreference.value.preferred ); const unpreferredCountries = computed( () => countriesPreference.value.unpreferred ); const countries = computed(() => [ ...preferredCountries.value, ...unpreferredCountries.value ]); const defaultCountry = computed( () => findCountry.value(defaultCountryOption.value) ); const fallbackCountry = computed( // biome-ignore lint/style/noNonNullAssertion: Countries is non-empty as this step. () => defaultCountry.value ?? countries.value[0] ); return { countries, preferredCountries, unpreferredCountries, defaultCountry, fallbackCountry, findCountry }; } function resolveMessage(message, context) { return typeof message === "function" ? message(context) : ( // biome-ignore lint/suspicious/noExplicitAny: TypeScript low support for conditional types. message ?? void 0 ); } function usePhoneInputMessages(options) { const { useOption } = usePhoneInputPluginOptions(); const exampleFormatOption = useOption( "exampleFormat", options, "national" ); const exampleOption = useOption( "example", options, () => formatPhoneObject( exampleFormatOption.value, getExample(country.value.iso2) ) ); const labelOption = useOption("label", options, "Phone"); const ariaLabelOption = useOption("ariaLabel", options); const countryLabelOption = useOption( "countryLabel", options, (ctx) => `Country for "${ctx.label}"` ); const countryAriaLabelOption = useOption("countryAriaLabel", options); const placeholderOption = useOption("placeholder", options); const invalidMessageOption = useOption( "invalidMessage", options, (ctx) => `The "${ctx.label}" field is not a valid phone number (example: ${ctx.example}).` ); const country = computed(() => unref(options.country)); const example = computed( () => resolveMessage(exampleOption.value, { country: country.value, example: void 0, label: void 0 }) ); const label = computed( () => resolveMessage(labelOption.value, { country: country.value, example: example.value, label: void 0 }) ); const ariaLabel = computed( () => resolveMessage(ariaLabelOption.value, { country: country.value, example: example.value, label: void 0 }) ); const resolveMessageRef = computed( () => (message) => resolveMessage(message, { country: country.value, example: example.value, label: label.value }) ); return { example, label, ariaLabel, countryLabel: computed( () => resolveMessageRef.value(countryLabelOption.value) ), countryAriaLabel: computed( () => resolveMessageRef.value(countryAriaLabelOption.value) ), placeholder: computed( () => resolveMessageRef.value(placeholderOption.value) ), invalidMessage: computed( () => resolveMessageRef.value(invalidMessageOption.value) ), resolveMessage: resolveMessageRef }; } function usePhoneInput(options) { const countries = usePhoneInputCountries(options); const { useOption } = usePhoneInputPluginOptions(); const guessCountryOption = useOption("guessCountry", options); const modelFormatOption = useOption("modelFormat", options, "e164"); const displayFormatOption = useOption( "displayFormat", options, "national" ); const exampleFormatOption = useOption( "exampleFormat", options, displayFormatOption ); const displayFormatDelayOption = useOption("displayFormatDelay", options); const displayFormatOnBlurOption = useOption( "displayFormatOnBlur", options, true ); const validateOption = useOption( "validate", options, (phone2, country2) => !phone2 || phone2.valid && phone2.regionCode === country2.iso2 ); const countryInputRef = options.countryInputRef ?? shallowRef(); const phoneInputRef = options.phoneInputRef ?? shallowRef(); const phone = ref(null); const country = options.country ?? ref(); const countryGuessing = ref(false); const countryObject = computed({ get: () => countries.findCountry.value(country.value) ?? countries.fallbackCountry.value, set: (value) => { country.value = value.iso2; } }); const phoneObject = computed(() => { const input = (phone.value ?? "").trim(); return input !== "" ? parsePhoneNumber((phone.value ?? "").trim(), { regionCode: countryObject.value.iso2 }) : null; }); const phoneValid = computed(() => { if (validateOption.value === null) { return null; } return validateOption.value(phoneObject.value, countryObject.value); }); const phoneFormatted = computed(() => { if (phoneObject.value === null) { return phone.value ?? ""; } return modelFormatOption.value !== null ? formatPhoneObject(modelFormatOption.value, phoneObject.value) || (phone.value ?? "") : phoneObject.value.number?.input ?? ""; }); const displayFormatOnBlurCanBound = computed( () => !!countryInputRef.value || !!phoneInputRef.value ); let delayedFormatPhoneInputValueTimeout; const formatPhoneInputValue = () => { clearTimeout(delayedFormatPhoneInputValueTimeout); if (phoneObject.value && phoneValid.value && displayFormatOption.value !== null) { phone.value = formatPhoneObject( displayFormatOption.value, phoneObject.value ); } }; const updateCountryFromPhone = () => { if (phoneObject.value?.regionCode) { const phoneCountry = countries.findCountry.value( phoneObject.value.regionCode ); if (phoneCountry) { countryObject.value = phoneCountry; return phoneCountry; } } return null; }; const onModelValueChange = (next) => { if (next !== (phoneObject.value?.number?.input ?? "") && next !== phoneFormatted.value) { phone.value = next ?? ""; formatPhoneInputValue(); } }; const onCountryChange = (next, prev) => { if (!next || !countries.findCountry.value(next)) { country.value = prev ?? countries.fallbackCountry.value.iso2; return; } if (phoneObject.value?.regionCode && phoneObject.value.regionCode !== next) { const phoneSignificant = phoneObject.value.number?.significant; if (phoneSignificant) { const nextPhoneObject = parsePhoneNumber( `+${getCountryCodeForRegionCode(next)}${phoneSignificant}` ); phone.value = formatPhoneObject( displayFormatOption.value, nextPhoneObject ); } } if (guessCountryOption.value && "memoized" in guessCountryOption.value) { if (isRef(guessCountryOption.value.memoized)) { guessCountryOption.value.memoized.value = next; } else { guessCountryOption.value.memoized = next; } } }; const onPhoneChange = (next) => { if (typeof next !== "string") { phone.value = ""; return; } updateCountryFromPhone(); }; const onPhoneObjectChange = () => { const displayFormatDelay = !displayFormatOnBlurOption.value || !displayFormatOnBlurCanBound.value ? displayFormatDelayOption.value ?? true : displayFormatDelayOption.value; if (displayFormatDelay === true || typeof displayFormatDelay === "number") { const displayFormatDelayOrDefault = displayFormatDelay === true ? 1e3 : displayFormatDelay; if (displayFormatDelayOrDefault > 0) { delayedFormatPhoneInputValueTimeout = setTimeout( formatPhoneInputValue, displayFormatDelayOrDefault ); } else { formatPhoneInputValue(); } } }; const onPhoneFormattedChange = () => { options.modelValue.value = phoneFormatted.value; }; const onInputBlur = () => { if (displayFormatOnBlurOption.value) { formatPhoneInputValue(); } }; const onInputRefChange = (next, prev) => { const unrefElement = (value) => value?.$el ?? value; unrefElement(prev)?.removeEventListener("blur", onInputBlur); unrefElement(next)?.addEventListener("blur", onInputBlur); }; const manualCountry = countries.findCountry.value(country.value); country.value = (manualCountry ?? countries.fallbackCountry.value).iso2; phone.value = options.modelValue.value ?? ""; const detectedCountry = updateCountryFromPhone(); if (phoneValid.value) { formatPhoneInputValue(); } if (!manualCountry && !detectedCountry) { const guessCountry = () => { if (countries.countries.value.length === 1) { return countries.countries.value[0]; } const guessCountryValue = guessCountryOption.value; if (guessCountryValue) { const memoizedGuessedCountry = unref(guessCountryValue.memoized); if (memoizedGuessedCountry) { return memoizedGuessedCountry; } return guessCountryValue(); } return null; }; countryGuessing.value = true; const useGuessedCountry = (guessedCountry2) => { countryObject.value = countries.findCountry.value(guessedCountry2) ?? countries.fallbackCountry.value; countryGuessing.value = false; }; const guessedCountry = guessCountry(); if (guessedCountry && typeof guessedCountry !== "string") { (async () => { useGuessedCountry(await guessedCountry); })(); } else { useGuessedCountry(guessedCountry); } } watch(displayFormatOption, formatPhoneInputValue); watch(options.modelValue, onModelValueChange); watch(phone, onPhoneChange); watch(country, onCountryChange); watch(phoneObject, onPhoneObjectChange); watch(phoneFormatted, onPhoneFormattedChange); watch(countryInputRef, onInputRefChange, { immediate: true }); watch(phoneInputRef, onInputRefChange, { immediate: true }); return { ...countries, ...usePhoneInputMessages({ ...options, country: countryObject, exampleFormat: exampleFormatOption }), countryInputRef, phoneInputRef, phone, countryObject, phoneObject, phoneValid, countryGuessing, formatPhoneInputValue }; } function omitBy(object, predicate) { return Object.fromEntries( Object.keys(object).filter( (key) => key in object && predicate(key) ).map((key) => [key, object[key]]) ); } function pick(object, keys) { return Object.fromEntries( keys.filter((key) => key in object).map((key) => [key, object[key]]) ); } const vPhoneInputSharedProperties = [ // TODO Check other properties which should be included (bgColor, etc.) "variant", "flat", "tile", "density", "singleLine", "hideDetails", "reverse", "color", "bgColor", "theme", "disabled", "readonly", "rounded" ]; function makePhoneInputCountryProps() { return { /** * Locale to use when localizing country names. * * @defaultValue * `'en'` */ countryLocale: { type: String }, /** * Resolve a country name from its ISO-2 code. * * @defaultValue * Uses `Intl.DisplayNames` to localize the country name in defined `countryLocale`. */ countryName: { type: Function }, /** * List of available countries. * * @defaultValue * Uses `countries-list` two-letter codes, dial codes resolved using * `awesome-phonenumber` package's `getCountryCodeForRegionCode`, and * given locale (or `en`) translated names using `Intl.DisplayNames`. */ countries: { type: Array }, /** * List of countries to prefer. * * @remarks * When specified, any country which is in the list will appear first * in available countries. */ preferCountries: { type: Array }, /** * List of countries to include. * * @remarks * When specified, any country which is not in this list will be excluded * from available countries. */ includeCountries: { type: Array }, /** * List of countries to exclude. * * @remarks * When specified, any country which is in this list will be excluded * from available countries. */ excludeCountries: { type: Array }, /** * Default country to use. * * @defaultValue * First country from the available `countries` list. */ defaultCountry: { type: [String, Object] } }; } function makePhoneInputMessagesProps() { return { /** * Format example phone. * * @remarks * Using `null` will disable formating example phone numbers. * * @defaultValue * `displayFormat` value if available, `'national'` otherwise. */ exampleFormat: { type: [String, null] }, /** * Customize the phone input example. * * @defaultValue * Use `awesome-phonenumber` package's `getExample` function with current country. */ example: { type: [String, Function] }, /** * Customize the phone input label. * * @defaultValue * `'Phone'` */ label: { type: [String, Function] }, /** * Customize the phone input `aria-label`. */ ariaLabel: { type: [String, Function, null] }, /** * Customize the country input label. * * @defaultValue * `'Country for <label>'` when using the composable, `null` otherwise. */ countryLabel: { type: [ String, Function, null ] }, /** * Customize the country input `aria-label`. * * @defaultValue * `'Country for <label>'` when using the component, `null` otherwise. */ countryAriaLabel: { type: [ String, Function, null ] }, /** * Customize the phone input placeholder. */ placeholder: { type: [ String, Function, null ] }, /** * Customize the phone input invalid message returned by the * `validate` function generated rule. * * @defaultValue * `'The "<label>" field is not a valid phone number (example: <example>).'` */ invalidMessage: { type: [ String, Function, null ] } }; } function makePhoneInputComposableSpecificProps() { return { /** * Guess the country of the user. */ guessCountry: { type: Function }, /** * Format phone number for `modelValue` updates. * * @remarks * This only applies to valid phone numbers. * Using `null` will disable format feature and keep the input as is. * * @defaultValue * `'e164'` */ modelFormat: { type: [String, null] }, /** * Format phone number for text input value. * * @remarks * This only applies to valid phone numbers. * Using `null` will disable format feature and keep the input as is. * * @defaultValue * `'national'` */ displayFormat: { type: [String, null] }, /** * Delay (in milliseconds) before formatting the phone number * for text input value when detecting an input. * * @remarks * This only applies to valid phone numbers. * Using `0` will format immediately after input. * * @defaultValue * `1000` if `displayFormatOnBlur` is disabled. */ displayFormatDelay: { type: [Number, Boolean, Symbol], default: undefinedBoolean }, /** * Format the phone number for text input only on blur event. * * @remarks * This bypass the `displayFormatDelay` option, and only format the phone * number on blur event or on `displayFormat` property change. * Using `false` will restore the `displayFormatDelay` behavior. * When using the composable, it is required to bind `countryInputRef` and * `phoneInputRef` to appropriate focusable inputs. * * @defaultValue * `true` */ displayFormatOnBlur: { type: [Boolean, Symbol], default: undefinedBoolean }, /** * Validate a phone number. * * @remarks * Using `null` will disable the validation. * * @defaultValue * Ensure `ParsedPhoneNumber.valid` is true and `ParsedPhoneNumber.regionCode` is a selectable * country (not excluded from the countries list). */ validate: { type: [Function, null] } }; } function makePhoneInputComposableProps() { return { ...makePhoneInputCountryProps(), ...makePhoneInputMessagesProps(), ...makePhoneInputComposableSpecificProps() }; } function makePhoneInputComponentSpecificProps() { return { /** * Country input component. * * @internal Use `usePhoneInput` instead if you want to use a custom component. */ countryInputComponent: { type: [Function, Object] }, /** * Country input component specific props. * * @internal Use `countryProps` property instead. */ countryInputComponentProps: { type: Object }, /** * Country display component. */ countryDisplayComponent: { type: [Function, Object] }, /** * Toggle country input loading state whenever a country is currently guessing. */ guessLoading: { type: [Boolean, Symbol], default: undefinedBoolean }, /** * Properties to pass to the wrapper div. */ wrapperAttrs: { type: Object }, /** * Properties to pass to the country input (`VSelect` or `VAutocomplete`). */ countryProps: { type: Object }, /** * Properties to pass to the country input (`VTextField`). */ phoneProps: { type: Object }, /** * Customize the phone input hint. */ hint: { type: [String, Function] } }; } function makePhoneInputProps() { return { ...makePhoneInputComposableProps(), ...makePhoneInputComponentSpecificProps() }; } // @__NO_SIDE_EFFECTS__ function makePhoneInputCompleteProps() { return { ...VTextField.props, ...makePhoneInputProps() }; } const _hoisted_1 = { key: 1 }; const _hoisted_2 = { class: "v-phone-input__country__title" }; const _hoisted_3 = { key: 0, class: "v-phone-input__country__append text-body-2" }; const _sfc_main = /* @__PURE__ */ defineComponent({ ...{ inheritAttrs: false, components: { VTextField, VListItem } }, __name: "VPhoneInput", props: /* @__PURE__ */ mergeModels(/* @__PURE__ */ makePhoneInputCompleteProps(), { "modelValue": { default: "" }, "modelModifiers": {}, "country": { default: "" }, "countryModifiers": {} }), emits: /* @__PURE__ */ mergeModels(["update:country-object", "update:phone-object"], ["update:modelValue", "update:country"]), setup(__props, { expose: __expose, emit: __emit }) { const slots = useSlots(); const emit = __emit; const props = __props; const modelValue = useModel(__props, "modelValue"); const country = useModel(__props, "country"); const { useOption } = usePhoneInputPluginOptions(); const countryInputComponentOption = useOption("countryInputComponent", props); const countryInputComponentPropsOption = useOption( "countryInputComponentProps", props ); const countryDisplayComponentOption = useOption( "countryDisplayComponent", props ); const guessLoadingOption = useOption("guessLoading", props, true); const wrapperAttrsOption = useOption("wrapperAttrs", props); const countryPropsOption = useOption("countryProps", props); const phonePropsOption = useOption("phoneProps", props); const countryLabelOption = useOption("countryLabel", props, null); const countryAriaLabelOption = useOption( "countryAriaLabel", props, (ctx) => `Country for "${ctx.label}"` ); const hintOption = useOption("hint", props); const countryInputRef = shallowRef(); const phoneInputRef = shallowRef(); const countryInputFocused = ref(false); const { countries, countryGuessing, resolveMessage: resolveMessage2, countryLabel, countryAriaLabel, label, ariaLabel, placeholder, invalidMessage, countryObject, phone, phoneObject, phoneValid } = usePhoneInput({ country, modelValue, countryInputRef: computed(() => countryInputRef.value?.controlRef), phoneInputRef: computed(() => phoneInputRef.value?.controlRef), countryLocale: toRef(props, "countryLocale"), countryName: toRef(props, "countryName"), countries: toRef(props, "countries"), preferCountries: toRef(props, "preferCountries"), includeCountries: toRef(props, "includeCountries"), excludeCountries: toRef(props, "excludeCountries"), defaultCountry: toRef(props, "defaultCountry"), guessCountry: toRef(props, "guessCountry"), modelFormat: toRef(props, "modelFormat"), displayFormat: toRef(props, "displayFormat"), displayFormatDelay: toRef(props, "displayFormatDelay"), displayFormatOnBlur: toRef(props, "displayFormatOnBlur"), validate: toRef(props, "validate"), example: toRef(props, "example"), exampleFormat: toRef(props, "exampleFormat"), countryLabel: countryLabelOption, countryAriaLabel: countryAriaLabelOption, label: toRef(props, "label"), ariaLabel: toRef(props, "ariaLabel"), placeholder: toRef(props, "placeholder"), invalidMessage: toRef(props, "invalidMessage") }); const hint = computed(() => resolveMessage2.value(hintOption.value)); const phoneInputRules = computed(() => [ ...props.rules ?? [], () => { if (phoneValid.value === false) { return invalidMessage.value ?? false; } return true; } ]); const extractMenuProps = (from) => from && "menuProps" in from ? from.menuProps : {}; const countryInputProps = computed(() => ({ ...pick(props, vPhoneInputSharedProperties), ...countryInputComponentPropsOption.value, ref: countryInputRef, modelValue: country.value, label: countryLabel.value, "aria-label": countryAriaLabel.value, itemTitle: "name", itemValue: "iso2", items: countries.value, prependIcon: props.prependIcon, prependInnerIcon: props.prependInnerIcon, appendInnerIcon: void 0, appendIcon: void 0, loading: guessLoadingOption.value && countryGuessing.value, ...countryPropsOption.value, menuProps: { width: 300, maxHeight: 300, contentClass: "v-phone-input__country__menu", closeOnContentClick: true, ...extractMenuProps( countryInputComponentPropsOption.value ), ...extractMenuProps(countryPropsOption.value) }, class: [ "v-phone-input__country__input", { "v-phone-input__country__input--focused": countryInputFocused.value } ], onFocus: () => { countryInputFocused.value = true; }, onBlur: () => { countryInputFocused.value = false; }, "onUpdate:modelValue": (value) => { country.value = value; } })); const nonForwardedPropsKeys = [ ...Object.keys(makePhoneInputProps()), "modelValue", "country" ]; const forwardedProps = computed( () => omitBy( props, (key) => nonForwardedPropsKeys.indexOf(key) === -1 && props[key] !== VTextField.props[key]?.default ) ); const phoneInputProps = computed(() => ({ ...pick(props, vPhoneInputSharedProperties), ...forwardedProps.value, ref: phoneInputRef, modelValue: phone.value, label: label.value, "aria-label": ariaLabel.value, placeholder: placeholder.value, hint: hint.value, rules: phoneInputRules.value, prependIcon: void 0, prependInnerIcon: void 0, appendInnerIcon: props.appendInnerIcon, appendIcon: props.appendIcon, id: void 0, style: void 0, ...phonePropsOption.value, class: "v-phone-input__phone__input", type: "tel", "onUpdate:modelValue": (value) => { phone.value = value; } })); const parsedSlots = computed( () => Object.keys(slots).reduce( (nextSlots, name) => { const [namespace, ...names] = name.split(":"); if (namespace === "country-input" && names.length) { const targetName = names.join(":"); if (targetName !== "selection" && targetName !== "item") { nextSlots.country[targetName] = name; } } else { nextSlots.default[name] = null; } return nextSlots; }, { country: {}, default: {} } ) ); watch(countryObject, (next) => emit("update:country-object", next)); watch(phoneObject, (next) => emit("update:phone-object", next)); __expose({ countryInputRef, phoneInputRef, countryObject, phoneObject, isValid: computed(() => phoneInputRef.value?.isValid ?? null), errorMessages: computed( () => phoneInputRef.value?.errorMessages ?? [] ), reset: async () => phoneInputRef.value?.reset(), resetValidation: async () => phoneInputRef.value?.resetValidation(), validate: async (silent) => await phoneInputRef.value?.validate(silent) ?? [] }); return (_ctx, _cache) => { return openBlock(), createElementBlock("div", mergeProps({ class: [{ "v-phone-input--prepend-inner-icon": _ctx.prependInnerIcon }, "v-phone-input"] }, unref(wrapperAttrsOption)), [ (openBlock(), createBlock(resolveDynamicComponent(unref(countryInputComponentOption)), normalizeProps(guardReactiveProps(countryInputProps.value)), createSlots({ selection: withCtx((selectionProps) => [ renderSlot(_ctx.$slots, "country-input:selection", normalizeProps(guardReactiveProps(selectionProps)), () => [ renderSlot(_ctx.$slots, "country-display", { country: unref(countryObject), decorative: false }, () => [ unref(countryDisplayComponentOption) ? (openBlock(), createBlock(resolveDynamicComponent(unref(countryDisplayComponentOption)), { key: 0, country: unref(countryObject), decorative: false }, null, 8, ["country"])) : (openBlock(), createElementBlock("span", _hoisted_1, toDisplayString(`+${unref(countryObject).dialCode}`), 1)) ]) ]) ]), item: withCtx((itemProps) => [ renderSlot(_ctx.$slots, "country-input:item", normalizeProps(guardReactiveProps(itemProps)), () => [ createVNode(unref(VListItem), normalizeProps(guardReactiveProps(itemProps.props)), { prepend: withCtx(() => [ renderSlot(_ctx.$slots, "country-display", { country: itemProps.item.raw, decorative: true }, () => [ unref(countryDisplayComponentOption) ? (openBlock(), createBlock(resolveDynamicComponent(unref(countryDisplayComponentOption)), { key: 0, country: itemProps.item.raw, decorative: true }, null, 8, ["country"])) : createCommentVNode("", true) ]) ]), title: withCtx(() => [ renderSlot(_ctx.$slots, "country-name", { country: itemProps.item.raw }, () => [ createElementVNode("span", _hoisted_2, toDisplayString(itemProps.item.raw.name), 1) ]) ]), append: withCtx(() => [ renderSlot(_ctx.$slots, "country-append", { country: itemProps.item.raw }, () => [ unref(countryDisplayComponentOption) ? (openBlock(), createElementBlock("span", _hoisted_3, " +" + toDisplayString(itemProps.item.raw.dialCode), 1)) : createCommentVNode("", true) ]) ]), _: 2 }, 1040) ]) ]), _: 2 }, [ renderList(parsedSlots.value.country, (targetName, name) => { return { name, fn: withCtx((data) => [ renderSlot(_ctx.$slots, targetName, normalizeProps(guardReactiveProps(data))) ]) }; }) ]), 1040)), (openBlock(), createBlock(resolveDynamicComponent(unref(VTextField)), normalizeProps(guardReactiveProps(phoneInputProps.value)), createSlots({ _: 2 }, [ renderList(parsedSlots.value.default, (_, name) => { return { name, fn: withCtx((data) => [ renderSlot(_ctx.$slots, name, normalizeProps(guardReactiveProps(data))) ]) }; }) ]), 1040)) ], 16); }; } }); function createVPhoneInput(options) { return (app, pluginOptions) => { app.provide(V_PHONE_INPUT_INJECTION_KEY, { ...options, ...pluginOptions }); app.component("VPhoneInput", _sfc_main); }; } const autocompletePhoneCountryInput = { countryInputComponent: /* @__PURE__ */ markRaw(VAutocomplete), countryInputComponentProps: { filterKeys: ["raw.name", "raw.iso2", "raw.dialCode"], autocomplete: "off", "aria-autocomplete": "off" } }; async function guessPhoneCountry() { let response; let responseText; try { response = await fetch("https://ip2c.org/s"); responseText = await response.text(); } catch (_) { return void 0; } const countryData = responseText.toString().split(";"); if (!countryData || countryData[0] !== "1") { return void 0; } return countryData[1]; } function memoizeGuessPhoneCountry(guess, get, set) { return Object.defineProperty( () => guess(), "memoized", isRef(get) ? { value: get } : { get, set } ); } function providePhoneInputOptions(options) { provide(V_PHONE_INPUT_INJECTION_KEY, options); } const selectPhoneCountryInput = { countryInputComponent: /* @__PURE__ */ markRaw(VSelect), countryInputComponentProps: {} }; export { VPhoneCountryFlagSprite, VPhoneCountryFlagSvg, _sfc_main as VPhoneInput, autocompletePhoneCountryInput, createVPhoneInput, guessPhoneCountry, memoizeGuessPhoneCountry, providePhoneInputOptions, selectPhoneCountryInput, usePhoneInput, vPhoneInputSharedProperties };