@alikhalilll/a-tel-input 1.0.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +585 -72
- package/dist/_chunks/types.d.ts +661 -0
- package/dist/_chunks/types.js +52 -0
- package/dist/_chunks/types.js.map +1 -0
- package/dist/_chunks/usePhoneValidation.js +539 -0
- package/dist/_chunks/usePhoneValidation.js.map +1 -0
- package/dist/index.cjs +444 -683
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +122 -587
- package/dist/index.d.ts +122 -587
- package/dist/index.js +427 -646
- package/dist/index.js.map +1 -1
- package/dist/styles.css +3 -2
- package/dist/vee-validate/index.cjs +113 -0
- package/dist/vee-validate/index.cjs.map +1 -0
- package/dist/vee-validate/index.d.cts +86 -0
- package/dist/vee-validate/index.d.ts +86 -0
- package/dist/vee-validate/index.js +112 -0
- package/dist/vee-validate/index.js.map +1 -0
- package/dist/zod/index.cjs +211 -0
- package/dist/zod/index.cjs.map +1 -0
- package/dist/zod/index.d.cts +65 -0
- package/dist/zod/index.d.ts +65 -0
- package/dist/zod/index.js +208 -0
- package/dist/zod/index.js.map +1 -0
- package/package.json +33 -3
- package/src/components/ATelInput.vue +206 -66
- package/src/composables/useCountryDetection.ts +28 -11
- package/src/composables/useCountryMatching.ts +160 -20
- package/src/composables/useCountrySelection.ts +71 -0
- package/src/composables/usePhoneValidation.ts +81 -18
- package/src/composables/useSyncedModel.ts +80 -0
- package/src/composables/useTelInputValidation.ts +50 -11
- package/src/index.ts +2 -0
- package/src/types.ts +80 -0
- package/src/vee-validate/index.ts +2 -0
- package/src/vee-validate/useTelField.ts +202 -0
- package/src/zod/index.ts +259 -0
- package/web-types.json +44 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { i as normalizeDigits, n as usePhoneValidation, r as LOCALE_DIGIT_RANGES, t as localizeCountries } from "./_chunks/usePhoneValidation.js";
|
|
2
|
+
import { i as resolveMessages, n as DEFAULT_MESSAGES, r as aTelInputVariants, t as DEFAULT_ERROR_MESSAGES } from "./_chunks/types.js";
|
|
1
3
|
import * as vue from "vue";
|
|
2
4
|
import { Comment, Fragment, Teleport, Transition, camelize, cloneVNode, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createVNode, defineComponent, getCurrentInstance, guardReactiveProps, h, inject, isRef, mergeDefaults, mergeModels, mergeProps, nextTick, normalizeClass, normalizeProps, normalizeStyle, onBeforeUnmount, onMounted, onUnmounted, onUpdated, openBlock, provide, reactive, readonly, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, toHandlerKey, toRef, toRefs, toValue, triggerRef, unref, useId, useModel, useSlots, vModelText, watch, watchEffect, watchPostEffect, withCtx, withDirectives, withModifiers } from "vue";
|
|
3
|
-
import {
|
|
4
|
-
import examples from "libphonenumber-js/examples.mobile.json";
|
|
5
|
+
import { getCountries, parsePhoneNumberFromString } from "libphonenumber-js";
|
|
5
6
|
import { computedEager, createGlobalState, createSharedComposable, defaultWindow, onKeyStroke, reactiveOmit, unrefElement, useDebounceFn, useEventListener, useMediaQuery, useMounted, useVModel } from "@vueuse/core";
|
|
6
|
-
import { cva } from "class-variance-authority";
|
|
7
7
|
import { hideOthers } from "aria-hidden";
|
|
8
8
|
//#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
9
9
|
function r$2(e) {
|
|
@@ -3404,508 +3404,6 @@ function cn$2(...inputs) {
|
|
|
3404
3404
|
return twMerge$2(clsx$2(inputs));
|
|
3405
3405
|
}
|
|
3406
3406
|
//#endregion
|
|
3407
|
-
//#region src/utils/digits.ts
|
|
3408
|
-
/**
|
|
3409
|
-
* Alternative-numeral support. Phone numbers are routinely entered with the digits of the
|
|
3410
|
-
* user's own script — Arabic-Indic, Persian/Urdu, Devanagari, Bengali. `libphonenumber-js`
|
|
3411
|
-
* and our own `\d` cleanup only understand ASCII `0-9`, so anything else silently becomes
|
|
3412
|
-
* an empty number. `normalizeDigits` folds those scripts down to ASCII before validation.
|
|
3413
|
-
*/
|
|
3414
|
-
/**
|
|
3415
|
-
* Base code points of contiguous decimal-digit blocks. Each block runs `base`‥`base+9`
|
|
3416
|
-
* for digit `0`‥`9`, so the ASCII digit is `codePoint - base`. Add a script by appending
|
|
3417
|
-
* one entry here.
|
|
3418
|
-
*/
|
|
3419
|
-
const LOCALE_DIGIT_RANGES = [
|
|
3420
|
-
{
|
|
3421
|
-
name: "arabic-indic",
|
|
3422
|
-
base: 1632
|
|
3423
|
-
},
|
|
3424
|
-
{
|
|
3425
|
-
name: "extended-arabic",
|
|
3426
|
-
base: 1776
|
|
3427
|
-
},
|
|
3428
|
-
{
|
|
3429
|
-
name: "devanagari",
|
|
3430
|
-
base: 2406
|
|
3431
|
-
},
|
|
3432
|
-
{
|
|
3433
|
-
name: "bengali",
|
|
3434
|
-
base: 2534
|
|
3435
|
-
}
|
|
3436
|
-
];
|
|
3437
|
-
/** Lookup of every non-ASCII digit code point → its ASCII character. */
|
|
3438
|
-
const DIGIT_MAP = (() => {
|
|
3439
|
-
const map = /* @__PURE__ */ new Map();
|
|
3440
|
-
for (const { base } of LOCALE_DIGIT_RANGES) for (let d = 0; d <= 9; d++) map.set(base + d, String(d));
|
|
3441
|
-
return map;
|
|
3442
|
-
})();
|
|
3443
|
-
/**
|
|
3444
|
-
* Replace any supported non-ASCII decimal digit with its ASCII equivalent. Every other
|
|
3445
|
-
* character (spaces, `+`, separators, letters) is left untouched — callers still run their
|
|
3446
|
-
* own `\D` cleanup afterwards.
|
|
3447
|
-
*/
|
|
3448
|
-
function normalizeDigits(input) {
|
|
3449
|
-
const str = String(input ?? "");
|
|
3450
|
-
let out = "";
|
|
3451
|
-
for (const ch of str) {
|
|
3452
|
-
const cp = ch.codePointAt(0);
|
|
3453
|
-
out += cp != null && DIGIT_MAP.get(cp) || ch;
|
|
3454
|
-
}
|
|
3455
|
-
return out;
|
|
3456
|
-
}
|
|
3457
|
-
//#endregion
|
|
3458
|
-
//#region src/composables/usePhoneValidation.ts
|
|
3459
|
-
/**
|
|
3460
|
-
* Country list + phone validation, framework-agnostic.
|
|
3461
|
-
*
|
|
3462
|
-
* Ported from the reference @pkgs/ui ATelInput composable with these cleanups:
|
|
3463
|
-
* - Drop Nuxt-only `process.client` checks → use plain `typeof window !== 'undefined'`.
|
|
3464
|
-
* - Drop Arabic default placeholder; let consumers pass their own.
|
|
3465
|
-
* - Expand the offline fallback list from 2 → ~20 of the most-populated countries.
|
|
3466
|
-
* - Keep REST Countries fetch + localStorage cache + libphonenumber-js examples + fast `search_key`.
|
|
3467
|
-
*/
|
|
3468
|
-
const STORAGE_KEY = "ali_ui_phone_countries_v1";
|
|
3469
|
-
const REST_COUNTRIES_URL = "https://restcountries.com/v3.1/all?fields=name,cca2,idd,flags";
|
|
3470
|
-
const EX = examples;
|
|
3471
|
-
const isBrowser$1 = () => typeof window !== "undefined";
|
|
3472
|
-
function toDigits(v) {
|
|
3473
|
-
return normalizeDigits(String(v ?? "")).replace(/\D/g, "");
|
|
3474
|
-
}
|
|
3475
|
-
/**
|
|
3476
|
-
* Render an ASCII digit string in a locale's numeral system (e.g. `'ar'` → `٠-٩`).
|
|
3477
|
-
* Used only for display hints — falls back to ASCII if the locale is unknown.
|
|
3478
|
-
*/
|
|
3479
|
-
function localizeDigits(digits, locale) {
|
|
3480
|
-
if (!locale) return digits;
|
|
3481
|
-
try {
|
|
3482
|
-
const fmt = new Intl.NumberFormat(locale, { useGrouping: false });
|
|
3483
|
-
return digits.replace(/[0-9]/g, (d) => fmt.format(Number(d)));
|
|
3484
|
-
} catch {
|
|
3485
|
-
return digits;
|
|
3486
|
-
}
|
|
3487
|
-
}
|
|
3488
|
-
function ensurePlusDial(dial) {
|
|
3489
|
-
const d = toDigits(dial);
|
|
3490
|
-
return d ? `+${d}` : "";
|
|
3491
|
-
}
|
|
3492
|
-
function normalizeIso2(iso2) {
|
|
3493
|
-
return String(iso2 ?? "").trim().toUpperCase();
|
|
3494
|
-
}
|
|
3495
|
-
function dropLeadingZeros(digits) {
|
|
3496
|
-
return String(digits ?? "").replace(/^0+/, "");
|
|
3497
|
-
}
|
|
3498
|
-
function buildFullE164(dial, digits) {
|
|
3499
|
-
const dialClean = ensurePlusDial(dial);
|
|
3500
|
-
const nsn = dropLeadingZeros(toDigits(digits));
|
|
3501
|
-
return dialClean && nsn ? `${dialClean}${nsn}` : null;
|
|
3502
|
-
}
|
|
3503
|
-
function inferLengthFromExample(national) {
|
|
3504
|
-
const d = toDigits(national);
|
|
3505
|
-
if (!d) return {
|
|
3506
|
-
min: null,
|
|
3507
|
-
max: null
|
|
3508
|
-
};
|
|
3509
|
-
const n = d.length;
|
|
3510
|
-
return {
|
|
3511
|
-
min: Math.max(4, n - 2),
|
|
3512
|
-
max: n + 2
|
|
3513
|
-
};
|
|
3514
|
-
}
|
|
3515
|
-
function buildDialCode(idd) {
|
|
3516
|
-
const root = idd?.root?.trim();
|
|
3517
|
-
if (!root || !root.startsWith("+")) return null;
|
|
3518
|
-
const out = `${root}${idd?.suffixes?.[0]?.trim() ?? ""}`;
|
|
3519
|
-
return out.startsWith("+") ? out : null;
|
|
3520
|
-
}
|
|
3521
|
-
function normalizeSearchKey(input) {
|
|
3522
|
-
return String(input ?? "").toLowerCase().replace(/\s+/g, " ").trim().replace(/[^\p{L}\p{N}+ ]/gu, "");
|
|
3523
|
-
}
|
|
3524
|
-
/**
|
|
3525
|
-
* Return a copy of the country list with display names localized to `locale` via
|
|
3526
|
-
* `Intl.DisplayNames`. `search_key` is rebuilt (keeping the English name too) so search
|
|
3527
|
-
* still matches either spelling. Unknown locales / regions fall back to the English name.
|
|
3528
|
-
*/
|
|
3529
|
-
function localizeCountries(list, locale) {
|
|
3530
|
-
if (!locale) return list;
|
|
3531
|
-
let display;
|
|
3532
|
-
try {
|
|
3533
|
-
display = new Intl.DisplayNames([locale], { type: "region" });
|
|
3534
|
-
} catch {
|
|
3535
|
-
return list;
|
|
3536
|
-
}
|
|
3537
|
-
return list.map((c) => {
|
|
3538
|
-
let localized = c.raw_data.name;
|
|
3539
|
-
try {
|
|
3540
|
-
localized = display.of(c.raw_data.iso2) || c.raw_data.name;
|
|
3541
|
-
} catch {}
|
|
3542
|
-
if (localized === c.raw_data.name) return c;
|
|
3543
|
-
const dial = c.raw_data.dial_code;
|
|
3544
|
-
return {
|
|
3545
|
-
...c,
|
|
3546
|
-
label: `${localized} (${dial})`,
|
|
3547
|
-
search_key: normalizeSearchKey(`${localized} ${c.raw_data.name} ${dial} ${c.raw_data.iso2} ${c.raw_data.dial_digits}`),
|
|
3548
|
-
raw_data: {
|
|
3549
|
-
...c.raw_data,
|
|
3550
|
-
name: localized
|
|
3551
|
-
}
|
|
3552
|
-
};
|
|
3553
|
-
});
|
|
3554
|
-
}
|
|
3555
|
-
function makeFallback(iso2, name, dial) {
|
|
3556
|
-
const dialDigits = toDigits(dial);
|
|
3557
|
-
return {
|
|
3558
|
-
label: `${name} (+${dialDigits})`,
|
|
3559
|
-
value: iso2,
|
|
3560
|
-
search_key: normalizeSearchKey(`${name} +${dialDigits} ${iso2}`),
|
|
3561
|
-
raw_data: {
|
|
3562
|
-
iso2,
|
|
3563
|
-
dial_code: `+${dialDigits}`,
|
|
3564
|
-
dial_digits: dialDigits,
|
|
3565
|
-
name,
|
|
3566
|
-
flag: `https://flagcdn.com/w40/${iso2.toLowerCase()}.png`,
|
|
3567
|
-
source: "fallback",
|
|
3568
|
-
original: {}
|
|
3569
|
-
}
|
|
3570
|
-
};
|
|
3571
|
-
}
|
|
3572
|
-
const FALLBACK_COUNTRIES = [
|
|
3573
|
-
makeFallback("SA", "Saudi Arabia", "+966"),
|
|
3574
|
-
makeFallback("EG", "Egypt", "+20"),
|
|
3575
|
-
makeFallback("AE", "United Arab Emirates", "+971"),
|
|
3576
|
-
makeFallback("US", "United States", "+1"),
|
|
3577
|
-
makeFallback("GB", "United Kingdom", "+44"),
|
|
3578
|
-
makeFallback("DE", "Germany", "+49"),
|
|
3579
|
-
makeFallback("FR", "France", "+33"),
|
|
3580
|
-
makeFallback("ES", "Spain", "+34"),
|
|
3581
|
-
makeFallback("IT", "Italy", "+39"),
|
|
3582
|
-
makeFallback("TR", "Turkey", "+90"),
|
|
3583
|
-
makeFallback("RU", "Russia", "+7"),
|
|
3584
|
-
makeFallback("CN", "China", "+86"),
|
|
3585
|
-
makeFallback("IN", "India", "+91"),
|
|
3586
|
-
makeFallback("JP", "Japan", "+81"),
|
|
3587
|
-
makeFallback("KR", "South Korea", "+82"),
|
|
3588
|
-
makeFallback("BR", "Brazil", "+55"),
|
|
3589
|
-
makeFallback("MX", "Mexico", "+52"),
|
|
3590
|
-
makeFallback("CA", "Canada", "+1"),
|
|
3591
|
-
makeFallback("AU", "Australia", "+61"),
|
|
3592
|
-
makeFallback("NG", "Nigeria", "+234"),
|
|
3593
|
-
makeFallback("PK", "Pakistan", "+92"),
|
|
3594
|
-
makeFallback("ID", "Indonesia", "+62")
|
|
3595
|
-
];
|
|
3596
|
-
function usePhoneValidation() {
|
|
3597
|
-
const countries = ref([]);
|
|
3598
|
-
const isCountriesLoading = ref(false);
|
|
3599
|
-
const byValue = ref(/* @__PURE__ */ new Map());
|
|
3600
|
-
const byDialDigits = ref(/* @__PURE__ */ new Map());
|
|
3601
|
-
function rebuildIndexes(list) {
|
|
3602
|
-
const valueMap = /* @__PURE__ */ new Map();
|
|
3603
|
-
const dialMap = /* @__PURE__ */ new Map();
|
|
3604
|
-
for (const item of list) {
|
|
3605
|
-
valueMap.set(item.value, item);
|
|
3606
|
-
const dial = item.raw_data.dial_digits;
|
|
3607
|
-
if (dial) {
|
|
3608
|
-
const bucket = dialMap.get(dial) ?? [];
|
|
3609
|
-
bucket.push(item);
|
|
3610
|
-
dialMap.set(dial, bucket);
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
|
-
byValue.value = valueMap;
|
|
3614
|
-
byDialDigits.value = dialMap;
|
|
3615
|
-
}
|
|
3616
|
-
function upsertCountries(list) {
|
|
3617
|
-
countries.value = list;
|
|
3618
|
-
rebuildIndexes(list);
|
|
3619
|
-
}
|
|
3620
|
-
function normalizeRestCountries(list) {
|
|
3621
|
-
const out = [];
|
|
3622
|
-
for (const c of list) {
|
|
3623
|
-
const name = c?.name?.common?.trim();
|
|
3624
|
-
const iso2 = normalizeIso2(c?.cca2);
|
|
3625
|
-
const dial = buildDialCode(c?.idd);
|
|
3626
|
-
const flag = c?.flags?.png?.trim() || c?.flags?.svg?.trim() || null;
|
|
3627
|
-
if (!name || !iso2 || !dial) continue;
|
|
3628
|
-
const dialDigits = toDigits(dial);
|
|
3629
|
-
const search_key = normalizeSearchKey(`${name} ${dial} ${iso2} ${dialDigits}`);
|
|
3630
|
-
out.push({
|
|
3631
|
-
label: `${name} (${dial})`,
|
|
3632
|
-
value: iso2,
|
|
3633
|
-
search_key,
|
|
3634
|
-
raw_data: {
|
|
3635
|
-
iso2,
|
|
3636
|
-
dial_code: dial,
|
|
3637
|
-
dial_digits: dialDigits,
|
|
3638
|
-
name,
|
|
3639
|
-
flag,
|
|
3640
|
-
source: "restcountries",
|
|
3641
|
-
original: c
|
|
3642
|
-
}
|
|
3643
|
-
});
|
|
3644
|
-
}
|
|
3645
|
-
const map = /* @__PURE__ */ new Map();
|
|
3646
|
-
for (const item of out) {
|
|
3647
|
-
const prev = map.get(item.value);
|
|
3648
|
-
if (!prev) {
|
|
3649
|
-
map.set(item.value, item);
|
|
3650
|
-
continue;
|
|
3651
|
-
}
|
|
3652
|
-
const prevScore = (prev.raw_data.flag ? 1 : 0) + (prev.raw_data.dial_code ? 1 : 0);
|
|
3653
|
-
if ((item.raw_data.flag ? 1 : 0) + (item.raw_data.dial_code ? 1 : 0) > prevScore) map.set(item.value, item);
|
|
3654
|
-
}
|
|
3655
|
-
return Array.from(map.values()).sort((a, b) => a.raw_data.name.localeCompare(b.raw_data.name));
|
|
3656
|
-
}
|
|
3657
|
-
async function getCountries(options) {
|
|
3658
|
-
const force = Boolean(options?.force);
|
|
3659
|
-
if (!force && countries.value.length) return countries.value;
|
|
3660
|
-
if (!force && isBrowser$1()) try {
|
|
3661
|
-
const cached = localStorage.getItem(STORAGE_KEY);
|
|
3662
|
-
if (cached) {
|
|
3663
|
-
const parsed = JSON.parse(cached);
|
|
3664
|
-
if (Array.isArray(parsed) && parsed.length) {
|
|
3665
|
-
upsertCountries(parsed);
|
|
3666
|
-
return countries.value;
|
|
3667
|
-
}
|
|
3668
|
-
}
|
|
3669
|
-
} catch {}
|
|
3670
|
-
isCountriesLoading.value = true;
|
|
3671
|
-
try {
|
|
3672
|
-
const res = await fetch(REST_COUNTRIES_URL);
|
|
3673
|
-
if (!res.ok) throw new Error(`Failed to fetch countries: ${res.status}`);
|
|
3674
|
-
const normalized = normalizeRestCountries(await res.json());
|
|
3675
|
-
upsertCountries(normalized.length ? normalized : FALLBACK_COUNTRIES);
|
|
3676
|
-
if (isBrowser$1()) try {
|
|
3677
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(countries.value));
|
|
3678
|
-
} catch {}
|
|
3679
|
-
return countries.value;
|
|
3680
|
-
} catch {
|
|
3681
|
-
upsertCountries(FALLBACK_COUNTRIES);
|
|
3682
|
-
return countries.value;
|
|
3683
|
-
} finally {
|
|
3684
|
-
isCountriesLoading.value = false;
|
|
3685
|
-
}
|
|
3686
|
-
}
|
|
3687
|
-
function searchCountries(keyword, limit = 50) {
|
|
3688
|
-
const q = normalizeSearchKey(keyword);
|
|
3689
|
-
if (!q) return countries.value.slice(0, limit);
|
|
3690
|
-
const res = [];
|
|
3691
|
-
for (const item of countries.value) if (item.search_key.includes(q)) {
|
|
3692
|
-
res.push(item);
|
|
3693
|
-
if (res.length >= limit) break;
|
|
3694
|
-
}
|
|
3695
|
-
return res;
|
|
3696
|
-
}
|
|
3697
|
-
function getCountryByValue(value) {
|
|
3698
|
-
return byValue.value.get(normalizeIso2(value)) ?? null;
|
|
3699
|
-
}
|
|
3700
|
-
function getCountriesByDial(dial) {
|
|
3701
|
-
return byDialDigits.value.get(toDigits(dial)) ?? [];
|
|
3702
|
-
}
|
|
3703
|
-
function getRequiredInfo(country, locale) {
|
|
3704
|
-
const iso2 = normalizeIso2(country.iso2);
|
|
3705
|
-
if (!iso2) return null;
|
|
3706
|
-
try {
|
|
3707
|
-
const example = getExampleNumber(iso2, EX);
|
|
3708
|
-
const exampleNational = example?.formatNational?.() ?? "";
|
|
3709
|
-
const exampleE164 = example?.format?.("E.164") ?? "";
|
|
3710
|
-
const inferred = inferLengthFromExample(exampleNational);
|
|
3711
|
-
const dial_code = country.dial_code ? ensurePlusDial(country.dial_code) : exampleE164 ? `+${example?.countryCallingCode}` : "";
|
|
3712
|
-
const digitsExample = toDigits(exampleNational);
|
|
3713
|
-
return {
|
|
3714
|
-
iso2,
|
|
3715
|
-
dial_code,
|
|
3716
|
-
placeholder: "",
|
|
3717
|
-
example_national: exampleNational,
|
|
3718
|
-
example_e164: exampleE164,
|
|
3719
|
-
national_number_length: inferred,
|
|
3720
|
-
format_hint: digitsExample ? `e.g. ${localizeDigits(digitsExample, locale)}` : ""
|
|
3721
|
-
};
|
|
3722
|
-
} catch {
|
|
3723
|
-
return null;
|
|
3724
|
-
}
|
|
3725
|
-
}
|
|
3726
|
-
function validate(input) {
|
|
3727
|
-
const country = input.country ?? null;
|
|
3728
|
-
if (!country?.iso2) return {
|
|
3729
|
-
ok: false,
|
|
3730
|
-
reason: "missing_country",
|
|
3731
|
-
country: null,
|
|
3732
|
-
phone: {
|
|
3733
|
-
raw: ("phone" in input ? input.phone : null) ?? null,
|
|
3734
|
-
digits: ""
|
|
3735
|
-
},
|
|
3736
|
-
full_phone: null,
|
|
3737
|
-
required: null
|
|
3738
|
-
};
|
|
3739
|
-
const iso2 = normalizeIso2(country.iso2);
|
|
3740
|
-
const required = getRequiredInfo({
|
|
3741
|
-
iso2,
|
|
3742
|
-
dial_code: country.dial_code
|
|
3743
|
-
}, input.locale);
|
|
3744
|
-
if (!required) return {
|
|
3745
|
-
ok: false,
|
|
3746
|
-
reason: "country_not_supported",
|
|
3747
|
-
country: {
|
|
3748
|
-
iso2,
|
|
3749
|
-
dial_code: ensurePlusDial(country.dial_code)
|
|
3750
|
-
},
|
|
3751
|
-
phone: {
|
|
3752
|
-
raw: ("phone" in input ? input.phone : null) ?? null,
|
|
3753
|
-
digits: ""
|
|
3754
|
-
},
|
|
3755
|
-
full_phone: null,
|
|
3756
|
-
required: null
|
|
3757
|
-
};
|
|
3758
|
-
if (!("phone" in input)) return {
|
|
3759
|
-
ok: true,
|
|
3760
|
-
reason: null,
|
|
3761
|
-
country: {
|
|
3762
|
-
iso2: required.iso2,
|
|
3763
|
-
dial_code: required.dial_code
|
|
3764
|
-
},
|
|
3765
|
-
phone: {
|
|
3766
|
-
raw: null,
|
|
3767
|
-
digits: ""
|
|
3768
|
-
},
|
|
3769
|
-
full_phone: null,
|
|
3770
|
-
required
|
|
3771
|
-
};
|
|
3772
|
-
const raw = input.phone;
|
|
3773
|
-
const digits = toDigits(raw);
|
|
3774
|
-
if (!raw || !String(raw).trim()) return {
|
|
3775
|
-
ok: true,
|
|
3776
|
-
reason: null,
|
|
3777
|
-
country: {
|
|
3778
|
-
iso2: required.iso2,
|
|
3779
|
-
dial_code: required.dial_code
|
|
3780
|
-
},
|
|
3781
|
-
phone: {
|
|
3782
|
-
raw: raw ?? null,
|
|
3783
|
-
digits: ""
|
|
3784
|
-
},
|
|
3785
|
-
full_phone: null,
|
|
3786
|
-
required
|
|
3787
|
-
};
|
|
3788
|
-
if (String(raw).replace(/\s+/g, "").match(/[^\d+]/)) return {
|
|
3789
|
-
ok: false,
|
|
3790
|
-
reason: "phone_has_non_digits",
|
|
3791
|
-
country: {
|
|
3792
|
-
iso2: required.iso2,
|
|
3793
|
-
dial_code: required.dial_code
|
|
3794
|
-
},
|
|
3795
|
-
phone: {
|
|
3796
|
-
raw,
|
|
3797
|
-
digits
|
|
3798
|
-
},
|
|
3799
|
-
full_phone: buildFullE164(required.dial_code, digits),
|
|
3800
|
-
required
|
|
3801
|
-
};
|
|
3802
|
-
const nsn = dropLeadingZeros(digits);
|
|
3803
|
-
const { min, max } = required.national_number_length;
|
|
3804
|
-
if (min !== null && nsn.length < min) return {
|
|
3805
|
-
ok: false,
|
|
3806
|
-
reason: "too_short",
|
|
3807
|
-
country: {
|
|
3808
|
-
iso2: required.iso2,
|
|
3809
|
-
dial_code: required.dial_code
|
|
3810
|
-
},
|
|
3811
|
-
phone: {
|
|
3812
|
-
raw,
|
|
3813
|
-
digits
|
|
3814
|
-
},
|
|
3815
|
-
full_phone: buildFullE164(required.dial_code, digits),
|
|
3816
|
-
required,
|
|
3817
|
-
details: {
|
|
3818
|
-
min,
|
|
3819
|
-
actual: nsn.length
|
|
3820
|
-
}
|
|
3821
|
-
};
|
|
3822
|
-
if (max !== null && nsn.length > max) return {
|
|
3823
|
-
ok: false,
|
|
3824
|
-
reason: "too_long",
|
|
3825
|
-
country: {
|
|
3826
|
-
iso2: required.iso2,
|
|
3827
|
-
dial_code: required.dial_code
|
|
3828
|
-
},
|
|
3829
|
-
phone: {
|
|
3830
|
-
raw,
|
|
3831
|
-
digits
|
|
3832
|
-
},
|
|
3833
|
-
full_phone: buildFullE164(required.dial_code, digits),
|
|
3834
|
-
required,
|
|
3835
|
-
details: {
|
|
3836
|
-
max,
|
|
3837
|
-
actual: nsn.length
|
|
3838
|
-
}
|
|
3839
|
-
};
|
|
3840
|
-
const full = buildFullE164(required.dial_code, digits) ?? String(raw);
|
|
3841
|
-
try {
|
|
3842
|
-
if (!isValidPhoneNumber(full, iso2)) {
|
|
3843
|
-
const parsed = parsePhoneNumberFromString(full, iso2);
|
|
3844
|
-
return {
|
|
3845
|
-
ok: false,
|
|
3846
|
-
reason: "invalid_phone",
|
|
3847
|
-
country: {
|
|
3848
|
-
iso2: required.iso2,
|
|
3849
|
-
dial_code: required.dial_code
|
|
3850
|
-
},
|
|
3851
|
-
phone: {
|
|
3852
|
-
raw,
|
|
3853
|
-
digits
|
|
3854
|
-
},
|
|
3855
|
-
full_phone: parsed?.number ?? null,
|
|
3856
|
-
required,
|
|
3857
|
-
details: {
|
|
3858
|
-
type: parsed?.getType?.() ?? null,
|
|
3859
|
-
possible: parsed?.isPossible?.() ?? null,
|
|
3860
|
-
country: parsed?.country ?? null
|
|
3861
|
-
}
|
|
3862
|
-
};
|
|
3863
|
-
}
|
|
3864
|
-
const parsed = parsePhoneNumberFromString(full, iso2);
|
|
3865
|
-
return {
|
|
3866
|
-
ok: true,
|
|
3867
|
-
reason: null,
|
|
3868
|
-
country: {
|
|
3869
|
-
iso2: required.iso2,
|
|
3870
|
-
dial_code: required.dial_code
|
|
3871
|
-
},
|
|
3872
|
-
phone: {
|
|
3873
|
-
raw,
|
|
3874
|
-
digits
|
|
3875
|
-
},
|
|
3876
|
-
full_phone: parsed?.number ?? full,
|
|
3877
|
-
required
|
|
3878
|
-
};
|
|
3879
|
-
} catch (e) {
|
|
3880
|
-
return {
|
|
3881
|
-
ok: false,
|
|
3882
|
-
reason: "parse_failed",
|
|
3883
|
-
country: {
|
|
3884
|
-
iso2: required.iso2,
|
|
3885
|
-
dial_code: required.dial_code
|
|
3886
|
-
},
|
|
3887
|
-
phone: {
|
|
3888
|
-
raw,
|
|
3889
|
-
digits
|
|
3890
|
-
},
|
|
3891
|
-
full_phone: buildFullE164(required.dial_code, digits),
|
|
3892
|
-
required,
|
|
3893
|
-
details: { error: e?.message ?? String(e) }
|
|
3894
|
-
};
|
|
3895
|
-
}
|
|
3896
|
-
}
|
|
3897
|
-
return {
|
|
3898
|
-
countries,
|
|
3899
|
-
isCountriesLoading,
|
|
3900
|
-
getCountries,
|
|
3901
|
-
searchCountries,
|
|
3902
|
-
getCountryByValue,
|
|
3903
|
-
getCountriesByDial,
|
|
3904
|
-
getRequiredInfo,
|
|
3905
|
-
validate
|
|
3906
|
-
};
|
|
3907
|
-
}
|
|
3908
|
-
//#endregion
|
|
3909
3407
|
//#region src/composables/useCountryDetection.ts
|
|
3910
3408
|
/**
|
|
3911
3409
|
* Best-effort country detection chain: IP geolocation → timezone → navigator.language → fallback.
|
|
@@ -4015,24 +3513,32 @@ function tryLocale() {
|
|
|
4015
3513
|
return null;
|
|
4016
3514
|
}
|
|
4017
3515
|
}
|
|
3516
|
+
const inflightIpFetch = /* @__PURE__ */ new Map();
|
|
4018
3517
|
async function tryIp(endpoint, timeoutMs) {
|
|
4019
3518
|
if (!isBrowser() || typeof fetch !== "function") return null;
|
|
3519
|
+
const existing = inflightIpFetch.get(endpoint);
|
|
3520
|
+
if (existing) return existing;
|
|
4020
3521
|
const controller = new AbortController();
|
|
4021
3522
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
3523
|
+
const promise = (async () => {
|
|
3524
|
+
try {
|
|
3525
|
+
const res = await fetch(endpoint, {
|
|
3526
|
+
signal: controller.signal,
|
|
3527
|
+
credentials: "omit"
|
|
3528
|
+
});
|
|
3529
|
+
if (!res.ok) return null;
|
|
3530
|
+
const data = await res.json();
|
|
3531
|
+
const code = (data.country_code ?? data.country ?? "").toString().toUpperCase();
|
|
3532
|
+
return /^[A-Z]{2}$/.test(code) ? code : null;
|
|
3533
|
+
} catch {
|
|
3534
|
+
return null;
|
|
3535
|
+
} finally {
|
|
3536
|
+
clearTimeout(timer);
|
|
3537
|
+
inflightIpFetch.delete(endpoint);
|
|
3538
|
+
}
|
|
3539
|
+
})();
|
|
3540
|
+
inflightIpFetch.set(endpoint, promise);
|
|
3541
|
+
return promise;
|
|
4036
3542
|
}
|
|
4037
3543
|
function readCache() {
|
|
4038
3544
|
if (!isBrowser()) return null;
|
|
@@ -4098,6 +3604,11 @@ function useCountryDetection(opts = {}) {
|
|
|
4098
3604
|
}
|
|
4099
3605
|
//#endregion
|
|
4100
3606
|
//#region src/composables/useCountryMatching.ts
|
|
3607
|
+
/** Cached snapshot of every country libphonenumber knows about (~250 ISO2 codes).
|
|
3608
|
+
* Used by tier 2 of `matchLeadingDialCode` as the last-resort iteration so detection
|
|
3609
|
+
* works for *every* country, not just the popular ones in the bundled fallback list.
|
|
3610
|
+
* Cached at module load — `getCountries()` is a static metadata table, no I/O. */
|
|
3611
|
+
const ALL_LIBPHONENUMBER_ISO2 = getCountries();
|
|
4101
3612
|
/** Synchronous dial-digit → ISO2 fallback for common countries, used when the async
|
|
4102
3613
|
* REST Countries fetch hasn't populated `getCountriesByDial`'s index yet at setup. */
|
|
4103
3614
|
const DIAL_TO_ISO2_FALLBACK = {
|
|
@@ -4164,6 +3675,58 @@ const DIAL_TO_ISO2_FALLBACK = {
|
|
|
4164
3675
|
/** localStorage key for the user's most recently picked countries. Used as a
|
|
4165
3676
|
* tie-breaker when multiple countries share a dial code (e.g. all NANP). */
|
|
4166
3677
|
const COUNTRY_RECENTS_KEY = "ali_ui_country_recents_v1";
|
|
3678
|
+
/** ISO2 codes iterated by tier 2 of `matchLeadingDialCode` when looking for a country
|
|
3679
|
+
* that accepts a local-format input as valid. Mirrors the `FALLBACK_COUNTRIES` list in
|
|
3680
|
+
* {@link usePhoneValidation} (kept in sync by tests + by being short and obvious).
|
|
3681
|
+
* Order matters — earlier entries get priority when multiple countries would each
|
|
3682
|
+
* validate the same input. Built around the most-populated / most-likely countries. */
|
|
3683
|
+
const FALLBACK_ISO2_LIST = [
|
|
3684
|
+
"SA",
|
|
3685
|
+
"EG",
|
|
3686
|
+
"AE",
|
|
3687
|
+
"US",
|
|
3688
|
+
"GB",
|
|
3689
|
+
"DE",
|
|
3690
|
+
"FR",
|
|
3691
|
+
"ES",
|
|
3692
|
+
"IT",
|
|
3693
|
+
"TR",
|
|
3694
|
+
"RU",
|
|
3695
|
+
"CN",
|
|
3696
|
+
"IN",
|
|
3697
|
+
"JP",
|
|
3698
|
+
"KR",
|
|
3699
|
+
"BR",
|
|
3700
|
+
"MX",
|
|
3701
|
+
"CA",
|
|
3702
|
+
"AU",
|
|
3703
|
+
"NG",
|
|
3704
|
+
"PK",
|
|
3705
|
+
"ID"
|
|
3706
|
+
];
|
|
3707
|
+
/** Build a minimal `CountryOption` from libphonenumber metadata when the async REST
|
|
3708
|
+
* Countries list hasn't loaded the entry yet. Used so country **detection** works
|
|
3709
|
+
* generically for any libphonenumber country, not just the ~22 in the offline
|
|
3710
|
+
* fallback list. The picker will overwrite this synthetic record with the real one
|
|
3711
|
+
* (with localized name + flag) as soon as `getCountries()` resolves. */
|
|
3712
|
+
function buildSyntheticCountry(iso2, dialDigits) {
|
|
3713
|
+
const ISO2 = iso2.toUpperCase();
|
|
3714
|
+
const digits = String(dialDigits).replace(/\D/g, "");
|
|
3715
|
+
return {
|
|
3716
|
+
label: `${ISO2} (+${digits})`,
|
|
3717
|
+
value: ISO2,
|
|
3718
|
+
search_key: `${ISO2.toLowerCase()} +${digits} ${digits}`,
|
|
3719
|
+
raw_data: {
|
|
3720
|
+
iso2: ISO2,
|
|
3721
|
+
dial_code: `+${digits}`,
|
|
3722
|
+
dial_digits: digits,
|
|
3723
|
+
name: ISO2,
|
|
3724
|
+
flag: `https://flagcdn.com/w40/${ISO2.toLowerCase()}.png`,
|
|
3725
|
+
source: "fallback",
|
|
3726
|
+
original: {}
|
|
3727
|
+
}
|
|
3728
|
+
};
|
|
3729
|
+
}
|
|
4167
3730
|
function readRecents() {
|
|
4168
3731
|
if (typeof window === "undefined") return [];
|
|
4169
3732
|
try {
|
|
@@ -4209,38 +3772,75 @@ function useCountryMatching(deps) {
|
|
|
4209
3772
|
const n = Number(digits);
|
|
4210
3773
|
return Number.isFinite(n) ? n : null;
|
|
4211
3774
|
}
|
|
3775
|
+
const MATCHER_CACHE_MAX = 128;
|
|
3776
|
+
const matcherCache = /* @__PURE__ */ new Map();
|
|
3777
|
+
function readMatcherCache(key) {
|
|
3778
|
+
if (!matcherCache.has(key)) return void 0;
|
|
3779
|
+
const value = matcherCache.get(key);
|
|
3780
|
+
matcherCache.delete(key);
|
|
3781
|
+
matcherCache.set(key, value);
|
|
3782
|
+
return value;
|
|
3783
|
+
}
|
|
3784
|
+
function writeMatcherCache(key, value) {
|
|
3785
|
+
if (matcherCache.size >= MATCHER_CACHE_MAX) {
|
|
3786
|
+
const oldest = matcherCache.keys().next().value;
|
|
3787
|
+
if (oldest !== void 0) matcherCache.delete(oldest);
|
|
3788
|
+
}
|
|
3789
|
+
matcherCache.set(key, value);
|
|
3790
|
+
}
|
|
4212
3791
|
/** Three-tier match of the leading digits to a country:
|
|
4213
3792
|
* 1. libphonenumber international parse (handles NANP disambiguation).
|
|
4214
|
-
* 2. libphonenumber national-format parse
|
|
4215
|
-
* formats like Egyptian `01066105963` with no
|
|
3793
|
+
* 2. libphonenumber national-format parse, iterating through candidate hint
|
|
3794
|
+
* countries (handles local formats like Egyptian `01066105963` with no
|
|
3795
|
+
* dial-code prefix). Universal coverage via `getCountries()`.
|
|
4216
3796
|
* 3. Longest-prefix match against the dial-digits index, with the current
|
|
4217
|
-
* selection / recents as tie-breakers when multiple countries share a code.
|
|
3797
|
+
* selection / recents as tie-breakers when multiple countries share a code.
|
|
3798
|
+
*
|
|
3799
|
+
* Results are LRU-cached per input + context to avoid re-paying tier-2 iteration
|
|
3800
|
+
* cost when the user backspaces and retypes the same prefix. */
|
|
4218
3801
|
function matchLeadingDialCode(digits, options = {}) {
|
|
4219
3802
|
if (!digits) return null;
|
|
4220
3803
|
const { hintCountry, currentIso2 } = options;
|
|
3804
|
+
const cacheKey = `${digits}|${hintCountry ?? ""}|${currentIso2 ?? ""}`;
|
|
3805
|
+
const cached = readMatcherCache(cacheKey);
|
|
3806
|
+
if (cached !== void 0) return cached;
|
|
3807
|
+
const result = runMatch(digits, hintCountry, currentIso2);
|
|
3808
|
+
writeMatcherCache(cacheKey, result);
|
|
3809
|
+
return result;
|
|
3810
|
+
}
|
|
3811
|
+
function runMatch(digits, hintCountry, currentIso2) {
|
|
4221
3812
|
try {
|
|
4222
3813
|
const parsed = parsePhoneNumberFromString(`+${digits}`);
|
|
4223
|
-
if (parsed?.country && parsed.countryCallingCode) {
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
nationalNumber: String(parsed.nationalNumber ?? "")
|
|
4228
|
-
};
|
|
4229
|
-
}
|
|
4230
|
-
} catch {}
|
|
4231
|
-
if (hintCountry && digits.length >= 4) try {
|
|
4232
|
-
const parsed = parsePhoneNumberFromString(digits, hintCountry);
|
|
4233
|
-
if (parsed?.isValid()) {
|
|
4234
|
-
const matched = getCountryByValue(parsed.country || hintCountry);
|
|
4235
|
-
if (matched) return {
|
|
4236
|
-
country: matched,
|
|
4237
|
-
nationalNumber: String(parsed.nationalNumber ?? "")
|
|
4238
|
-
};
|
|
4239
|
-
}
|
|
3814
|
+
if (parsed?.country && parsed.countryCallingCode) return {
|
|
3815
|
+
country: getCountryByValue(parsed.country) ?? buildSyntheticCountry(parsed.country, String(parsed.countryCallingCode)),
|
|
3816
|
+
nationalNumber: String(parsed.nationalNumber ?? "")
|
|
3817
|
+
};
|
|
4240
3818
|
} catch {}
|
|
3819
|
+
if (digits.length >= 4) {
|
|
3820
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
3821
|
+
if (hintCountry) candidates.add(hintCountry.toUpperCase());
|
|
3822
|
+
if (currentIso2) candidates.add(currentIso2.toUpperCase());
|
|
3823
|
+
for (const recent of readRecents()) candidates.add(recent.toUpperCase());
|
|
3824
|
+
for (const fallback of FALLBACK_ISO2_LIST) candidates.add(fallback);
|
|
3825
|
+
for (const all of ALL_LIBPHONENUMBER_ISO2) candidates.add(all);
|
|
3826
|
+
for (const iso2 of candidates) try {
|
|
3827
|
+
const parsed = parsePhoneNumberFromString(digits, iso2);
|
|
3828
|
+
if (parsed?.isValid()) {
|
|
3829
|
+
const resolvedIso2 = parsed.country || iso2;
|
|
3830
|
+
return {
|
|
3831
|
+
country: getCountryByValue(resolvedIso2) ?? buildSyntheticCountry(resolvedIso2, String(parsed.countryCallingCode ?? "")),
|
|
3832
|
+
nationalNumber: String(parsed.nationalNumber ?? "")
|
|
3833
|
+
};
|
|
3834
|
+
}
|
|
3835
|
+
} catch {}
|
|
3836
|
+
}
|
|
4241
3837
|
for (let len = Math.min(3, digits.length); len >= 1; len--) {
|
|
4242
3838
|
const prefix = digits.slice(0, len);
|
|
4243
|
-
|
|
3839
|
+
let group = getCountriesByDial(prefix);
|
|
3840
|
+
if (!group.length) {
|
|
3841
|
+
const iso2 = DIAL_TO_ISO2_FALLBACK[prefix];
|
|
3842
|
+
if (iso2) group = [getCountryByValue(iso2) ?? buildSyntheticCountry(iso2, prefix)];
|
|
3843
|
+
}
|
|
4244
3844
|
if (!group.length) continue;
|
|
4245
3845
|
const nationalNumber = digits.slice(prefix.length);
|
|
4246
3846
|
if (group.length === 1) return {
|
|
@@ -4312,18 +3912,36 @@ function useTelInputValidation(deps, inputs, config) {
|
|
|
4312
3912
|
phone: inputs.phone.value ?? "",
|
|
4313
3913
|
locale: config.locale()
|
|
4314
3914
|
}));
|
|
3915
|
+
const externalErrorActive = computed(() => {
|
|
3916
|
+
const e = config.externalError();
|
|
3917
|
+
return typeof e === "string" && e.length > 0;
|
|
3918
|
+
});
|
|
4315
3919
|
const validationState = computed(() => {
|
|
3920
|
+
if (externalErrorActive.value) return "error";
|
|
4316
3921
|
if (!inputs.phone.value) return "idle";
|
|
4317
3922
|
return validation.value.ok ? "valid" : "error";
|
|
4318
3923
|
});
|
|
4319
|
-
const visibleValidationState = computed(() =>
|
|
3924
|
+
const visibleValidationState = computed(() => {
|
|
3925
|
+
if (externalErrorActive.value) return "error";
|
|
3926
|
+
const mode = config.validateOn() ?? "change";
|
|
3927
|
+
if (mode === "eager") return validationState.value;
|
|
3928
|
+
if (mode === "blur" && !inputs.hasBlurred.value) return "idle";
|
|
3929
|
+
return inputs.hasFinishedTyping.value ? validationState.value : "idle";
|
|
3930
|
+
});
|
|
4320
3931
|
const errorMessage = computed(() => {
|
|
3932
|
+
const ext = config.externalError();
|
|
3933
|
+
if (typeof ext === "string" && ext.length > 0) return ext;
|
|
4321
3934
|
const v = validation.value;
|
|
4322
3935
|
if (v.ok || !v.reason) return null;
|
|
4323
3936
|
if (!inputs.phone.value) return null;
|
|
4324
3937
|
return config.errorMessages()?.[v.reason] ?? inputs.messages.value.errorMessages[v.reason];
|
|
4325
3938
|
});
|
|
4326
|
-
const showError = computed(() =>
|
|
3939
|
+
const showError = computed(() => {
|
|
3940
|
+
if (!errorMessage.value) return false;
|
|
3941
|
+
if (externalErrorActive.value) return true;
|
|
3942
|
+
if (!config.showValidation()) return false;
|
|
3943
|
+
return visibleValidationState.value === "error";
|
|
3944
|
+
});
|
|
4327
3945
|
return {
|
|
4328
3946
|
validation,
|
|
4329
3947
|
required,
|
|
@@ -4339,54 +3957,88 @@ function useTelInputValidation(deps, inputs, config) {
|
|
|
4339
3957
|
};
|
|
4340
3958
|
}
|
|
4341
3959
|
//#endregion
|
|
4342
|
-
//#region src/
|
|
4343
|
-
const aTelInputVariants = cva("a-tel-input__field", {
|
|
4344
|
-
variants: { size: {
|
|
4345
|
-
xs: "",
|
|
4346
|
-
sm: "",
|
|
4347
|
-
md: "",
|
|
4348
|
-
lg: "",
|
|
4349
|
-
xl: ""
|
|
4350
|
-
} },
|
|
4351
|
-
defaultVariants: { size: "md" }
|
|
4352
|
-
});
|
|
4353
|
-
const DEFAULT_ERROR_MESSAGES = {
|
|
4354
|
-
missing_country: "Please select a country.",
|
|
4355
|
-
country_not_supported: "This country is not supported.",
|
|
4356
|
-
phone_has_non_digits: "Phone number can only contain digits.",
|
|
4357
|
-
too_short: "Phone number is too short.",
|
|
4358
|
-
too_long: "Phone number is too long.",
|
|
4359
|
-
invalid_phone: "Phone number is invalid.",
|
|
4360
|
-
parse_failed: "Could not parse phone number."
|
|
4361
|
-
};
|
|
4362
|
-
/** English defaults for every {@link TelInputMessages} key. */
|
|
4363
|
-
const DEFAULT_MESSAGES = {
|
|
4364
|
-
searchPlaceholder: "Search country or +code…",
|
|
4365
|
-
emptyText: "No countries found.",
|
|
4366
|
-
loadingText: "Loading countries…",
|
|
4367
|
-
suggestedLabel: "Suggested",
|
|
4368
|
-
allCountriesLabel: "All countries",
|
|
4369
|
-
errorMessages: DEFAULT_ERROR_MESSAGES,
|
|
4370
|
-
countryLabel: "Country",
|
|
4371
|
-
selectCountryLabel: "Select country",
|
|
4372
|
-
phoneInputLabel: "Phone number"
|
|
4373
|
-
};
|
|
3960
|
+
//#region src/composables/useCountrySelection.ts
|
|
4374
3961
|
/**
|
|
4375
|
-
*
|
|
4376
|
-
*
|
|
3962
|
+
* The picker selection state machine for {@link ATelInput}, consolidated into a
|
|
3963
|
+
* single composable so the component doesn't have to juggle three boolean flags
|
|
3964
|
+
* (`userPickedCountry` / `autoSettingCountry` / `inputDetectionApplied`) and
|
|
3965
|
+
* reason about their pairwise interactions.
|
|
3966
|
+
*
|
|
3967
|
+
* Every write to the selection goes through {@link UseCountrySelectionReturn.set},
|
|
3968
|
+
* which records both the new ISO2 and the *origin* of the change. That makes the
|
|
3969
|
+
* downstream decision — should detection re-route the picker on the next typed-input
|
|
3970
|
+
* burst? — a one-liner: `if (detectionLocked.value) return;`.
|
|
4377
3971
|
*/
|
|
4378
|
-
function
|
|
4379
|
-
|
|
3972
|
+
function useCountrySelection() {
|
|
3973
|
+
const iso2 = ref("");
|
|
3974
|
+
const source = ref("none");
|
|
3975
|
+
function set(nextIso2, nextSource) {
|
|
3976
|
+
iso2.value = nextIso2;
|
|
3977
|
+
source.value = nextSource;
|
|
3978
|
+
}
|
|
3979
|
+
function clear() {
|
|
3980
|
+
iso2.value = "";
|
|
3981
|
+
source.value = "none";
|
|
3982
|
+
}
|
|
4380
3983
|
return {
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
}
|
|
3984
|
+
iso2,
|
|
3985
|
+
source,
|
|
3986
|
+
set,
|
|
3987
|
+
clear,
|
|
3988
|
+
detectionLocked: computed(() => source.value === "picker" || source.value === "input")
|
|
4387
3989
|
};
|
|
4388
3990
|
}
|
|
4389
3991
|
//#endregion
|
|
3992
|
+
//#region src/composables/useSyncedModel.ts
|
|
3993
|
+
/**
|
|
3994
|
+
* Two-way bidirectional sync between a `defineModel` ref and internal component
|
|
3995
|
+
* state — with the **echo-loop guard** built in. Solves a recurring class of
|
|
3996
|
+
* bugs in this component where two watchers (external→internal and
|
|
3997
|
+
* internal→external) would fight each other and rewrite values the user just
|
|
3998
|
+
* typed.
|
|
3999
|
+
*
|
|
4000
|
+
* Mechanics:
|
|
4001
|
+
*
|
|
4002
|
+
* 1. When any of `triggers` change AND we're not currently applying an
|
|
4003
|
+
* external write, recompute the model value via `compose()` and write it
|
|
4004
|
+
* into `model`. Stamp `lastEmitted` first so we recognise the echo.
|
|
4005
|
+
* 2. When `model` changes AND the new value isn't the echo of our last emit,
|
|
4006
|
+
* apply it into internal state via `apply()`. The `applying` flag is held
|
|
4007
|
+
* for the duration of `apply()` so step (1) skips while we mutate.
|
|
4008
|
+
*
|
|
4009
|
+
* Used for:
|
|
4010
|
+
* - `modelValue` (E.164 string) ↔ `phone` + `selectedIso2`.
|
|
4011
|
+
* - `country` (dial-number) ↔ `selectedIso2`.
|
|
4012
|
+
*
|
|
4013
|
+
* The hand-rolled equivalents (`applyingModelValue` / `lastEmittedModelValue`
|
|
4014
|
+
* plus the country↔iso2 watcher pair with `autoSettingCountry`) collapse into
|
|
4015
|
+
* two calls to this helper.
|
|
4016
|
+
*/
|
|
4017
|
+
function useSyncedModel(options) {
|
|
4018
|
+
const { model, triggers, compose, apply } = options;
|
|
4019
|
+
const isEqual = options.isEqual ?? Object.is;
|
|
4020
|
+
let applying = false;
|
|
4021
|
+
let lastEmitted = { __unset: true };
|
|
4022
|
+
const isEcho = (v) => typeof lastEmitted === "object" && lastEmitted !== null && "__unset" in lastEmitted ? false : isEqual(v, lastEmitted);
|
|
4023
|
+
watch(model, (next) => {
|
|
4024
|
+
if (isEcho(next)) return;
|
|
4025
|
+
applying = true;
|
|
4026
|
+
try {
|
|
4027
|
+
apply(next);
|
|
4028
|
+
} finally {
|
|
4029
|
+
applying = false;
|
|
4030
|
+
}
|
|
4031
|
+
}, { immediate: true });
|
|
4032
|
+
watch(triggers, () => {
|
|
4033
|
+
if (applying) return;
|
|
4034
|
+
const next = compose();
|
|
4035
|
+
if (!isEqual(next, model.value)) {
|
|
4036
|
+
lastEmitted = next;
|
|
4037
|
+
model.value = next;
|
|
4038
|
+
}
|
|
4039
|
+
}, { flush: "post" });
|
|
4040
|
+
}
|
|
4041
|
+
//#endregion
|
|
4390
4042
|
//#region ../AResponsivePopover/dist/index.js
|
|
4391
4043
|
var __defProp = Object.defineProperty;
|
|
4392
4044
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -17116,7 +16768,7 @@ const _sfc_main$9 = /* @__PURE__ */ defineComponent({
|
|
|
17116
16768
|
if (typeof document === "undefined") return [];
|
|
17117
16769
|
return Array.from(document.querySelectorAll("[data-responsive-popover-scroll-container=\"true\"]"));
|
|
17118
16770
|
},
|
|
17119
|
-
active: computed(() => !!ctx?.open.value &&
|
|
16771
|
+
active: computed(() => !!ctx?.open.value && scrollLockMode.value === "events")
|
|
17120
16772
|
});
|
|
17121
16773
|
const __returned__ = {
|
|
17122
16774
|
props,
|
|
@@ -17162,7 +16814,8 @@ function _sfc_render$9(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
17162
16814
|
])) : (openBlock(), createBlock($setup["ADrawerContent"], {
|
|
17163
16815
|
key: 1,
|
|
17164
16816
|
class: normalizeClass($setup.mergedClass),
|
|
17165
|
-
"data-slot": "responsive-popover-content"
|
|
16817
|
+
"data-slot": "responsive-popover-content",
|
|
16818
|
+
"data-responsive-popover-scroll-container": "true"
|
|
17166
16819
|
}, {
|
|
17167
16820
|
default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
|
|
17168
16821
|
_: 3
|
|
@@ -17821,7 +17474,7 @@ const _hoisted_6$1 = {
|
|
|
17821
17474
|
};
|
|
17822
17475
|
const _hoisted_7$1 = { class: "a-country-select__list" };
|
|
17823
17476
|
const _hoisted_8$1 = { class: "a-country-select__loading" };
|
|
17824
|
-
const _hoisted_9 = { class: "a-country-select__empty" };
|
|
17477
|
+
const _hoisted_9$1 = { class: "a-country-select__empty" };
|
|
17825
17478
|
const _hoisted_10 = {
|
|
17826
17479
|
key: 0,
|
|
17827
17480
|
"data-slot": "country-select-group",
|
|
@@ -17919,7 +17572,7 @@ function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
17919
17572
|
createElementVNode("div", _hoisted_7$1, [$setup.isCountriesLoading && $setup.effectiveCountries.length === 0 ? renderSlot(_ctx.$slots, "loading", { key: 0 }, () => [createElementVNode("div", _hoisted_8$1, toDisplayString($setup.props.loadingText), 1)], true) : $setup.isSearching && $setup.filtered.length === 0 ? renderSlot(_ctx.$slots, "empty", {
|
|
17920
17573
|
key: 1,
|
|
17921
17574
|
query: $setup.search
|
|
17922
|
-
}, () => [createElementVNode("div", _hoisted_9, toDisplayString($setup.props.emptyText), 1)], true) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [
|
|
17575
|
+
}, () => [createElementVNode("div", _hoisted_9$1, toDisplayString($setup.props.emptyText), 1)], true) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [
|
|
17923
17576
|
createCommentVNode(" Suggested group "),
|
|
17924
17577
|
$setup.suggested.length > 0 ? (openBlock(), createElementBlock("section", _hoisted_10, [renderSlot(_ctx.$slots, "group-header", {
|
|
17925
17578
|
label: $setup.props.suggestedLabel,
|
|
@@ -18048,6 +17701,27 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18048
17701
|
required: false,
|
|
18049
17702
|
skipCheck: true
|
|
18050
17703
|
},
|
|
17704
|
+
modelValue: {
|
|
17705
|
+
type: String,
|
|
17706
|
+
required: false
|
|
17707
|
+
},
|
|
17708
|
+
name: {
|
|
17709
|
+
type: String,
|
|
17710
|
+
required: false
|
|
17711
|
+
},
|
|
17712
|
+
error: {
|
|
17713
|
+
type: [String, null],
|
|
17714
|
+
required: false
|
|
17715
|
+
},
|
|
17716
|
+
validating: {
|
|
17717
|
+
type: Boolean,
|
|
17718
|
+
required: false
|
|
17719
|
+
},
|
|
17720
|
+
validateOn: {
|
|
17721
|
+
type: String,
|
|
17722
|
+
required: false,
|
|
17723
|
+
default: "change"
|
|
17724
|
+
},
|
|
18051
17725
|
placeholder: {
|
|
18052
17726
|
type: String,
|
|
18053
17727
|
required: false,
|
|
@@ -18190,28 +17864,65 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18190
17864
|
type: [Number, null],
|
|
18191
17865
|
default: null
|
|
18192
17866
|
},
|
|
18193
|
-
"countryModifiers": {}
|
|
17867
|
+
"countryModifiers": {},
|
|
17868
|
+
"modelValue": {
|
|
17869
|
+
type: String,
|
|
17870
|
+
default: ""
|
|
17871
|
+
},
|
|
17872
|
+
"modelModifiers": {}
|
|
18194
17873
|
}),
|
|
18195
|
-
emits: [
|
|
18196
|
-
|
|
17874
|
+
emits: /* @__PURE__ */ mergeModels([
|
|
17875
|
+
"update:modelValue",
|
|
17876
|
+
"update:phone",
|
|
17877
|
+
"update:country",
|
|
17878
|
+
"blur",
|
|
17879
|
+
"focus"
|
|
17880
|
+
], [
|
|
17881
|
+
"update:phone",
|
|
17882
|
+
"update:country",
|
|
17883
|
+
"update:modelValue"
|
|
17884
|
+
]),
|
|
17885
|
+
setup(__props, { expose: __expose, emit: __emit }) {
|
|
18197
17886
|
const props = __props;
|
|
17887
|
+
const emit = __emit;
|
|
18198
17888
|
const phone = useModel(__props, "phone");
|
|
18199
17889
|
/** Public `v-model:country` — the **dial number** (e.g. `20` for Egypt, `44` for the UK,
|
|
18200
17890
|
* `1` for the NANP block). `null` means no country selected. Internally the component
|
|
18201
17891
|
* tracks a richer ISO2 code (`selectedIso2`) because dial codes alone can't disambiguate
|
|
18202
17892
|
* NANP (`+1` covers 25+ countries) — the picker still needs an exact country. */
|
|
18203
17893
|
const country = useModel(__props, "country");
|
|
18204
|
-
/**
|
|
18205
|
-
*
|
|
18206
|
-
|
|
17894
|
+
/**
|
|
17895
|
+
* Default v-model — the canonical **E.164** string (e.g. `'+201066105963'`).
|
|
17896
|
+
*
|
|
17897
|
+
* Single-string contract for VeeValidate's `<Field v-slot="{ field }">` pattern
|
|
17898
|
+
* (`v-bind="field"`), native `<form>` submission, or any `v-model="phoneE164"`
|
|
17899
|
+
* consumer. Bind it with:
|
|
17900
|
+
*
|
|
17901
|
+
* <ATelInput v-model="phoneE164" />
|
|
17902
|
+
*
|
|
17903
|
+
* <VeeField v-slot="{ field, errors }" name="phone">
|
|
17904
|
+
* <ATelInput v-bind="field" :error="errors[0]" />
|
|
17905
|
+
* </VeeField>
|
|
17906
|
+
*
|
|
17907
|
+
* When set externally, the value is parsed via libphonenumber-js → the country
|
|
17908
|
+
* picker and the digits-only `phone` model are derived from it. When the user
|
|
17909
|
+
* types or picks a country, the composed E.164 is written back out. Stays in
|
|
17910
|
+
* sync with `v-model:phone` / `v-model:country` — you can use either contract.
|
|
17911
|
+
*/
|
|
17912
|
+
const modelValue = useModel(__props, "modelValue");
|
|
17913
|
+
/** The picker selection state machine — `iso2` is the internal source of truth, `source`
|
|
17914
|
+
* records where the current selection came from, `detectionLocked` answers "should
|
|
17915
|
+
* typed-input detection re-route the picker on the next burst?". Single mutator: `set`.
|
|
17916
|
+
* Replaces the historical flag soup (`userPickedCountry` / `autoSettingCountry` /
|
|
17917
|
+
* `inputDetectionApplied`). */
|
|
17918
|
+
const selection = useCountrySelection();
|
|
17919
|
+
const selectedIso2 = selection.iso2;
|
|
18207
17920
|
const { getCountries, validate, getRequiredInfo, getCountryByValue, getCountriesByDial } = usePhoneValidation();
|
|
18208
17921
|
const { resolveCountryIdentifier, dialNumberFor, matchLeadingDialCode } = useCountryMatching({
|
|
18209
17922
|
getCountryByValue,
|
|
18210
17923
|
getCountriesByDial
|
|
18211
17924
|
});
|
|
18212
17925
|
getCountries();
|
|
18213
|
-
const userPickedCountry = ref(false);
|
|
18214
|
-
const autoSettingCountry = ref(false);
|
|
18215
17926
|
/** Silently resolved via IP/timezone/locale when `detectFromInput` is on — used as a hint
|
|
18216
17927
|
* so local-format numbers (e.g. Egyptian `01066105963`) can be parsed without a `+` prefix.
|
|
18217
17928
|
* Seeded from `defaultCountry` so it has a usable value before async detection resolves. */
|
|
@@ -18225,18 +17936,28 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18225
17936
|
currentIso2: selectedIso2.value
|
|
18226
17937
|
});
|
|
18227
17938
|
}
|
|
17939
|
+
/** User explicitly picked a country from the picker — locks the selection so subsequent
|
|
17940
|
+
* typed-input detection cannot churn the picker. */
|
|
17941
|
+
function onPickerPick(iso2) {
|
|
17942
|
+
selection.set(iso2, "picker");
|
|
17943
|
+
}
|
|
18228
17944
|
const typing = useTypingPhase({
|
|
18229
17945
|
debounceMs: computed(() => Math.max(0, props.detectDebounceMs)),
|
|
18230
17946
|
onSettle: () => {
|
|
18231
17947
|
if (!props.detectFromInput) return;
|
|
18232
|
-
if (
|
|
17948
|
+
if (selection.detectionLocked.value) return;
|
|
18233
17949
|
const current = phone.value;
|
|
18234
17950
|
if (!current) return;
|
|
17951
|
+
const typedInternational = (displayValue.value ?? "").trimStart().startsWith("+");
|
|
17952
|
+
if (selectedIso2.value && !typedInternational) return;
|
|
18235
17953
|
typing.markDetectionAttempt();
|
|
18236
17954
|
const match = tryMatchPhone(current);
|
|
18237
17955
|
if (!match) return;
|
|
18238
|
-
|
|
18239
|
-
|
|
17956
|
+
if (match.country.value === selectedIso2.value && match.nationalNumber === phone.value) {
|
|
17957
|
+
selection.source.value = "input";
|
|
17958
|
+
return;
|
|
17959
|
+
}
|
|
17960
|
+
selection.set(match.country.value, "input");
|
|
18240
17961
|
phone.value = match.nationalNumber;
|
|
18241
17962
|
}
|
|
18242
17963
|
});
|
|
@@ -18247,8 +17968,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18247
17968
|
const seed = resolveCountryIdentifier(props.defaultCountry);
|
|
18248
17969
|
if (seed) {
|
|
18249
17970
|
inferredCountry.value = seed;
|
|
18250
|
-
|
|
18251
|
-
selectedIso2.value = seed;
|
|
17971
|
+
selection.set(seed, "default");
|
|
18252
17972
|
return;
|
|
18253
17973
|
}
|
|
18254
17974
|
}
|
|
@@ -18267,43 +17987,31 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18267
17987
|
const iso2 = detected ? detected.toUpperCase() : "";
|
|
18268
17988
|
if (props.detectFromInput) {
|
|
18269
17989
|
inferredCountry.value = iso2;
|
|
18270
|
-
if (phone.value && !
|
|
17990
|
+
if (phone.value && !selection.detectionLocked.value && !selectedIso2.value) {
|
|
18271
17991
|
const match = tryMatchPhone(phone.value);
|
|
18272
17992
|
if (match) {
|
|
18273
|
-
|
|
18274
|
-
selectedIso2.value = match.country.value;
|
|
17993
|
+
selection.set(match.country.value, "input");
|
|
18275
17994
|
phone.value = match.nationalNumber;
|
|
18276
17995
|
}
|
|
18277
17996
|
}
|
|
18278
17997
|
return;
|
|
18279
17998
|
}
|
|
18280
|
-
if (!selectedIso2.value && iso2)
|
|
18281
|
-
autoSettingCountry.value = true;
|
|
18282
|
-
selectedIso2.value = iso2;
|
|
18283
|
-
}
|
|
17999
|
+
if (!selectedIso2.value && iso2) selection.set(iso2, "env");
|
|
18284
18000
|
});
|
|
18285
|
-
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
|
|
18289
|
-
|
|
18290
|
-
if (
|
|
18291
|
-
|
|
18001
|
+
useSyncedModel({
|
|
18002
|
+
model: country,
|
|
18003
|
+
triggers: [selectedIso2],
|
|
18004
|
+
compose: () => selectedIso2.value ? dialNumberFor(selectedIso2.value) : null,
|
|
18005
|
+
apply: (next) => {
|
|
18006
|
+
if (next == null) {
|
|
18007
|
+
selection.clear();
|
|
18008
|
+
return;
|
|
18009
|
+
}
|
|
18010
|
+
if (dialNumberFor(selectedIso2.value) === next) return;
|
|
18011
|
+
const iso2 = resolveCountryIdentifier(String(next));
|
|
18012
|
+
if (iso2) selection.set(iso2, "external");
|
|
18292
18013
|
}
|
|
18293
|
-
|
|
18294
|
-
const iso2 = resolveCountryIdentifier(String(next));
|
|
18295
|
-
if (iso2) selectedIso2.value = iso2;
|
|
18296
|
-
}, { immediate: true });
|
|
18297
|
-
/** Internal → external: keep `country` (dial number) in lockstep with `selectedIso2`, and
|
|
18298
|
-
* flag "user manually picked from picker" when the change isn't one we initiated.
|
|
18299
|
-
* `flush: 'sync'` so the `autoSettingCountry` guard is reliable. */
|
|
18300
|
-
watch(selectedIso2, (iso2, prev) => {
|
|
18301
|
-
const wasAutoSet = autoSettingCountry.value;
|
|
18302
|
-
autoSettingCountry.value = false;
|
|
18303
|
-
const nextDial = dialNumberFor(iso2);
|
|
18304
|
-
if (country.value !== nextDial) country.value = nextDial;
|
|
18305
|
-
if (!wasAutoSet && props.detectFromInput && iso2 && prev !== iso2) userPickedCountry.value = true;
|
|
18306
|
-
}, { flush: "sync" });
|
|
18014
|
+
});
|
|
18307
18015
|
/** The string shown in the `<input>`. Deliberately decoupled from `phone` (the digits-only
|
|
18308
18016
|
* model) so the visible field is NOT rewritten mid-edit — non-digits / alternative numerals
|
|
18309
18017
|
* are normalized into `phone` immediately, but the displayed value is only cleaned up once
|
|
@@ -18312,6 +18020,29 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18312
18020
|
/** Set when the in-flight `phone` change came from the user typing — tells the `phone`
|
|
18313
18021
|
* watcher to leave `displayValue` alone (the user is still editing it). */
|
|
18314
18022
|
let phoneEditedByInput = false;
|
|
18023
|
+
useSyncedModel({
|
|
18024
|
+
model: modelValue,
|
|
18025
|
+
triggers: [phone, selectedIso2],
|
|
18026
|
+
compose: () => {
|
|
18027
|
+
if (!selectedIso2.value || !phone.value) return "";
|
|
18028
|
+
return validate({
|
|
18029
|
+
country: { iso2: selectedIso2.value },
|
|
18030
|
+
phone: phone.value
|
|
18031
|
+
}).full_phone ?? "";
|
|
18032
|
+
},
|
|
18033
|
+
apply: (next) => {
|
|
18034
|
+
const trimmed = String(next ?? "").trim();
|
|
18035
|
+
if (!trimmed) {
|
|
18036
|
+
if (phone.value !== "") phone.value = "";
|
|
18037
|
+
if (selectedIso2.value !== "") selection.clear();
|
|
18038
|
+
return;
|
|
18039
|
+
}
|
|
18040
|
+
const parsed = parsePhoneNumberFromString(trimmed.startsWith("+") ? trimmed : `+${trimmed.replace(/^\+/, "")}`);
|
|
18041
|
+
if (!parsed || !parsed.country) return;
|
|
18042
|
+
if (selectedIso2.value !== parsed.country) selection.set(parsed.country, "external");
|
|
18043
|
+
if (phone.value !== parsed.nationalNumber) phone.value = parsed.nationalNumber;
|
|
18044
|
+
}
|
|
18045
|
+
});
|
|
18315
18046
|
function commitPhone(value) {
|
|
18316
18047
|
phoneEditedByInput = true;
|
|
18317
18048
|
phone.value = value;
|
|
@@ -18322,11 +18053,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18322
18053
|
const cleaned = normalizeDigits(target.value).replace(/\D/g, "");
|
|
18323
18054
|
if (!cleaned) {
|
|
18324
18055
|
typing.reset();
|
|
18325
|
-
if (props.detectFromInput)
|
|
18326
|
-
autoSettingCountry.value = true;
|
|
18327
|
-
selectedIso2.value = "";
|
|
18328
|
-
userPickedCountry.value = false;
|
|
18329
|
-
}
|
|
18056
|
+
if (props.detectFromInput) selection.clear();
|
|
18330
18057
|
commitPhone("");
|
|
18331
18058
|
return;
|
|
18332
18059
|
}
|
|
@@ -18359,6 +18086,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18359
18086
|
* `undefined` so it inherits from the page. The field row itself is always LTR so the
|
|
18360
18087
|
* dial prefix / digits / flag trigger keep a consistent order. */
|
|
18361
18088
|
const dirAttr = computed(() => props.dir === "ltr" || props.dir === "rtl" ? props.dir : void 0);
|
|
18089
|
+
/** Set to `true` the first time the input is blurred. Drives `validateOn: 'blur'`. */
|
|
18090
|
+
const hasBlurred = ref(false);
|
|
18362
18091
|
const { validation, required, validationState, visibleValidationState, errorMessage, showError, showHint, selectedDialCode } = useTelInputValidation({
|
|
18363
18092
|
validate,
|
|
18364
18093
|
getRequiredInfo,
|
|
@@ -18367,15 +18096,35 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18367
18096
|
phone,
|
|
18368
18097
|
selectedIso2,
|
|
18369
18098
|
hasFinishedTyping,
|
|
18099
|
+
hasBlurred,
|
|
18370
18100
|
messages
|
|
18371
18101
|
}, {
|
|
18372
18102
|
locale: () => props.locale,
|
|
18373
18103
|
showValidation: () => props.showValidation,
|
|
18374
|
-
errorMessages: () => props.errorMessages
|
|
18104
|
+
errorMessages: () => props.errorMessages,
|
|
18105
|
+
validateOn: () => props.validateOn,
|
|
18106
|
+
externalError: () => props.error
|
|
18375
18107
|
});
|
|
18376
18108
|
const effectivePlaceholder = computed(() => props.placeholder || required.value?.format_hint || messages.value.phoneInputLabel);
|
|
18377
18109
|
const helperId = useId();
|
|
18378
18110
|
const describedBy = computed(() => showError.value || showHint.value ? helperId : void 0);
|
|
18111
|
+
const inputRef = ref(null);
|
|
18112
|
+
function handleBlur(e) {
|
|
18113
|
+
hasBlurred.value = true;
|
|
18114
|
+
emit("blur", e);
|
|
18115
|
+
}
|
|
18116
|
+
function handleFocus(e) {
|
|
18117
|
+
emit("focus", e);
|
|
18118
|
+
}
|
|
18119
|
+
function focus(options) {
|
|
18120
|
+
inputRef.value?.focus(options);
|
|
18121
|
+
}
|
|
18122
|
+
function blur() {
|
|
18123
|
+
inputRef.value?.blur();
|
|
18124
|
+
}
|
|
18125
|
+
function select() {
|
|
18126
|
+
inputRef.value?.select();
|
|
18127
|
+
}
|
|
18379
18128
|
__expose({
|
|
18380
18129
|
validation,
|
|
18381
18130
|
required,
|
|
@@ -18384,12 +18133,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18384
18133
|
visibleValidationState,
|
|
18385
18134
|
isDetecting,
|
|
18386
18135
|
hasFinishedTyping,
|
|
18387
|
-
detectionAttempted
|
|
18136
|
+
detectionAttempted,
|
|
18137
|
+
focus,
|
|
18138
|
+
blur,
|
|
18139
|
+
select
|
|
18388
18140
|
});
|
|
18389
18141
|
const __returned__ = {
|
|
18390
18142
|
props,
|
|
18143
|
+
emit,
|
|
18391
18144
|
phone,
|
|
18392
18145
|
country,
|
|
18146
|
+
modelValue,
|
|
18147
|
+
selection,
|
|
18393
18148
|
selectedIso2,
|
|
18394
18149
|
getCountries,
|
|
18395
18150
|
validate,
|
|
@@ -18399,10 +18154,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18399
18154
|
resolveCountryIdentifier,
|
|
18400
18155
|
dialNumberFor,
|
|
18401
18156
|
matchLeadingDialCode,
|
|
18402
|
-
userPickedCountry,
|
|
18403
|
-
autoSettingCountry,
|
|
18404
18157
|
inferredCountry,
|
|
18405
18158
|
tryMatchPhone,
|
|
18159
|
+
onPickerPick,
|
|
18406
18160
|
typing,
|
|
18407
18161
|
isDetecting,
|
|
18408
18162
|
hasFinishedTyping,
|
|
@@ -18419,6 +18173,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18419
18173
|
handlePhoneChange,
|
|
18420
18174
|
messages,
|
|
18421
18175
|
dirAttr,
|
|
18176
|
+
hasBlurred,
|
|
18422
18177
|
validation,
|
|
18423
18178
|
required,
|
|
18424
18179
|
validationState,
|
|
@@ -18430,6 +18185,12 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18430
18185
|
effectivePlaceholder,
|
|
18431
18186
|
helperId,
|
|
18432
18187
|
describedBy,
|
|
18188
|
+
inputRef,
|
|
18189
|
+
handleBlur,
|
|
18190
|
+
handleFocus,
|
|
18191
|
+
focus,
|
|
18192
|
+
blur,
|
|
18193
|
+
select,
|
|
18433
18194
|
get cn() {
|
|
18434
18195
|
return cn$2;
|
|
18435
18196
|
},
|
|
@@ -18471,25 +18232,34 @@ const _hoisted_4 = {
|
|
|
18471
18232
|
};
|
|
18472
18233
|
const _hoisted_5 = [
|
|
18473
18234
|
"value",
|
|
18235
|
+
"name",
|
|
18474
18236
|
"disabled",
|
|
18475
18237
|
"placeholder",
|
|
18476
18238
|
"aria-label",
|
|
18477
18239
|
"aria-invalid",
|
|
18478
18240
|
"aria-describedby",
|
|
18241
|
+
"aria-errormessage",
|
|
18242
|
+
"aria-busy",
|
|
18479
18243
|
"data-has-dial"
|
|
18480
18244
|
];
|
|
18481
18245
|
const _hoisted_6 = {
|
|
18246
|
+
key: 0,
|
|
18247
|
+
class: "a-tel-input__validating",
|
|
18248
|
+
"data-slot": "tel-input-validating",
|
|
18249
|
+
"aria-hidden": "true"
|
|
18250
|
+
};
|
|
18251
|
+
const _hoisted_7 = {
|
|
18482
18252
|
key: 0,
|
|
18483
18253
|
class: "a-tel-input__detecting",
|
|
18484
18254
|
"aria-hidden": "true",
|
|
18485
18255
|
"data-slot": "tel-input-detecting"
|
|
18486
18256
|
};
|
|
18487
|
-
const
|
|
18257
|
+
const _hoisted_8 = {
|
|
18488
18258
|
key: 0,
|
|
18489
18259
|
class: "a-tel-input__country-wrapper",
|
|
18490
18260
|
"data-slot": "tel-input-country-wrapper"
|
|
18491
18261
|
};
|
|
18492
|
-
const
|
|
18262
|
+
const _hoisted_9 = ["id"];
|
|
18493
18263
|
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
18494
18264
|
return openBlock(), createElementBlock("div", {
|
|
18495
18265
|
class: normalizeClass($setup.cn("a-tel-input", _ctx.$attrs.class)),
|
|
@@ -18507,31 +18277,41 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
18507
18277
|
renderSlot(_ctx.$slots, "prefix", {}, void 0, true),
|
|
18508
18278
|
$setup.selectedDialCode ? (openBlock(), createElementBlock("span", _hoisted_4, toDisplayString($setup.selectedDialCode), 1)) : createCommentVNode("v-if", true),
|
|
18509
18279
|
createElementVNode("input", {
|
|
18280
|
+
ref: "inputRef",
|
|
18510
18281
|
value: $setup.displayValue,
|
|
18511
18282
|
type: "tel",
|
|
18512
18283
|
inputmode: "numeric",
|
|
18513
18284
|
autocomplete: "tel",
|
|
18514
18285
|
dir: "ltr",
|
|
18515
18286
|
"data-slot": "tel-input-field",
|
|
18287
|
+
name: $setup.props.name,
|
|
18516
18288
|
disabled: $setup.props.disabled || $setup.props.loading,
|
|
18517
18289
|
placeholder: $setup.effectivePlaceholder,
|
|
18518
18290
|
"aria-label": $setup.messages.phoneInputLabel,
|
|
18519
18291
|
"aria-invalid": $setup.visibleValidationState === "error" || void 0,
|
|
18520
18292
|
"aria-describedby": $setup.describedBy,
|
|
18293
|
+
"aria-errormessage": $setup.visibleValidationState === "error" ? $setup.helperId : void 0,
|
|
18294
|
+
"aria-busy": $setup.props.validating || void 0,
|
|
18521
18295
|
class: normalizeClass($setup.cn("a-tel-input__input", $setup.props.inputClass)),
|
|
18522
18296
|
"data-has-dial": $setup.selectedDialCode ? "" : void 0,
|
|
18523
18297
|
onInput: $setup.handlePhoneInput,
|
|
18524
|
-
onChange: $setup.handlePhoneChange
|
|
18298
|
+
onChange: $setup.handlePhoneChange,
|
|
18299
|
+
onBlur: $setup.handleBlur,
|
|
18300
|
+
onFocus: $setup.handleFocus
|
|
18525
18301
|
}, null, 42, _hoisted_5),
|
|
18302
|
+
createCommentVNode(" Async-validation spinner (e.g. server-side \"phone exists?\" check). Independent\n of `isDetecting` (which is for country detection) so both can be shown without\n interfering. Lives next to the input and never disables it. "),
|
|
18303
|
+
createVNode(Transition, { name: "a-tell-detect" }, {
|
|
18304
|
+
default: withCtx(() => [$setup.props.validating ? (openBlock(), createElementBlock("div", _hoisted_6, [renderSlot(_ctx.$slots, "validating", {}, () => [createVNode($setup["SpinnerIcon"], { class: "a-tel-input__detecting-icon" })], true)])) : createCommentVNode("v-if", true)]),
|
|
18305
|
+
_: 3
|
|
18306
|
+
}),
|
|
18526
18307
|
createCommentVNode(" Detection-in-flight spinner — shown only during the first debounce window,\n before the picker has appeared. Once the picker is visible (success OR a failed\n attempt that revealed the empty picker) we stop re-flashing on every keystroke. "),
|
|
18527
18308
|
createVNode(Transition, { name: "a-tell-detect" }, {
|
|
18528
|
-
default: withCtx(() => [$setup.isDetecting && !$setup.selectedIso2 && !$setup.detectionAttempted ? (openBlock(), createElementBlock("div",
|
|
18309
|
+
default: withCtx(() => [$setup.isDetecting && !$setup.selectedIso2 && !$setup.detectionAttempted ? (openBlock(), createElementBlock("div", _hoisted_7, [renderSlot(_ctx.$slots, "detecting", {}, () => [createVNode($setup["SpinnerIcon"], { class: "a-tel-input__detecting-icon" })], true)])) : createCommentVNode("v-if", true)]),
|
|
18529
18310
|
_: 3
|
|
18530
18311
|
}),
|
|
18531
18312
|
createVNode(Transition, { name: "a-tell-country" }, {
|
|
18532
|
-
default: withCtx(() => [!$setup.props.detectFromInput || $setup.selectedIso2 || $setup.detectionAttempted ? (openBlock(), createElementBlock("div",
|
|
18313
|
+
default: withCtx(() => [!$setup.props.detectFromInput || $setup.selectedIso2 || $setup.detectionAttempted ? (openBlock(), createElementBlock("div", _hoisted_8, [createVNode($setup["ACountrySelect"], {
|
|
18533
18314
|
selected: $setup.selectedIso2,
|
|
18534
|
-
"onUpdate:selected": _cache[0] || (_cache[0] = ($event) => $setup.selectedIso2 = $event),
|
|
18535
18315
|
"allowed-dial-codes": $setup.props.allowedDialCodes,
|
|
18536
18316
|
disabled: $setup.props.disabled || $setup.props.loading,
|
|
18537
18317
|
size: $setup.props.size,
|
|
@@ -18542,6 +18322,7 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
18542
18322
|
"suggested-label": $setup.messages.suggestedLabel,
|
|
18543
18323
|
"all-countries-label": $setup.messages.allCountriesLabel,
|
|
18544
18324
|
"country-label": $setup.messages.countryLabel,
|
|
18325
|
+
"onUpdate:selected": $setup.onPickerPick,
|
|
18545
18326
|
"select-country-label": $setup.messages.selectCountryLabel,
|
|
18546
18327
|
"flag-url": $setup.props.flagUrl,
|
|
18547
18328
|
searcher: $setup.props.searcher,
|
|
@@ -18648,7 +18429,7 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
18648
18429
|
}, () => [createElementVNode("p", {
|
|
18649
18430
|
"data-slot": "tel-input-hint",
|
|
18650
18431
|
class: normalizeClass($setup.cn("a-tel-input__hint", $setup.props.hintClass))
|
|
18651
|
-
}, toDisplayString($setup.required.format_hint), 3)], true) : createCommentVNode("v-if", true)], 8,
|
|
18432
|
+
}, toDisplayString($setup.required.format_hint), 3)], true) : createCommentVNode("v-if", true)], 8, _hoisted_9)
|
|
18652
18433
|
], 10, _hoisted_1);
|
|
18653
18434
|
}
|
|
18654
18435
|
var ATelInput_default = /* @__PURE__ */ export_helper_default(_sfc_main, [
|
|
@@ -18657,6 +18438,6 @@ var ATelInput_default = /* @__PURE__ */ export_helper_default(_sfc_main, [
|
|
|
18657
18438
|
["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ATelInput/src/components/ATelInput.vue"]
|
|
18658
18439
|
]);
|
|
18659
18440
|
//#endregion
|
|
18660
|
-
export { ACountryFlag_default as ACountryFlag, ACountrySelect_default as ACountrySelect, ATelInput_default as ATelInput, COUNTRY_RECENTS_KEY, DEFAULT_ERROR_MESSAGES, DEFAULT_MESSAGES, DIAL_TO_ISO2_FALLBACK, LOCALE_DIGIT_RANGES, aTelInputVariants, defaultFlagUrl, detectCountry, localizeCountries, normalizeDigits, resolveMessages, useCountryDetection, useCountryMatching, usePhoneValidation, useTelInputValidation, useTypingPhase };
|
|
18441
|
+
export { ACountryFlag_default as ACountryFlag, ACountrySelect_default as ACountrySelect, ATelInput_default as ATelInput, COUNTRY_RECENTS_KEY, DEFAULT_ERROR_MESSAGES, DEFAULT_MESSAGES, DIAL_TO_ISO2_FALLBACK, FALLBACK_ISO2_LIST, LOCALE_DIGIT_RANGES, aTelInputVariants, defaultFlagUrl, detectCountry, localizeCountries, normalizeDigits, resolveMessages, useCountryDetection, useCountryMatching, useCountrySelection, usePhoneValidation, useSyncedModel, useTelInputValidation, useTypingPhase };
|
|
18661
18442
|
|
|
18662
18443
|
//# sourceMappingURL=index.js.map
|