@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.cjs
CHANGED
|
@@ -1,33 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var __defProp$1 = Object.defineProperty;
|
|
5
|
-
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames$1 = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __copyProps$1 = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames$1(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
-
key = keys[i];
|
|
12
|
-
if (!__hasOwnProp$1.call(to, key) && key !== except) __defProp$1(to, key, {
|
|
13
|
-
get: ((k) => from[k]).bind(null, key),
|
|
14
|
-
enumerable: !(desc = __getOwnPropDesc$1(from, key)) || desc.enumerable
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps$1(isNodeMode || !mod || !mod.__esModule ? __defProp$1(target, "default", {
|
|
20
|
-
value: mod,
|
|
21
|
-
enumerable: true
|
|
22
|
-
}) : target, mod));
|
|
23
|
-
//#endregion
|
|
2
|
+
const require_usePhoneValidation = require("./_chunks/usePhoneValidation.js");
|
|
3
|
+
const require_types = require("./_chunks/types.js");
|
|
24
4
|
let vue = require("vue");
|
|
25
|
-
vue = __toESM(vue, 1);
|
|
5
|
+
vue = require_usePhoneValidation.__toESM(vue, 1);
|
|
26
6
|
let libphonenumber_js = require("libphonenumber-js");
|
|
27
|
-
let libphonenumber_js_examples_mobile_json = require("libphonenumber-js/examples.mobile.json");
|
|
28
|
-
libphonenumber_js_examples_mobile_json = __toESM(libphonenumber_js_examples_mobile_json, 1);
|
|
29
7
|
let _vueuse_core = require("@vueuse/core");
|
|
30
|
-
let class_variance_authority = require("class-variance-authority");
|
|
31
8
|
let aria_hidden = require("aria-hidden");
|
|
32
9
|
//#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
33
10
|
function r$2(e) {
|
|
@@ -3428,508 +3405,6 @@ function cn$2(...inputs) {
|
|
|
3428
3405
|
return twMerge$2(clsx$2(inputs));
|
|
3429
3406
|
}
|
|
3430
3407
|
//#endregion
|
|
3431
|
-
//#region src/utils/digits.ts
|
|
3432
|
-
/**
|
|
3433
|
-
* Alternative-numeral support. Phone numbers are routinely entered with the digits of the
|
|
3434
|
-
* user's own script — Arabic-Indic, Persian/Urdu, Devanagari, Bengali. `libphonenumber-js`
|
|
3435
|
-
* and our own `\d` cleanup only understand ASCII `0-9`, so anything else silently becomes
|
|
3436
|
-
* an empty number. `normalizeDigits` folds those scripts down to ASCII before validation.
|
|
3437
|
-
*/
|
|
3438
|
-
/**
|
|
3439
|
-
* Base code points of contiguous decimal-digit blocks. Each block runs `base`‥`base+9`
|
|
3440
|
-
* for digit `0`‥`9`, so the ASCII digit is `codePoint - base`. Add a script by appending
|
|
3441
|
-
* one entry here.
|
|
3442
|
-
*/
|
|
3443
|
-
const LOCALE_DIGIT_RANGES = [
|
|
3444
|
-
{
|
|
3445
|
-
name: "arabic-indic",
|
|
3446
|
-
base: 1632
|
|
3447
|
-
},
|
|
3448
|
-
{
|
|
3449
|
-
name: "extended-arabic",
|
|
3450
|
-
base: 1776
|
|
3451
|
-
},
|
|
3452
|
-
{
|
|
3453
|
-
name: "devanagari",
|
|
3454
|
-
base: 2406
|
|
3455
|
-
},
|
|
3456
|
-
{
|
|
3457
|
-
name: "bengali",
|
|
3458
|
-
base: 2534
|
|
3459
|
-
}
|
|
3460
|
-
];
|
|
3461
|
-
/** Lookup of every non-ASCII digit code point → its ASCII character. */
|
|
3462
|
-
const DIGIT_MAP = (() => {
|
|
3463
|
-
const map = /* @__PURE__ */ new Map();
|
|
3464
|
-
for (const { base } of LOCALE_DIGIT_RANGES) for (let d = 0; d <= 9; d++) map.set(base + d, String(d));
|
|
3465
|
-
return map;
|
|
3466
|
-
})();
|
|
3467
|
-
/**
|
|
3468
|
-
* Replace any supported non-ASCII decimal digit with its ASCII equivalent. Every other
|
|
3469
|
-
* character (spaces, `+`, separators, letters) is left untouched — callers still run their
|
|
3470
|
-
* own `\D` cleanup afterwards.
|
|
3471
|
-
*/
|
|
3472
|
-
function normalizeDigits(input) {
|
|
3473
|
-
const str = String(input ?? "");
|
|
3474
|
-
let out = "";
|
|
3475
|
-
for (const ch of str) {
|
|
3476
|
-
const cp = ch.codePointAt(0);
|
|
3477
|
-
out += cp != null && DIGIT_MAP.get(cp) || ch;
|
|
3478
|
-
}
|
|
3479
|
-
return out;
|
|
3480
|
-
}
|
|
3481
|
-
//#endregion
|
|
3482
|
-
//#region src/composables/usePhoneValidation.ts
|
|
3483
|
-
/**
|
|
3484
|
-
* Country list + phone validation, framework-agnostic.
|
|
3485
|
-
*
|
|
3486
|
-
* Ported from the reference @pkgs/ui ATelInput composable with these cleanups:
|
|
3487
|
-
* - Drop Nuxt-only `process.client` checks → use plain `typeof window !== 'undefined'`.
|
|
3488
|
-
* - Drop Arabic default placeholder; let consumers pass their own.
|
|
3489
|
-
* - Expand the offline fallback list from 2 → ~20 of the most-populated countries.
|
|
3490
|
-
* - Keep REST Countries fetch + localStorage cache + libphonenumber-js examples + fast `search_key`.
|
|
3491
|
-
*/
|
|
3492
|
-
const STORAGE_KEY = "ali_ui_phone_countries_v1";
|
|
3493
|
-
const REST_COUNTRIES_URL = "https://restcountries.com/v3.1/all?fields=name,cca2,idd,flags";
|
|
3494
|
-
const EX = libphonenumber_js_examples_mobile_json.default;
|
|
3495
|
-
const isBrowser$1 = () => typeof window !== "undefined";
|
|
3496
|
-
function toDigits(v) {
|
|
3497
|
-
return normalizeDigits(String(v ?? "")).replace(/\D/g, "");
|
|
3498
|
-
}
|
|
3499
|
-
/**
|
|
3500
|
-
* Render an ASCII digit string in a locale's numeral system (e.g. `'ar'` → `٠-٩`).
|
|
3501
|
-
* Used only for display hints — falls back to ASCII if the locale is unknown.
|
|
3502
|
-
*/
|
|
3503
|
-
function localizeDigits(digits, locale) {
|
|
3504
|
-
if (!locale) return digits;
|
|
3505
|
-
try {
|
|
3506
|
-
const fmt = new Intl.NumberFormat(locale, { useGrouping: false });
|
|
3507
|
-
return digits.replace(/[0-9]/g, (d) => fmt.format(Number(d)));
|
|
3508
|
-
} catch {
|
|
3509
|
-
return digits;
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
function ensurePlusDial(dial) {
|
|
3513
|
-
const d = toDigits(dial);
|
|
3514
|
-
return d ? `+${d}` : "";
|
|
3515
|
-
}
|
|
3516
|
-
function normalizeIso2(iso2) {
|
|
3517
|
-
return String(iso2 ?? "").trim().toUpperCase();
|
|
3518
|
-
}
|
|
3519
|
-
function dropLeadingZeros(digits) {
|
|
3520
|
-
return String(digits ?? "").replace(/^0+/, "");
|
|
3521
|
-
}
|
|
3522
|
-
function buildFullE164(dial, digits) {
|
|
3523
|
-
const dialClean = ensurePlusDial(dial);
|
|
3524
|
-
const nsn = dropLeadingZeros(toDigits(digits));
|
|
3525
|
-
return dialClean && nsn ? `${dialClean}${nsn}` : null;
|
|
3526
|
-
}
|
|
3527
|
-
function inferLengthFromExample(national) {
|
|
3528
|
-
const d = toDigits(national);
|
|
3529
|
-
if (!d) return {
|
|
3530
|
-
min: null,
|
|
3531
|
-
max: null
|
|
3532
|
-
};
|
|
3533
|
-
const n = d.length;
|
|
3534
|
-
return {
|
|
3535
|
-
min: Math.max(4, n - 2),
|
|
3536
|
-
max: n + 2
|
|
3537
|
-
};
|
|
3538
|
-
}
|
|
3539
|
-
function buildDialCode(idd) {
|
|
3540
|
-
const root = idd?.root?.trim();
|
|
3541
|
-
if (!root || !root.startsWith("+")) return null;
|
|
3542
|
-
const out = `${root}${idd?.suffixes?.[0]?.trim() ?? ""}`;
|
|
3543
|
-
return out.startsWith("+") ? out : null;
|
|
3544
|
-
}
|
|
3545
|
-
function normalizeSearchKey(input) {
|
|
3546
|
-
return String(input ?? "").toLowerCase().replace(/\s+/g, " ").trim().replace(/[^\p{L}\p{N}+ ]/gu, "");
|
|
3547
|
-
}
|
|
3548
|
-
/**
|
|
3549
|
-
* Return a copy of the country list with display names localized to `locale` via
|
|
3550
|
-
* `Intl.DisplayNames`. `search_key` is rebuilt (keeping the English name too) so search
|
|
3551
|
-
* still matches either spelling. Unknown locales / regions fall back to the English name.
|
|
3552
|
-
*/
|
|
3553
|
-
function localizeCountries(list, locale) {
|
|
3554
|
-
if (!locale) return list;
|
|
3555
|
-
let display;
|
|
3556
|
-
try {
|
|
3557
|
-
display = new Intl.DisplayNames([locale], { type: "region" });
|
|
3558
|
-
} catch {
|
|
3559
|
-
return list;
|
|
3560
|
-
}
|
|
3561
|
-
return list.map((c) => {
|
|
3562
|
-
let localized = c.raw_data.name;
|
|
3563
|
-
try {
|
|
3564
|
-
localized = display.of(c.raw_data.iso2) || c.raw_data.name;
|
|
3565
|
-
} catch {}
|
|
3566
|
-
if (localized === c.raw_data.name) return c;
|
|
3567
|
-
const dial = c.raw_data.dial_code;
|
|
3568
|
-
return {
|
|
3569
|
-
...c,
|
|
3570
|
-
label: `${localized} (${dial})`,
|
|
3571
|
-
search_key: normalizeSearchKey(`${localized} ${c.raw_data.name} ${dial} ${c.raw_data.iso2} ${c.raw_data.dial_digits}`),
|
|
3572
|
-
raw_data: {
|
|
3573
|
-
...c.raw_data,
|
|
3574
|
-
name: localized
|
|
3575
|
-
}
|
|
3576
|
-
};
|
|
3577
|
-
});
|
|
3578
|
-
}
|
|
3579
|
-
function makeFallback(iso2, name, dial) {
|
|
3580
|
-
const dialDigits = toDigits(dial);
|
|
3581
|
-
return {
|
|
3582
|
-
label: `${name} (+${dialDigits})`,
|
|
3583
|
-
value: iso2,
|
|
3584
|
-
search_key: normalizeSearchKey(`${name} +${dialDigits} ${iso2}`),
|
|
3585
|
-
raw_data: {
|
|
3586
|
-
iso2,
|
|
3587
|
-
dial_code: `+${dialDigits}`,
|
|
3588
|
-
dial_digits: dialDigits,
|
|
3589
|
-
name,
|
|
3590
|
-
flag: `https://flagcdn.com/w40/${iso2.toLowerCase()}.png`,
|
|
3591
|
-
source: "fallback",
|
|
3592
|
-
original: {}
|
|
3593
|
-
}
|
|
3594
|
-
};
|
|
3595
|
-
}
|
|
3596
|
-
const FALLBACK_COUNTRIES = [
|
|
3597
|
-
makeFallback("SA", "Saudi Arabia", "+966"),
|
|
3598
|
-
makeFallback("EG", "Egypt", "+20"),
|
|
3599
|
-
makeFallback("AE", "United Arab Emirates", "+971"),
|
|
3600
|
-
makeFallback("US", "United States", "+1"),
|
|
3601
|
-
makeFallback("GB", "United Kingdom", "+44"),
|
|
3602
|
-
makeFallback("DE", "Germany", "+49"),
|
|
3603
|
-
makeFallback("FR", "France", "+33"),
|
|
3604
|
-
makeFallback("ES", "Spain", "+34"),
|
|
3605
|
-
makeFallback("IT", "Italy", "+39"),
|
|
3606
|
-
makeFallback("TR", "Turkey", "+90"),
|
|
3607
|
-
makeFallback("RU", "Russia", "+7"),
|
|
3608
|
-
makeFallback("CN", "China", "+86"),
|
|
3609
|
-
makeFallback("IN", "India", "+91"),
|
|
3610
|
-
makeFallback("JP", "Japan", "+81"),
|
|
3611
|
-
makeFallback("KR", "South Korea", "+82"),
|
|
3612
|
-
makeFallback("BR", "Brazil", "+55"),
|
|
3613
|
-
makeFallback("MX", "Mexico", "+52"),
|
|
3614
|
-
makeFallback("CA", "Canada", "+1"),
|
|
3615
|
-
makeFallback("AU", "Australia", "+61"),
|
|
3616
|
-
makeFallback("NG", "Nigeria", "+234"),
|
|
3617
|
-
makeFallback("PK", "Pakistan", "+92"),
|
|
3618
|
-
makeFallback("ID", "Indonesia", "+62")
|
|
3619
|
-
];
|
|
3620
|
-
function usePhoneValidation() {
|
|
3621
|
-
const countries = (0, vue.ref)([]);
|
|
3622
|
-
const isCountriesLoading = (0, vue.ref)(false);
|
|
3623
|
-
const byValue = (0, vue.ref)(/* @__PURE__ */ new Map());
|
|
3624
|
-
const byDialDigits = (0, vue.ref)(/* @__PURE__ */ new Map());
|
|
3625
|
-
function rebuildIndexes(list) {
|
|
3626
|
-
const valueMap = /* @__PURE__ */ new Map();
|
|
3627
|
-
const dialMap = /* @__PURE__ */ new Map();
|
|
3628
|
-
for (const item of list) {
|
|
3629
|
-
valueMap.set(item.value, item);
|
|
3630
|
-
const dial = item.raw_data.dial_digits;
|
|
3631
|
-
if (dial) {
|
|
3632
|
-
const bucket = dialMap.get(dial) ?? [];
|
|
3633
|
-
bucket.push(item);
|
|
3634
|
-
dialMap.set(dial, bucket);
|
|
3635
|
-
}
|
|
3636
|
-
}
|
|
3637
|
-
byValue.value = valueMap;
|
|
3638
|
-
byDialDigits.value = dialMap;
|
|
3639
|
-
}
|
|
3640
|
-
function upsertCountries(list) {
|
|
3641
|
-
countries.value = list;
|
|
3642
|
-
rebuildIndexes(list);
|
|
3643
|
-
}
|
|
3644
|
-
function normalizeRestCountries(list) {
|
|
3645
|
-
const out = [];
|
|
3646
|
-
for (const c of list) {
|
|
3647
|
-
const name = c?.name?.common?.trim();
|
|
3648
|
-
const iso2 = normalizeIso2(c?.cca2);
|
|
3649
|
-
const dial = buildDialCode(c?.idd);
|
|
3650
|
-
const flag = c?.flags?.png?.trim() || c?.flags?.svg?.trim() || null;
|
|
3651
|
-
if (!name || !iso2 || !dial) continue;
|
|
3652
|
-
const dialDigits = toDigits(dial);
|
|
3653
|
-
const search_key = normalizeSearchKey(`${name} ${dial} ${iso2} ${dialDigits}`);
|
|
3654
|
-
out.push({
|
|
3655
|
-
label: `${name} (${dial})`,
|
|
3656
|
-
value: iso2,
|
|
3657
|
-
search_key,
|
|
3658
|
-
raw_data: {
|
|
3659
|
-
iso2,
|
|
3660
|
-
dial_code: dial,
|
|
3661
|
-
dial_digits: dialDigits,
|
|
3662
|
-
name,
|
|
3663
|
-
flag,
|
|
3664
|
-
source: "restcountries",
|
|
3665
|
-
original: c
|
|
3666
|
-
}
|
|
3667
|
-
});
|
|
3668
|
-
}
|
|
3669
|
-
const map = /* @__PURE__ */ new Map();
|
|
3670
|
-
for (const item of out) {
|
|
3671
|
-
const prev = map.get(item.value);
|
|
3672
|
-
if (!prev) {
|
|
3673
|
-
map.set(item.value, item);
|
|
3674
|
-
continue;
|
|
3675
|
-
}
|
|
3676
|
-
const prevScore = (prev.raw_data.flag ? 1 : 0) + (prev.raw_data.dial_code ? 1 : 0);
|
|
3677
|
-
if ((item.raw_data.flag ? 1 : 0) + (item.raw_data.dial_code ? 1 : 0) > prevScore) map.set(item.value, item);
|
|
3678
|
-
}
|
|
3679
|
-
return Array.from(map.values()).sort((a, b) => a.raw_data.name.localeCompare(b.raw_data.name));
|
|
3680
|
-
}
|
|
3681
|
-
async function getCountries(options) {
|
|
3682
|
-
const force = Boolean(options?.force);
|
|
3683
|
-
if (!force && countries.value.length) return countries.value;
|
|
3684
|
-
if (!force && isBrowser$1()) try {
|
|
3685
|
-
const cached = localStorage.getItem(STORAGE_KEY);
|
|
3686
|
-
if (cached) {
|
|
3687
|
-
const parsed = JSON.parse(cached);
|
|
3688
|
-
if (Array.isArray(parsed) && parsed.length) {
|
|
3689
|
-
upsertCountries(parsed);
|
|
3690
|
-
return countries.value;
|
|
3691
|
-
}
|
|
3692
|
-
}
|
|
3693
|
-
} catch {}
|
|
3694
|
-
isCountriesLoading.value = true;
|
|
3695
|
-
try {
|
|
3696
|
-
const res = await fetch(REST_COUNTRIES_URL);
|
|
3697
|
-
if (!res.ok) throw new Error(`Failed to fetch countries: ${res.status}`);
|
|
3698
|
-
const normalized = normalizeRestCountries(await res.json());
|
|
3699
|
-
upsertCountries(normalized.length ? normalized : FALLBACK_COUNTRIES);
|
|
3700
|
-
if (isBrowser$1()) try {
|
|
3701
|
-
localStorage.setItem(STORAGE_KEY, JSON.stringify(countries.value));
|
|
3702
|
-
} catch {}
|
|
3703
|
-
return countries.value;
|
|
3704
|
-
} catch {
|
|
3705
|
-
upsertCountries(FALLBACK_COUNTRIES);
|
|
3706
|
-
return countries.value;
|
|
3707
|
-
} finally {
|
|
3708
|
-
isCountriesLoading.value = false;
|
|
3709
|
-
}
|
|
3710
|
-
}
|
|
3711
|
-
function searchCountries(keyword, limit = 50) {
|
|
3712
|
-
const q = normalizeSearchKey(keyword);
|
|
3713
|
-
if (!q) return countries.value.slice(0, limit);
|
|
3714
|
-
const res = [];
|
|
3715
|
-
for (const item of countries.value) if (item.search_key.includes(q)) {
|
|
3716
|
-
res.push(item);
|
|
3717
|
-
if (res.length >= limit) break;
|
|
3718
|
-
}
|
|
3719
|
-
return res;
|
|
3720
|
-
}
|
|
3721
|
-
function getCountryByValue(value) {
|
|
3722
|
-
return byValue.value.get(normalizeIso2(value)) ?? null;
|
|
3723
|
-
}
|
|
3724
|
-
function getCountriesByDial(dial) {
|
|
3725
|
-
return byDialDigits.value.get(toDigits(dial)) ?? [];
|
|
3726
|
-
}
|
|
3727
|
-
function getRequiredInfo(country, locale) {
|
|
3728
|
-
const iso2 = normalizeIso2(country.iso2);
|
|
3729
|
-
if (!iso2) return null;
|
|
3730
|
-
try {
|
|
3731
|
-
const example = (0, libphonenumber_js.getExampleNumber)(iso2, EX);
|
|
3732
|
-
const exampleNational = example?.formatNational?.() ?? "";
|
|
3733
|
-
const exampleE164 = example?.format?.("E.164") ?? "";
|
|
3734
|
-
const inferred = inferLengthFromExample(exampleNational);
|
|
3735
|
-
const dial_code = country.dial_code ? ensurePlusDial(country.dial_code) : exampleE164 ? `+${example?.countryCallingCode}` : "";
|
|
3736
|
-
const digitsExample = toDigits(exampleNational);
|
|
3737
|
-
return {
|
|
3738
|
-
iso2,
|
|
3739
|
-
dial_code,
|
|
3740
|
-
placeholder: "",
|
|
3741
|
-
example_national: exampleNational,
|
|
3742
|
-
example_e164: exampleE164,
|
|
3743
|
-
national_number_length: inferred,
|
|
3744
|
-
format_hint: digitsExample ? `e.g. ${localizeDigits(digitsExample, locale)}` : ""
|
|
3745
|
-
};
|
|
3746
|
-
} catch {
|
|
3747
|
-
return null;
|
|
3748
|
-
}
|
|
3749
|
-
}
|
|
3750
|
-
function validate(input) {
|
|
3751
|
-
const country = input.country ?? null;
|
|
3752
|
-
if (!country?.iso2) return {
|
|
3753
|
-
ok: false,
|
|
3754
|
-
reason: "missing_country",
|
|
3755
|
-
country: null,
|
|
3756
|
-
phone: {
|
|
3757
|
-
raw: ("phone" in input ? input.phone : null) ?? null,
|
|
3758
|
-
digits: ""
|
|
3759
|
-
},
|
|
3760
|
-
full_phone: null,
|
|
3761
|
-
required: null
|
|
3762
|
-
};
|
|
3763
|
-
const iso2 = normalizeIso2(country.iso2);
|
|
3764
|
-
const required = getRequiredInfo({
|
|
3765
|
-
iso2,
|
|
3766
|
-
dial_code: country.dial_code
|
|
3767
|
-
}, input.locale);
|
|
3768
|
-
if (!required) return {
|
|
3769
|
-
ok: false,
|
|
3770
|
-
reason: "country_not_supported",
|
|
3771
|
-
country: {
|
|
3772
|
-
iso2,
|
|
3773
|
-
dial_code: ensurePlusDial(country.dial_code)
|
|
3774
|
-
},
|
|
3775
|
-
phone: {
|
|
3776
|
-
raw: ("phone" in input ? input.phone : null) ?? null,
|
|
3777
|
-
digits: ""
|
|
3778
|
-
},
|
|
3779
|
-
full_phone: null,
|
|
3780
|
-
required: null
|
|
3781
|
-
};
|
|
3782
|
-
if (!("phone" in input)) return {
|
|
3783
|
-
ok: true,
|
|
3784
|
-
reason: null,
|
|
3785
|
-
country: {
|
|
3786
|
-
iso2: required.iso2,
|
|
3787
|
-
dial_code: required.dial_code
|
|
3788
|
-
},
|
|
3789
|
-
phone: {
|
|
3790
|
-
raw: null,
|
|
3791
|
-
digits: ""
|
|
3792
|
-
},
|
|
3793
|
-
full_phone: null,
|
|
3794
|
-
required
|
|
3795
|
-
};
|
|
3796
|
-
const raw = input.phone;
|
|
3797
|
-
const digits = toDigits(raw);
|
|
3798
|
-
if (!raw || !String(raw).trim()) return {
|
|
3799
|
-
ok: true,
|
|
3800
|
-
reason: null,
|
|
3801
|
-
country: {
|
|
3802
|
-
iso2: required.iso2,
|
|
3803
|
-
dial_code: required.dial_code
|
|
3804
|
-
},
|
|
3805
|
-
phone: {
|
|
3806
|
-
raw: raw ?? null,
|
|
3807
|
-
digits: ""
|
|
3808
|
-
},
|
|
3809
|
-
full_phone: null,
|
|
3810
|
-
required
|
|
3811
|
-
};
|
|
3812
|
-
if (String(raw).replace(/\s+/g, "").match(/[^\d+]/)) return {
|
|
3813
|
-
ok: false,
|
|
3814
|
-
reason: "phone_has_non_digits",
|
|
3815
|
-
country: {
|
|
3816
|
-
iso2: required.iso2,
|
|
3817
|
-
dial_code: required.dial_code
|
|
3818
|
-
},
|
|
3819
|
-
phone: {
|
|
3820
|
-
raw,
|
|
3821
|
-
digits
|
|
3822
|
-
},
|
|
3823
|
-
full_phone: buildFullE164(required.dial_code, digits),
|
|
3824
|
-
required
|
|
3825
|
-
};
|
|
3826
|
-
const nsn = dropLeadingZeros(digits);
|
|
3827
|
-
const { min, max } = required.national_number_length;
|
|
3828
|
-
if (min !== null && nsn.length < min) return {
|
|
3829
|
-
ok: false,
|
|
3830
|
-
reason: "too_short",
|
|
3831
|
-
country: {
|
|
3832
|
-
iso2: required.iso2,
|
|
3833
|
-
dial_code: required.dial_code
|
|
3834
|
-
},
|
|
3835
|
-
phone: {
|
|
3836
|
-
raw,
|
|
3837
|
-
digits
|
|
3838
|
-
},
|
|
3839
|
-
full_phone: buildFullE164(required.dial_code, digits),
|
|
3840
|
-
required,
|
|
3841
|
-
details: {
|
|
3842
|
-
min,
|
|
3843
|
-
actual: nsn.length
|
|
3844
|
-
}
|
|
3845
|
-
};
|
|
3846
|
-
if (max !== null && nsn.length > max) return {
|
|
3847
|
-
ok: false,
|
|
3848
|
-
reason: "too_long",
|
|
3849
|
-
country: {
|
|
3850
|
-
iso2: required.iso2,
|
|
3851
|
-
dial_code: required.dial_code
|
|
3852
|
-
},
|
|
3853
|
-
phone: {
|
|
3854
|
-
raw,
|
|
3855
|
-
digits
|
|
3856
|
-
},
|
|
3857
|
-
full_phone: buildFullE164(required.dial_code, digits),
|
|
3858
|
-
required,
|
|
3859
|
-
details: {
|
|
3860
|
-
max,
|
|
3861
|
-
actual: nsn.length
|
|
3862
|
-
}
|
|
3863
|
-
};
|
|
3864
|
-
const full = buildFullE164(required.dial_code, digits) ?? String(raw);
|
|
3865
|
-
try {
|
|
3866
|
-
if (!(0, libphonenumber_js.isValidPhoneNumber)(full, iso2)) {
|
|
3867
|
-
const parsed = (0, libphonenumber_js.parsePhoneNumberFromString)(full, iso2);
|
|
3868
|
-
return {
|
|
3869
|
-
ok: false,
|
|
3870
|
-
reason: "invalid_phone",
|
|
3871
|
-
country: {
|
|
3872
|
-
iso2: required.iso2,
|
|
3873
|
-
dial_code: required.dial_code
|
|
3874
|
-
},
|
|
3875
|
-
phone: {
|
|
3876
|
-
raw,
|
|
3877
|
-
digits
|
|
3878
|
-
},
|
|
3879
|
-
full_phone: parsed?.number ?? null,
|
|
3880
|
-
required,
|
|
3881
|
-
details: {
|
|
3882
|
-
type: parsed?.getType?.() ?? null,
|
|
3883
|
-
possible: parsed?.isPossible?.() ?? null,
|
|
3884
|
-
country: parsed?.country ?? null
|
|
3885
|
-
}
|
|
3886
|
-
};
|
|
3887
|
-
}
|
|
3888
|
-
const parsed = (0, libphonenumber_js.parsePhoneNumberFromString)(full, iso2);
|
|
3889
|
-
return {
|
|
3890
|
-
ok: true,
|
|
3891
|
-
reason: null,
|
|
3892
|
-
country: {
|
|
3893
|
-
iso2: required.iso2,
|
|
3894
|
-
dial_code: required.dial_code
|
|
3895
|
-
},
|
|
3896
|
-
phone: {
|
|
3897
|
-
raw,
|
|
3898
|
-
digits
|
|
3899
|
-
},
|
|
3900
|
-
full_phone: parsed?.number ?? full,
|
|
3901
|
-
required
|
|
3902
|
-
};
|
|
3903
|
-
} catch (e) {
|
|
3904
|
-
return {
|
|
3905
|
-
ok: false,
|
|
3906
|
-
reason: "parse_failed",
|
|
3907
|
-
country: {
|
|
3908
|
-
iso2: required.iso2,
|
|
3909
|
-
dial_code: required.dial_code
|
|
3910
|
-
},
|
|
3911
|
-
phone: {
|
|
3912
|
-
raw,
|
|
3913
|
-
digits
|
|
3914
|
-
},
|
|
3915
|
-
full_phone: buildFullE164(required.dial_code, digits),
|
|
3916
|
-
required,
|
|
3917
|
-
details: { error: e?.message ?? String(e) }
|
|
3918
|
-
};
|
|
3919
|
-
}
|
|
3920
|
-
}
|
|
3921
|
-
return {
|
|
3922
|
-
countries,
|
|
3923
|
-
isCountriesLoading,
|
|
3924
|
-
getCountries,
|
|
3925
|
-
searchCountries,
|
|
3926
|
-
getCountryByValue,
|
|
3927
|
-
getCountriesByDial,
|
|
3928
|
-
getRequiredInfo,
|
|
3929
|
-
validate
|
|
3930
|
-
};
|
|
3931
|
-
}
|
|
3932
|
-
//#endregion
|
|
3933
3408
|
//#region src/composables/useCountryDetection.ts
|
|
3934
3409
|
/**
|
|
3935
3410
|
* Best-effort country detection chain: IP geolocation → timezone → navigator.language → fallback.
|
|
@@ -4039,24 +3514,32 @@ function tryLocale() {
|
|
|
4039
3514
|
return null;
|
|
4040
3515
|
}
|
|
4041
3516
|
}
|
|
3517
|
+
const inflightIpFetch = /* @__PURE__ */ new Map();
|
|
4042
3518
|
async function tryIp(endpoint, timeoutMs) {
|
|
4043
3519
|
if (!isBrowser() || typeof fetch !== "function") return null;
|
|
3520
|
+
const existing = inflightIpFetch.get(endpoint);
|
|
3521
|
+
if (existing) return existing;
|
|
4044
3522
|
const controller = new AbortController();
|
|
4045
3523
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
3524
|
+
const promise = (async () => {
|
|
3525
|
+
try {
|
|
3526
|
+
const res = await fetch(endpoint, {
|
|
3527
|
+
signal: controller.signal,
|
|
3528
|
+
credentials: "omit"
|
|
3529
|
+
});
|
|
3530
|
+
if (!res.ok) return null;
|
|
3531
|
+
const data = await res.json();
|
|
3532
|
+
const code = (data.country_code ?? data.country ?? "").toString().toUpperCase();
|
|
3533
|
+
return /^[A-Z]{2}$/.test(code) ? code : null;
|
|
3534
|
+
} catch {
|
|
3535
|
+
return null;
|
|
3536
|
+
} finally {
|
|
3537
|
+
clearTimeout(timer);
|
|
3538
|
+
inflightIpFetch.delete(endpoint);
|
|
3539
|
+
}
|
|
3540
|
+
})();
|
|
3541
|
+
inflightIpFetch.set(endpoint, promise);
|
|
3542
|
+
return promise;
|
|
4060
3543
|
}
|
|
4061
3544
|
function readCache() {
|
|
4062
3545
|
if (!isBrowser()) return null;
|
|
@@ -4122,6 +3605,11 @@ function useCountryDetection(opts = {}) {
|
|
|
4122
3605
|
}
|
|
4123
3606
|
//#endregion
|
|
4124
3607
|
//#region src/composables/useCountryMatching.ts
|
|
3608
|
+
/** Cached snapshot of every country libphonenumber knows about (~250 ISO2 codes).
|
|
3609
|
+
* Used by tier 2 of `matchLeadingDialCode` as the last-resort iteration so detection
|
|
3610
|
+
* works for *every* country, not just the popular ones in the bundled fallback list.
|
|
3611
|
+
* Cached at module load — `getCountries()` is a static metadata table, no I/O. */
|
|
3612
|
+
const ALL_LIBPHONENUMBER_ISO2 = (0, libphonenumber_js.getCountries)();
|
|
4125
3613
|
/** Synchronous dial-digit → ISO2 fallback for common countries, used when the async
|
|
4126
3614
|
* REST Countries fetch hasn't populated `getCountriesByDial`'s index yet at setup. */
|
|
4127
3615
|
const DIAL_TO_ISO2_FALLBACK = {
|
|
@@ -4188,6 +3676,58 @@ const DIAL_TO_ISO2_FALLBACK = {
|
|
|
4188
3676
|
/** localStorage key for the user's most recently picked countries. Used as a
|
|
4189
3677
|
* tie-breaker when multiple countries share a dial code (e.g. all NANP). */
|
|
4190
3678
|
const COUNTRY_RECENTS_KEY = "ali_ui_country_recents_v1";
|
|
3679
|
+
/** ISO2 codes iterated by tier 2 of `matchLeadingDialCode` when looking for a country
|
|
3680
|
+
* that accepts a local-format input as valid. Mirrors the `FALLBACK_COUNTRIES` list in
|
|
3681
|
+
* {@link usePhoneValidation} (kept in sync by tests + by being short and obvious).
|
|
3682
|
+
* Order matters — earlier entries get priority when multiple countries would each
|
|
3683
|
+
* validate the same input. Built around the most-populated / most-likely countries. */
|
|
3684
|
+
const FALLBACK_ISO2_LIST = [
|
|
3685
|
+
"SA",
|
|
3686
|
+
"EG",
|
|
3687
|
+
"AE",
|
|
3688
|
+
"US",
|
|
3689
|
+
"GB",
|
|
3690
|
+
"DE",
|
|
3691
|
+
"FR",
|
|
3692
|
+
"ES",
|
|
3693
|
+
"IT",
|
|
3694
|
+
"TR",
|
|
3695
|
+
"RU",
|
|
3696
|
+
"CN",
|
|
3697
|
+
"IN",
|
|
3698
|
+
"JP",
|
|
3699
|
+
"KR",
|
|
3700
|
+
"BR",
|
|
3701
|
+
"MX",
|
|
3702
|
+
"CA",
|
|
3703
|
+
"AU",
|
|
3704
|
+
"NG",
|
|
3705
|
+
"PK",
|
|
3706
|
+
"ID"
|
|
3707
|
+
];
|
|
3708
|
+
/** Build a minimal `CountryOption` from libphonenumber metadata when the async REST
|
|
3709
|
+
* Countries list hasn't loaded the entry yet. Used so country **detection** works
|
|
3710
|
+
* generically for any libphonenumber country, not just the ~22 in the offline
|
|
3711
|
+
* fallback list. The picker will overwrite this synthetic record with the real one
|
|
3712
|
+
* (with localized name + flag) as soon as `getCountries()` resolves. */
|
|
3713
|
+
function buildSyntheticCountry(iso2, dialDigits) {
|
|
3714
|
+
const ISO2 = iso2.toUpperCase();
|
|
3715
|
+
const digits = String(dialDigits).replace(/\D/g, "");
|
|
3716
|
+
return {
|
|
3717
|
+
label: `${ISO2} (+${digits})`,
|
|
3718
|
+
value: ISO2,
|
|
3719
|
+
search_key: `${ISO2.toLowerCase()} +${digits} ${digits}`,
|
|
3720
|
+
raw_data: {
|
|
3721
|
+
iso2: ISO2,
|
|
3722
|
+
dial_code: `+${digits}`,
|
|
3723
|
+
dial_digits: digits,
|
|
3724
|
+
name: ISO2,
|
|
3725
|
+
flag: `https://flagcdn.com/w40/${ISO2.toLowerCase()}.png`,
|
|
3726
|
+
source: "fallback",
|
|
3727
|
+
original: {}
|
|
3728
|
+
}
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
4191
3731
|
function readRecents() {
|
|
4192
3732
|
if (typeof window === "undefined") return [];
|
|
4193
3733
|
try {
|
|
@@ -4233,38 +3773,75 @@ function useCountryMatching(deps) {
|
|
|
4233
3773
|
const n = Number(digits);
|
|
4234
3774
|
return Number.isFinite(n) ? n : null;
|
|
4235
3775
|
}
|
|
3776
|
+
const MATCHER_CACHE_MAX = 128;
|
|
3777
|
+
const matcherCache = /* @__PURE__ */ new Map();
|
|
3778
|
+
function readMatcherCache(key) {
|
|
3779
|
+
if (!matcherCache.has(key)) return void 0;
|
|
3780
|
+
const value = matcherCache.get(key);
|
|
3781
|
+
matcherCache.delete(key);
|
|
3782
|
+
matcherCache.set(key, value);
|
|
3783
|
+
return value;
|
|
3784
|
+
}
|
|
3785
|
+
function writeMatcherCache(key, value) {
|
|
3786
|
+
if (matcherCache.size >= MATCHER_CACHE_MAX) {
|
|
3787
|
+
const oldest = matcherCache.keys().next().value;
|
|
3788
|
+
if (oldest !== void 0) matcherCache.delete(oldest);
|
|
3789
|
+
}
|
|
3790
|
+
matcherCache.set(key, value);
|
|
3791
|
+
}
|
|
4236
3792
|
/** Three-tier match of the leading digits to a country:
|
|
4237
3793
|
* 1. libphonenumber international parse (handles NANP disambiguation).
|
|
4238
|
-
* 2. libphonenumber national-format parse
|
|
4239
|
-
* formats like Egyptian `01066105963` with no
|
|
3794
|
+
* 2. libphonenumber national-format parse, iterating through candidate hint
|
|
3795
|
+
* countries (handles local formats like Egyptian `01066105963` with no
|
|
3796
|
+
* dial-code prefix). Universal coverage via `getCountries()`.
|
|
4240
3797
|
* 3. Longest-prefix match against the dial-digits index, with the current
|
|
4241
|
-
* selection / recents as tie-breakers when multiple countries share a code.
|
|
3798
|
+
* selection / recents as tie-breakers when multiple countries share a code.
|
|
3799
|
+
*
|
|
3800
|
+
* Results are LRU-cached per input + context to avoid re-paying tier-2 iteration
|
|
3801
|
+
* cost when the user backspaces and retypes the same prefix. */
|
|
4242
3802
|
function matchLeadingDialCode(digits, options = {}) {
|
|
4243
3803
|
if (!digits) return null;
|
|
4244
3804
|
const { hintCountry, currentIso2 } = options;
|
|
3805
|
+
const cacheKey = `${digits}|${hintCountry ?? ""}|${currentIso2 ?? ""}`;
|
|
3806
|
+
const cached = readMatcherCache(cacheKey);
|
|
3807
|
+
if (cached !== void 0) return cached;
|
|
3808
|
+
const result = runMatch(digits, hintCountry, currentIso2);
|
|
3809
|
+
writeMatcherCache(cacheKey, result);
|
|
3810
|
+
return result;
|
|
3811
|
+
}
|
|
3812
|
+
function runMatch(digits, hintCountry, currentIso2) {
|
|
4245
3813
|
try {
|
|
4246
3814
|
const parsed = (0, libphonenumber_js.parsePhoneNumberFromString)(`+${digits}`);
|
|
4247
|
-
if (parsed?.country && parsed.countryCallingCode) {
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
nationalNumber: String(parsed.nationalNumber ?? "")
|
|
4252
|
-
};
|
|
4253
|
-
}
|
|
4254
|
-
} catch {}
|
|
4255
|
-
if (hintCountry && digits.length >= 4) try {
|
|
4256
|
-
const parsed = (0, libphonenumber_js.parsePhoneNumberFromString)(digits, hintCountry);
|
|
4257
|
-
if (parsed?.isValid()) {
|
|
4258
|
-
const matched = getCountryByValue(parsed.country || hintCountry);
|
|
4259
|
-
if (matched) return {
|
|
4260
|
-
country: matched,
|
|
4261
|
-
nationalNumber: String(parsed.nationalNumber ?? "")
|
|
4262
|
-
};
|
|
4263
|
-
}
|
|
3815
|
+
if (parsed?.country && parsed.countryCallingCode) return {
|
|
3816
|
+
country: getCountryByValue(parsed.country) ?? buildSyntheticCountry(parsed.country, String(parsed.countryCallingCode)),
|
|
3817
|
+
nationalNumber: String(parsed.nationalNumber ?? "")
|
|
3818
|
+
};
|
|
4264
3819
|
} catch {}
|
|
3820
|
+
if (digits.length >= 4) {
|
|
3821
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
3822
|
+
if (hintCountry) candidates.add(hintCountry.toUpperCase());
|
|
3823
|
+
if (currentIso2) candidates.add(currentIso2.toUpperCase());
|
|
3824
|
+
for (const recent of readRecents()) candidates.add(recent.toUpperCase());
|
|
3825
|
+
for (const fallback of FALLBACK_ISO2_LIST) candidates.add(fallback);
|
|
3826
|
+
for (const all of ALL_LIBPHONENUMBER_ISO2) candidates.add(all);
|
|
3827
|
+
for (const iso2 of candidates) try {
|
|
3828
|
+
const parsed = (0, libphonenumber_js.parsePhoneNumberFromString)(digits, iso2);
|
|
3829
|
+
if (parsed?.isValid()) {
|
|
3830
|
+
const resolvedIso2 = parsed.country || iso2;
|
|
3831
|
+
return {
|
|
3832
|
+
country: getCountryByValue(resolvedIso2) ?? buildSyntheticCountry(resolvedIso2, String(parsed.countryCallingCode ?? "")),
|
|
3833
|
+
nationalNumber: String(parsed.nationalNumber ?? "")
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
} catch {}
|
|
3837
|
+
}
|
|
4265
3838
|
for (let len = Math.min(3, digits.length); len >= 1; len--) {
|
|
4266
3839
|
const prefix = digits.slice(0, len);
|
|
4267
|
-
|
|
3840
|
+
let group = getCountriesByDial(prefix);
|
|
3841
|
+
if (!group.length) {
|
|
3842
|
+
const iso2 = DIAL_TO_ISO2_FALLBACK[prefix];
|
|
3843
|
+
if (iso2) group = [getCountryByValue(iso2) ?? buildSyntheticCountry(iso2, prefix)];
|
|
3844
|
+
}
|
|
4268
3845
|
if (!group.length) continue;
|
|
4269
3846
|
const nationalNumber = digits.slice(prefix.length);
|
|
4270
3847
|
if (group.length === 1) return {
|
|
@@ -4336,18 +3913,36 @@ function useTelInputValidation(deps, inputs, config) {
|
|
|
4336
3913
|
phone: inputs.phone.value ?? "",
|
|
4337
3914
|
locale: config.locale()
|
|
4338
3915
|
}));
|
|
3916
|
+
const externalErrorActive = (0, vue.computed)(() => {
|
|
3917
|
+
const e = config.externalError();
|
|
3918
|
+
return typeof e === "string" && e.length > 0;
|
|
3919
|
+
});
|
|
4339
3920
|
const validationState = (0, vue.computed)(() => {
|
|
3921
|
+
if (externalErrorActive.value) return "error";
|
|
4340
3922
|
if (!inputs.phone.value) return "idle";
|
|
4341
3923
|
return validation.value.ok ? "valid" : "error";
|
|
4342
3924
|
});
|
|
4343
|
-
const visibleValidationState = (0, vue.computed)(() =>
|
|
3925
|
+
const visibleValidationState = (0, vue.computed)(() => {
|
|
3926
|
+
if (externalErrorActive.value) return "error";
|
|
3927
|
+
const mode = config.validateOn() ?? "change";
|
|
3928
|
+
if (mode === "eager") return validationState.value;
|
|
3929
|
+
if (mode === "blur" && !inputs.hasBlurred.value) return "idle";
|
|
3930
|
+
return inputs.hasFinishedTyping.value ? validationState.value : "idle";
|
|
3931
|
+
});
|
|
4344
3932
|
const errorMessage = (0, vue.computed)(() => {
|
|
3933
|
+
const ext = config.externalError();
|
|
3934
|
+
if (typeof ext === "string" && ext.length > 0) return ext;
|
|
4345
3935
|
const v = validation.value;
|
|
4346
3936
|
if (v.ok || !v.reason) return null;
|
|
4347
3937
|
if (!inputs.phone.value) return null;
|
|
4348
3938
|
return config.errorMessages()?.[v.reason] ?? inputs.messages.value.errorMessages[v.reason];
|
|
4349
3939
|
});
|
|
4350
|
-
const showError = (0, vue.computed)(() =>
|
|
3940
|
+
const showError = (0, vue.computed)(() => {
|
|
3941
|
+
if (!errorMessage.value) return false;
|
|
3942
|
+
if (externalErrorActive.value) return true;
|
|
3943
|
+
if (!config.showValidation()) return false;
|
|
3944
|
+
return visibleValidationState.value === "error";
|
|
3945
|
+
});
|
|
4351
3946
|
return {
|
|
4352
3947
|
validation,
|
|
4353
3948
|
required,
|
|
@@ -4363,54 +3958,88 @@ function useTelInputValidation(deps, inputs, config) {
|
|
|
4363
3958
|
};
|
|
4364
3959
|
}
|
|
4365
3960
|
//#endregion
|
|
4366
|
-
//#region src/
|
|
4367
|
-
const aTelInputVariants = (0, class_variance_authority.cva)("a-tel-input__field", {
|
|
4368
|
-
variants: { size: {
|
|
4369
|
-
xs: "",
|
|
4370
|
-
sm: "",
|
|
4371
|
-
md: "",
|
|
4372
|
-
lg: "",
|
|
4373
|
-
xl: ""
|
|
4374
|
-
} },
|
|
4375
|
-
defaultVariants: { size: "md" }
|
|
4376
|
-
});
|
|
4377
|
-
const DEFAULT_ERROR_MESSAGES = {
|
|
4378
|
-
missing_country: "Please select a country.",
|
|
4379
|
-
country_not_supported: "This country is not supported.",
|
|
4380
|
-
phone_has_non_digits: "Phone number can only contain digits.",
|
|
4381
|
-
too_short: "Phone number is too short.",
|
|
4382
|
-
too_long: "Phone number is too long.",
|
|
4383
|
-
invalid_phone: "Phone number is invalid.",
|
|
4384
|
-
parse_failed: "Could not parse phone number."
|
|
4385
|
-
};
|
|
4386
|
-
/** English defaults for every {@link TelInputMessages} key. */
|
|
4387
|
-
const DEFAULT_MESSAGES = {
|
|
4388
|
-
searchPlaceholder: "Search country or +code…",
|
|
4389
|
-
emptyText: "No countries found.",
|
|
4390
|
-
loadingText: "Loading countries…",
|
|
4391
|
-
suggestedLabel: "Suggested",
|
|
4392
|
-
allCountriesLabel: "All countries",
|
|
4393
|
-
errorMessages: DEFAULT_ERROR_MESSAGES,
|
|
4394
|
-
countryLabel: "Country",
|
|
4395
|
-
selectCountryLabel: "Select country",
|
|
4396
|
-
phoneInputLabel: "Phone number"
|
|
4397
|
-
};
|
|
3961
|
+
//#region src/composables/useCountrySelection.ts
|
|
4398
3962
|
/**
|
|
4399
|
-
*
|
|
4400
|
-
*
|
|
3963
|
+
* The picker selection state machine for {@link ATelInput}, consolidated into a
|
|
3964
|
+
* single composable so the component doesn't have to juggle three boolean flags
|
|
3965
|
+
* (`userPickedCountry` / `autoSettingCountry` / `inputDetectionApplied`) and
|
|
3966
|
+
* reason about their pairwise interactions.
|
|
3967
|
+
*
|
|
3968
|
+
* Every write to the selection goes through {@link UseCountrySelectionReturn.set},
|
|
3969
|
+
* which records both the new ISO2 and the *origin* of the change. That makes the
|
|
3970
|
+
* downstream decision — should detection re-route the picker on the next typed-input
|
|
3971
|
+
* burst? — a one-liner: `if (detectionLocked.value) return;`.
|
|
4401
3972
|
*/
|
|
4402
|
-
function
|
|
4403
|
-
|
|
3973
|
+
function useCountrySelection() {
|
|
3974
|
+
const iso2 = (0, vue.ref)("");
|
|
3975
|
+
const source = (0, vue.ref)("none");
|
|
3976
|
+
function set(nextIso2, nextSource) {
|
|
3977
|
+
iso2.value = nextIso2;
|
|
3978
|
+
source.value = nextSource;
|
|
3979
|
+
}
|
|
3980
|
+
function clear() {
|
|
3981
|
+
iso2.value = "";
|
|
3982
|
+
source.value = "none";
|
|
3983
|
+
}
|
|
4404
3984
|
return {
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
}
|
|
3985
|
+
iso2,
|
|
3986
|
+
source,
|
|
3987
|
+
set,
|
|
3988
|
+
clear,
|
|
3989
|
+
detectionLocked: (0, vue.computed)(() => source.value === "picker" || source.value === "input")
|
|
4411
3990
|
};
|
|
4412
3991
|
}
|
|
4413
3992
|
//#endregion
|
|
3993
|
+
//#region src/composables/useSyncedModel.ts
|
|
3994
|
+
/**
|
|
3995
|
+
* Two-way bidirectional sync between a `defineModel` ref and internal component
|
|
3996
|
+
* state — with the **echo-loop guard** built in. Solves a recurring class of
|
|
3997
|
+
* bugs in this component where two watchers (external→internal and
|
|
3998
|
+
* internal→external) would fight each other and rewrite values the user just
|
|
3999
|
+
* typed.
|
|
4000
|
+
*
|
|
4001
|
+
* Mechanics:
|
|
4002
|
+
*
|
|
4003
|
+
* 1. When any of `triggers` change AND we're not currently applying an
|
|
4004
|
+
* external write, recompute the model value via `compose()` and write it
|
|
4005
|
+
* into `model`. Stamp `lastEmitted` first so we recognise the echo.
|
|
4006
|
+
* 2. When `model` changes AND the new value isn't the echo of our last emit,
|
|
4007
|
+
* apply it into internal state via `apply()`. The `applying` flag is held
|
|
4008
|
+
* for the duration of `apply()` so step (1) skips while we mutate.
|
|
4009
|
+
*
|
|
4010
|
+
* Used for:
|
|
4011
|
+
* - `modelValue` (E.164 string) ↔ `phone` + `selectedIso2`.
|
|
4012
|
+
* - `country` (dial-number) ↔ `selectedIso2`.
|
|
4013
|
+
*
|
|
4014
|
+
* The hand-rolled equivalents (`applyingModelValue` / `lastEmittedModelValue`
|
|
4015
|
+
* plus the country↔iso2 watcher pair with `autoSettingCountry`) collapse into
|
|
4016
|
+
* two calls to this helper.
|
|
4017
|
+
*/
|
|
4018
|
+
function useSyncedModel(options) {
|
|
4019
|
+
const { model, triggers, compose, apply } = options;
|
|
4020
|
+
const isEqual = options.isEqual ?? Object.is;
|
|
4021
|
+
let applying = false;
|
|
4022
|
+
let lastEmitted = { __unset: true };
|
|
4023
|
+
const isEcho = (v) => typeof lastEmitted === "object" && lastEmitted !== null && "__unset" in lastEmitted ? false : isEqual(v, lastEmitted);
|
|
4024
|
+
(0, vue.watch)(model, (next) => {
|
|
4025
|
+
if (isEcho(next)) return;
|
|
4026
|
+
applying = true;
|
|
4027
|
+
try {
|
|
4028
|
+
apply(next);
|
|
4029
|
+
} finally {
|
|
4030
|
+
applying = false;
|
|
4031
|
+
}
|
|
4032
|
+
}, { immediate: true });
|
|
4033
|
+
(0, vue.watch)(triggers, () => {
|
|
4034
|
+
if (applying) return;
|
|
4035
|
+
const next = compose();
|
|
4036
|
+
if (!isEqual(next, model.value)) {
|
|
4037
|
+
lastEmitted = next;
|
|
4038
|
+
model.value = next;
|
|
4039
|
+
}
|
|
4040
|
+
}, { flush: "post" });
|
|
4041
|
+
}
|
|
4042
|
+
//#endregion
|
|
4414
4043
|
//#region ../AResponsivePopover/dist/index.js
|
|
4415
4044
|
var __defProp = Object.defineProperty;
|
|
4416
4045
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -17140,7 +16769,7 @@ const _sfc_main$9 = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
17140
16769
|
if (typeof document === "undefined") return [];
|
|
17141
16770
|
return Array.from(document.querySelectorAll("[data-responsive-popover-scroll-container=\"true\"]"));
|
|
17142
16771
|
},
|
|
17143
|
-
active: (0, vue.computed)(() => !!ctx?.open.value &&
|
|
16772
|
+
active: (0, vue.computed)(() => !!ctx?.open.value && scrollLockMode.value === "events")
|
|
17144
16773
|
});
|
|
17145
16774
|
const __returned__ = {
|
|
17146
16775
|
props,
|
|
@@ -17186,7 +16815,8 @@ function _sfc_render$9(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
17186
16815
|
])) : ((0, vue.openBlock)(), (0, vue.createBlock)($setup["ADrawerContent"], {
|
|
17187
16816
|
key: 1,
|
|
17188
16817
|
class: (0, vue.normalizeClass)($setup.mergedClass),
|
|
17189
|
-
"data-slot": "responsive-popover-content"
|
|
16818
|
+
"data-slot": "responsive-popover-content",
|
|
16819
|
+
"data-responsive-popover-scroll-container": "true"
|
|
17190
16820
|
}, {
|
|
17191
16821
|
default: (0, vue.withCtx)(() => [(0, vue.renderSlot)(_ctx.$slots, "default")]),
|
|
17192
16822
|
_: 3
|
|
@@ -17597,11 +17227,11 @@ const _sfc_main$1 = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
17597
17227
|
setup(__props, { expose: __expose }) {
|
|
17598
17228
|
const props = __props;
|
|
17599
17229
|
const selected = (0, vue.useModel)(__props, "selected");
|
|
17600
|
-
const { countries: internalCountries, isCountriesLoading, getCountries, searchCountries: defaultSearch, getCountryByValue: lookupInternal } = usePhoneValidation();
|
|
17230
|
+
const { countries: internalCountries, isCountriesLoading, getCountries, searchCountries: defaultSearch, getCountryByValue: lookupInternal } = require_usePhoneValidation.usePhoneValidation();
|
|
17601
17231
|
const open = (0, vue.ref)(false);
|
|
17602
17232
|
const search = (0, vue.ref)("");
|
|
17603
17233
|
getCountries();
|
|
17604
|
-
const effectiveCountries = (0, vue.computed)(() => props.countries && props.countries.length ? props.countries : localizeCountries(internalCountries.value, props.locale));
|
|
17234
|
+
const effectiveCountries = (0, vue.computed)(() => props.countries && props.countries.length ? props.countries : require_usePhoneValidation.localizeCountries(internalCountries.value, props.locale));
|
|
17605
17235
|
const effectiveByValue = (0, vue.computed)(() => new Map(effectiveCountries.value.map((c) => [c.value, c])));
|
|
17606
17236
|
function lookup(iso2) {
|
|
17607
17237
|
if (!iso2) return null;
|
|
@@ -17845,7 +17475,7 @@ const _hoisted_6$1 = {
|
|
|
17845
17475
|
};
|
|
17846
17476
|
const _hoisted_7$1 = { class: "a-country-select__list" };
|
|
17847
17477
|
const _hoisted_8$1 = { class: "a-country-select__loading" };
|
|
17848
|
-
const _hoisted_9 = { class: "a-country-select__empty" };
|
|
17478
|
+
const _hoisted_9$1 = { class: "a-country-select__empty" };
|
|
17849
17479
|
const _hoisted_10 = {
|
|
17850
17480
|
key: 0,
|
|
17851
17481
|
"data-slot": "country-select-group",
|
|
@@ -17943,7 +17573,7 @@ function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
17943
17573
|
(0, vue.createElementVNode)("div", _hoisted_7$1, [$setup.isCountriesLoading && $setup.effectiveCountries.length === 0 ? (0, vue.renderSlot)(_ctx.$slots, "loading", { key: 0 }, () => [(0, vue.createElementVNode)("div", _hoisted_8$1, (0, vue.toDisplayString)($setup.props.loadingText), 1)], true) : $setup.isSearching && $setup.filtered.length === 0 ? (0, vue.renderSlot)(_ctx.$slots, "empty", {
|
|
17944
17574
|
key: 1,
|
|
17945
17575
|
query: $setup.search
|
|
17946
|
-
}, () => [(0, vue.createElementVNode)("div", _hoisted_9, (0, vue.toDisplayString)($setup.props.emptyText), 1)], true) : ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 2 }, [
|
|
17576
|
+
}, () => [(0, vue.createElementVNode)("div", _hoisted_9$1, (0, vue.toDisplayString)($setup.props.emptyText), 1)], true) : ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 2 }, [
|
|
17947
17577
|
(0, vue.createCommentVNode)(" Suggested group "),
|
|
17948
17578
|
$setup.suggested.length > 0 ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("section", _hoisted_10, [(0, vue.renderSlot)(_ctx.$slots, "group-header", {
|
|
17949
17579
|
label: $setup.props.suggestedLabel,
|
|
@@ -18072,6 +17702,27 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18072
17702
|
required: false,
|
|
18073
17703
|
skipCheck: true
|
|
18074
17704
|
},
|
|
17705
|
+
modelValue: {
|
|
17706
|
+
type: String,
|
|
17707
|
+
required: false
|
|
17708
|
+
},
|
|
17709
|
+
name: {
|
|
17710
|
+
type: String,
|
|
17711
|
+
required: false
|
|
17712
|
+
},
|
|
17713
|
+
error: {
|
|
17714
|
+
type: [String, null],
|
|
17715
|
+
required: false
|
|
17716
|
+
},
|
|
17717
|
+
validating: {
|
|
17718
|
+
type: Boolean,
|
|
17719
|
+
required: false
|
|
17720
|
+
},
|
|
17721
|
+
validateOn: {
|
|
17722
|
+
type: String,
|
|
17723
|
+
required: false,
|
|
17724
|
+
default: "change"
|
|
17725
|
+
},
|
|
18075
17726
|
placeholder: {
|
|
18076
17727
|
type: String,
|
|
18077
17728
|
required: false,
|
|
@@ -18214,28 +17865,65 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18214
17865
|
type: [Number, null],
|
|
18215
17866
|
default: null
|
|
18216
17867
|
},
|
|
18217
|
-
"countryModifiers": {}
|
|
17868
|
+
"countryModifiers": {},
|
|
17869
|
+
"modelValue": {
|
|
17870
|
+
type: String,
|
|
17871
|
+
default: ""
|
|
17872
|
+
},
|
|
17873
|
+
"modelModifiers": {}
|
|
18218
17874
|
}),
|
|
18219
|
-
emits:
|
|
18220
|
-
|
|
17875
|
+
emits: /* @__PURE__ */ (0, vue.mergeModels)([
|
|
17876
|
+
"update:modelValue",
|
|
17877
|
+
"update:phone",
|
|
17878
|
+
"update:country",
|
|
17879
|
+
"blur",
|
|
17880
|
+
"focus"
|
|
17881
|
+
], [
|
|
17882
|
+
"update:phone",
|
|
17883
|
+
"update:country",
|
|
17884
|
+
"update:modelValue"
|
|
17885
|
+
]),
|
|
17886
|
+
setup(__props, { expose: __expose, emit: __emit }) {
|
|
18221
17887
|
const props = __props;
|
|
17888
|
+
const emit = __emit;
|
|
18222
17889
|
const phone = (0, vue.useModel)(__props, "phone");
|
|
18223
17890
|
/** Public `v-model:country` — the **dial number** (e.g. `20` for Egypt, `44` for the UK,
|
|
18224
17891
|
* `1` for the NANP block). `null` means no country selected. Internally the component
|
|
18225
17892
|
* tracks a richer ISO2 code (`selectedIso2`) because dial codes alone can't disambiguate
|
|
18226
17893
|
* NANP (`+1` covers 25+ countries) — the picker still needs an exact country. */
|
|
18227
17894
|
const country = (0, vue.useModel)(__props, "country");
|
|
18228
|
-
/**
|
|
18229
|
-
*
|
|
18230
|
-
|
|
18231
|
-
|
|
17895
|
+
/**
|
|
17896
|
+
* Default v-model — the canonical **E.164** string (e.g. `'+201066105963'`).
|
|
17897
|
+
*
|
|
17898
|
+
* Single-string contract for VeeValidate's `<Field v-slot="{ field }">` pattern
|
|
17899
|
+
* (`v-bind="field"`), native `<form>` submission, or any `v-model="phoneE164"`
|
|
17900
|
+
* consumer. Bind it with:
|
|
17901
|
+
*
|
|
17902
|
+
* <ATelInput v-model="phoneE164" />
|
|
17903
|
+
*
|
|
17904
|
+
* <VeeField v-slot="{ field, errors }" name="phone">
|
|
17905
|
+
* <ATelInput v-bind="field" :error="errors[0]" />
|
|
17906
|
+
* </VeeField>
|
|
17907
|
+
*
|
|
17908
|
+
* When set externally, the value is parsed via libphonenumber-js → the country
|
|
17909
|
+
* picker and the digits-only `phone` model are derived from it. When the user
|
|
17910
|
+
* types or picks a country, the composed E.164 is written back out. Stays in
|
|
17911
|
+
* sync with `v-model:phone` / `v-model:country` — you can use either contract.
|
|
17912
|
+
*/
|
|
17913
|
+
const modelValue = (0, vue.useModel)(__props, "modelValue");
|
|
17914
|
+
/** The picker selection state machine — `iso2` is the internal source of truth, `source`
|
|
17915
|
+
* records where the current selection came from, `detectionLocked` answers "should
|
|
17916
|
+
* typed-input detection re-route the picker on the next burst?". Single mutator: `set`.
|
|
17917
|
+
* Replaces the historical flag soup (`userPickedCountry` / `autoSettingCountry` /
|
|
17918
|
+
* `inputDetectionApplied`). */
|
|
17919
|
+
const selection = useCountrySelection();
|
|
17920
|
+
const selectedIso2 = selection.iso2;
|
|
17921
|
+
const { getCountries, validate, getRequiredInfo, getCountryByValue, getCountriesByDial } = require_usePhoneValidation.usePhoneValidation();
|
|
18232
17922
|
const { resolveCountryIdentifier, dialNumberFor, matchLeadingDialCode } = useCountryMatching({
|
|
18233
17923
|
getCountryByValue,
|
|
18234
17924
|
getCountriesByDial
|
|
18235
17925
|
});
|
|
18236
17926
|
getCountries();
|
|
18237
|
-
const userPickedCountry = (0, vue.ref)(false);
|
|
18238
|
-
const autoSettingCountry = (0, vue.ref)(false);
|
|
18239
17927
|
/** Silently resolved via IP/timezone/locale when `detectFromInput` is on — used as a hint
|
|
18240
17928
|
* so local-format numbers (e.g. Egyptian `01066105963`) can be parsed without a `+` prefix.
|
|
18241
17929
|
* Seeded from `defaultCountry` so it has a usable value before async detection resolves. */
|
|
@@ -18249,18 +17937,28 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18249
17937
|
currentIso2: selectedIso2.value
|
|
18250
17938
|
});
|
|
18251
17939
|
}
|
|
17940
|
+
/** User explicitly picked a country from the picker — locks the selection so subsequent
|
|
17941
|
+
* typed-input detection cannot churn the picker. */
|
|
17942
|
+
function onPickerPick(iso2) {
|
|
17943
|
+
selection.set(iso2, "picker");
|
|
17944
|
+
}
|
|
18252
17945
|
const typing = useTypingPhase({
|
|
18253
17946
|
debounceMs: (0, vue.computed)(() => Math.max(0, props.detectDebounceMs)),
|
|
18254
17947
|
onSettle: () => {
|
|
18255
17948
|
if (!props.detectFromInput) return;
|
|
18256
|
-
if (
|
|
17949
|
+
if (selection.detectionLocked.value) return;
|
|
18257
17950
|
const current = phone.value;
|
|
18258
17951
|
if (!current) return;
|
|
17952
|
+
const typedInternational = (displayValue.value ?? "").trimStart().startsWith("+");
|
|
17953
|
+
if (selectedIso2.value && !typedInternational) return;
|
|
18259
17954
|
typing.markDetectionAttempt();
|
|
18260
17955
|
const match = tryMatchPhone(current);
|
|
18261
17956
|
if (!match) return;
|
|
18262
|
-
|
|
18263
|
-
|
|
17957
|
+
if (match.country.value === selectedIso2.value && match.nationalNumber === phone.value) {
|
|
17958
|
+
selection.source.value = "input";
|
|
17959
|
+
return;
|
|
17960
|
+
}
|
|
17961
|
+
selection.set(match.country.value, "input");
|
|
18264
17962
|
phone.value = match.nationalNumber;
|
|
18265
17963
|
}
|
|
18266
17964
|
});
|
|
@@ -18271,8 +17969,7 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18271
17969
|
const seed = resolveCountryIdentifier(props.defaultCountry);
|
|
18272
17970
|
if (seed) {
|
|
18273
17971
|
inferredCountry.value = seed;
|
|
18274
|
-
|
|
18275
|
-
selectedIso2.value = seed;
|
|
17972
|
+
selection.set(seed, "default");
|
|
18276
17973
|
return;
|
|
18277
17974
|
}
|
|
18278
17975
|
}
|
|
@@ -18291,43 +17988,31 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18291
17988
|
const iso2 = detected ? detected.toUpperCase() : "";
|
|
18292
17989
|
if (props.detectFromInput) {
|
|
18293
17990
|
inferredCountry.value = iso2;
|
|
18294
|
-
if (phone.value && !
|
|
17991
|
+
if (phone.value && !selection.detectionLocked.value && !selectedIso2.value) {
|
|
18295
17992
|
const match = tryMatchPhone(phone.value);
|
|
18296
17993
|
if (match) {
|
|
18297
|
-
|
|
18298
|
-
selectedIso2.value = match.country.value;
|
|
17994
|
+
selection.set(match.country.value, "input");
|
|
18299
17995
|
phone.value = match.nationalNumber;
|
|
18300
17996
|
}
|
|
18301
17997
|
}
|
|
18302
17998
|
return;
|
|
18303
17999
|
}
|
|
18304
|
-
if (!selectedIso2.value && iso2)
|
|
18305
|
-
autoSettingCountry.value = true;
|
|
18306
|
-
selectedIso2.value = iso2;
|
|
18307
|
-
}
|
|
18000
|
+
if (!selectedIso2.value && iso2) selection.set(iso2, "env");
|
|
18308
18001
|
});
|
|
18309
|
-
|
|
18310
|
-
|
|
18311
|
-
|
|
18312
|
-
|
|
18313
|
-
|
|
18314
|
-
if (
|
|
18315
|
-
|
|
18002
|
+
useSyncedModel({
|
|
18003
|
+
model: country,
|
|
18004
|
+
triggers: [selectedIso2],
|
|
18005
|
+
compose: () => selectedIso2.value ? dialNumberFor(selectedIso2.value) : null,
|
|
18006
|
+
apply: (next) => {
|
|
18007
|
+
if (next == null) {
|
|
18008
|
+
selection.clear();
|
|
18009
|
+
return;
|
|
18010
|
+
}
|
|
18011
|
+
if (dialNumberFor(selectedIso2.value) === next) return;
|
|
18012
|
+
const iso2 = resolveCountryIdentifier(String(next));
|
|
18013
|
+
if (iso2) selection.set(iso2, "external");
|
|
18316
18014
|
}
|
|
18317
|
-
|
|
18318
|
-
const iso2 = resolveCountryIdentifier(String(next));
|
|
18319
|
-
if (iso2) selectedIso2.value = iso2;
|
|
18320
|
-
}, { immediate: true });
|
|
18321
|
-
/** Internal → external: keep `country` (dial number) in lockstep with `selectedIso2`, and
|
|
18322
|
-
* flag "user manually picked from picker" when the change isn't one we initiated.
|
|
18323
|
-
* `flush: 'sync'` so the `autoSettingCountry` guard is reliable. */
|
|
18324
|
-
(0, vue.watch)(selectedIso2, (iso2, prev) => {
|
|
18325
|
-
const wasAutoSet = autoSettingCountry.value;
|
|
18326
|
-
autoSettingCountry.value = false;
|
|
18327
|
-
const nextDial = dialNumberFor(iso2);
|
|
18328
|
-
if (country.value !== nextDial) country.value = nextDial;
|
|
18329
|
-
if (!wasAutoSet && props.detectFromInput && iso2 && prev !== iso2) userPickedCountry.value = true;
|
|
18330
|
-
}, { flush: "sync" });
|
|
18015
|
+
});
|
|
18331
18016
|
/** The string shown in the `<input>`. Deliberately decoupled from `phone` (the digits-only
|
|
18332
18017
|
* model) so the visible field is NOT rewritten mid-edit — non-digits / alternative numerals
|
|
18333
18018
|
* are normalized into `phone` immediately, but the displayed value is only cleaned up once
|
|
@@ -18336,6 +18021,29 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18336
18021
|
/** Set when the in-flight `phone` change came from the user typing — tells the `phone`
|
|
18337
18022
|
* watcher to leave `displayValue` alone (the user is still editing it). */
|
|
18338
18023
|
let phoneEditedByInput = false;
|
|
18024
|
+
useSyncedModel({
|
|
18025
|
+
model: modelValue,
|
|
18026
|
+
triggers: [phone, selectedIso2],
|
|
18027
|
+
compose: () => {
|
|
18028
|
+
if (!selectedIso2.value || !phone.value) return "";
|
|
18029
|
+
return validate({
|
|
18030
|
+
country: { iso2: selectedIso2.value },
|
|
18031
|
+
phone: phone.value
|
|
18032
|
+
}).full_phone ?? "";
|
|
18033
|
+
},
|
|
18034
|
+
apply: (next) => {
|
|
18035
|
+
const trimmed = String(next ?? "").trim();
|
|
18036
|
+
if (!trimmed) {
|
|
18037
|
+
if (phone.value !== "") phone.value = "";
|
|
18038
|
+
if (selectedIso2.value !== "") selection.clear();
|
|
18039
|
+
return;
|
|
18040
|
+
}
|
|
18041
|
+
const parsed = (0, libphonenumber_js.parsePhoneNumberFromString)(trimmed.startsWith("+") ? trimmed : `+${trimmed.replace(/^\+/, "")}`);
|
|
18042
|
+
if (!parsed || !parsed.country) return;
|
|
18043
|
+
if (selectedIso2.value !== parsed.country) selection.set(parsed.country, "external");
|
|
18044
|
+
if (phone.value !== parsed.nationalNumber) phone.value = parsed.nationalNumber;
|
|
18045
|
+
}
|
|
18046
|
+
});
|
|
18339
18047
|
function commitPhone(value) {
|
|
18340
18048
|
phoneEditedByInput = true;
|
|
18341
18049
|
phone.value = value;
|
|
@@ -18343,14 +18051,10 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18343
18051
|
function handlePhoneInput(e) {
|
|
18344
18052
|
const target = e.target;
|
|
18345
18053
|
displayValue.value = target.value;
|
|
18346
|
-
const cleaned = normalizeDigits(target.value).replace(/\D/g, "");
|
|
18054
|
+
const cleaned = require_usePhoneValidation.normalizeDigits(target.value).replace(/\D/g, "");
|
|
18347
18055
|
if (!cleaned) {
|
|
18348
18056
|
typing.reset();
|
|
18349
|
-
if (props.detectFromInput)
|
|
18350
|
-
autoSettingCountry.value = true;
|
|
18351
|
-
selectedIso2.value = "";
|
|
18352
|
-
userPickedCountry.value = false;
|
|
18353
|
-
}
|
|
18057
|
+
if (props.detectFromInput) selection.clear();
|
|
18354
18058
|
commitPhone("");
|
|
18355
18059
|
return;
|
|
18356
18060
|
}
|
|
@@ -18361,10 +18065,10 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18361
18065
|
* value — fold alternative numerals to ASCII and drop any stray non-digits. */
|
|
18362
18066
|
function handlePhoneChange(e) {
|
|
18363
18067
|
const target = e.target;
|
|
18364
|
-
displayValue.value = normalizeDigits(target.value).replace(/\D/g, "");
|
|
18068
|
+
displayValue.value = require_usePhoneValidation.normalizeDigits(target.value).replace(/\D/g, "");
|
|
18365
18069
|
}
|
|
18366
18070
|
(0, vue.watch)(() => phone.value, (next) => {
|
|
18367
|
-
const cleaned = normalizeDigits(String(next ?? "")).replace(/\D/g, "");
|
|
18071
|
+
const cleaned = require_usePhoneValidation.normalizeDigits(String(next ?? "")).replace(/\D/g, "");
|
|
18368
18072
|
if (cleaned !== next) {
|
|
18369
18073
|
phone.value = cleaned;
|
|
18370
18074
|
return;
|
|
@@ -18377,12 +18081,14 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18377
18081
|
}, { flush: "post" });
|
|
18378
18082
|
/** Resolved UI strings — `messages` prop merged onto English defaults. The individual
|
|
18379
18083
|
* string props still win when both are set (see `errorMessage` / template bindings). */
|
|
18380
|
-
const messages = (0, vue.computed)(() => resolveMessages(props.messages));
|
|
18084
|
+
const messages = (0, vue.computed)(() => require_types.resolveMessages(props.messages));
|
|
18381
18085
|
/** `dir` of the outer wrapper — drives the hint/error text alignment and the country
|
|
18382
18086
|
* picker popover. Explicit `'ltr'`/`'rtl'` is applied; `'auto'` or an omitted prop yields
|
|
18383
18087
|
* `undefined` so it inherits from the page. The field row itself is always LTR so the
|
|
18384
18088
|
* dial prefix / digits / flag trigger keep a consistent order. */
|
|
18385
18089
|
const dirAttr = (0, vue.computed)(() => props.dir === "ltr" || props.dir === "rtl" ? props.dir : void 0);
|
|
18090
|
+
/** Set to `true` the first time the input is blurred. Drives `validateOn: 'blur'`. */
|
|
18091
|
+
const hasBlurred = (0, vue.ref)(false);
|
|
18386
18092
|
const { validation, required, validationState, visibleValidationState, errorMessage, showError, showHint, selectedDialCode } = useTelInputValidation({
|
|
18387
18093
|
validate,
|
|
18388
18094
|
getRequiredInfo,
|
|
@@ -18391,15 +18097,35 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18391
18097
|
phone,
|
|
18392
18098
|
selectedIso2,
|
|
18393
18099
|
hasFinishedTyping,
|
|
18100
|
+
hasBlurred,
|
|
18394
18101
|
messages
|
|
18395
18102
|
}, {
|
|
18396
18103
|
locale: () => props.locale,
|
|
18397
18104
|
showValidation: () => props.showValidation,
|
|
18398
|
-
errorMessages: () => props.errorMessages
|
|
18105
|
+
errorMessages: () => props.errorMessages,
|
|
18106
|
+
validateOn: () => props.validateOn,
|
|
18107
|
+
externalError: () => props.error
|
|
18399
18108
|
});
|
|
18400
18109
|
const effectivePlaceholder = (0, vue.computed)(() => props.placeholder || required.value?.format_hint || messages.value.phoneInputLabel);
|
|
18401
18110
|
const helperId = (0, vue.useId)();
|
|
18402
18111
|
const describedBy = (0, vue.computed)(() => showError.value || showHint.value ? helperId : void 0);
|
|
18112
|
+
const inputRef = (0, vue.ref)(null);
|
|
18113
|
+
function handleBlur(e) {
|
|
18114
|
+
hasBlurred.value = true;
|
|
18115
|
+
emit("blur", e);
|
|
18116
|
+
}
|
|
18117
|
+
function handleFocus(e) {
|
|
18118
|
+
emit("focus", e);
|
|
18119
|
+
}
|
|
18120
|
+
function focus(options) {
|
|
18121
|
+
inputRef.value?.focus(options);
|
|
18122
|
+
}
|
|
18123
|
+
function blur() {
|
|
18124
|
+
inputRef.value?.blur();
|
|
18125
|
+
}
|
|
18126
|
+
function select() {
|
|
18127
|
+
inputRef.value?.select();
|
|
18128
|
+
}
|
|
18403
18129
|
__expose({
|
|
18404
18130
|
validation,
|
|
18405
18131
|
required,
|
|
@@ -18408,12 +18134,18 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18408
18134
|
visibleValidationState,
|
|
18409
18135
|
isDetecting,
|
|
18410
18136
|
hasFinishedTyping,
|
|
18411
|
-
detectionAttempted
|
|
18137
|
+
detectionAttempted,
|
|
18138
|
+
focus,
|
|
18139
|
+
blur,
|
|
18140
|
+
select
|
|
18412
18141
|
});
|
|
18413
18142
|
const __returned__ = {
|
|
18414
18143
|
props,
|
|
18144
|
+
emit,
|
|
18415
18145
|
phone,
|
|
18416
18146
|
country,
|
|
18147
|
+
modelValue,
|
|
18148
|
+
selection,
|
|
18417
18149
|
selectedIso2,
|
|
18418
18150
|
getCountries,
|
|
18419
18151
|
validate,
|
|
@@ -18423,10 +18155,9 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18423
18155
|
resolveCountryIdentifier,
|
|
18424
18156
|
dialNumberFor,
|
|
18425
18157
|
matchLeadingDialCode,
|
|
18426
|
-
userPickedCountry,
|
|
18427
|
-
autoSettingCountry,
|
|
18428
18158
|
inferredCountry,
|
|
18429
18159
|
tryMatchPhone,
|
|
18160
|
+
onPickerPick,
|
|
18430
18161
|
typing,
|
|
18431
18162
|
isDetecting,
|
|
18432
18163
|
hasFinishedTyping,
|
|
@@ -18443,6 +18174,7 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18443
18174
|
handlePhoneChange,
|
|
18444
18175
|
messages,
|
|
18445
18176
|
dirAttr,
|
|
18177
|
+
hasBlurred,
|
|
18446
18178
|
validation,
|
|
18447
18179
|
required,
|
|
18448
18180
|
validationState,
|
|
@@ -18454,6 +18186,12 @@ const _sfc_main = /* @__PURE__ */ (0, vue.defineComponent)({
|
|
|
18454
18186
|
effectivePlaceholder,
|
|
18455
18187
|
helperId,
|
|
18456
18188
|
describedBy,
|
|
18189
|
+
inputRef,
|
|
18190
|
+
handleBlur,
|
|
18191
|
+
handleFocus,
|
|
18192
|
+
focus,
|
|
18193
|
+
blur,
|
|
18194
|
+
select,
|
|
18457
18195
|
get cn() {
|
|
18458
18196
|
return cn$2;
|
|
18459
18197
|
},
|
|
@@ -18495,25 +18233,34 @@ const _hoisted_4 = {
|
|
|
18495
18233
|
};
|
|
18496
18234
|
const _hoisted_5 = [
|
|
18497
18235
|
"value",
|
|
18236
|
+
"name",
|
|
18498
18237
|
"disabled",
|
|
18499
18238
|
"placeholder",
|
|
18500
18239
|
"aria-label",
|
|
18501
18240
|
"aria-invalid",
|
|
18502
18241
|
"aria-describedby",
|
|
18242
|
+
"aria-errormessage",
|
|
18243
|
+
"aria-busy",
|
|
18503
18244
|
"data-has-dial"
|
|
18504
18245
|
];
|
|
18505
18246
|
const _hoisted_6 = {
|
|
18247
|
+
key: 0,
|
|
18248
|
+
class: "a-tel-input__validating",
|
|
18249
|
+
"data-slot": "tel-input-validating",
|
|
18250
|
+
"aria-hidden": "true"
|
|
18251
|
+
};
|
|
18252
|
+
const _hoisted_7 = {
|
|
18506
18253
|
key: 0,
|
|
18507
18254
|
class: "a-tel-input__detecting",
|
|
18508
18255
|
"aria-hidden": "true",
|
|
18509
18256
|
"data-slot": "tel-input-detecting"
|
|
18510
18257
|
};
|
|
18511
|
-
const
|
|
18258
|
+
const _hoisted_8 = {
|
|
18512
18259
|
key: 0,
|
|
18513
18260
|
class: "a-tel-input__country-wrapper",
|
|
18514
18261
|
"data-slot": "tel-input-country-wrapper"
|
|
18515
18262
|
};
|
|
18516
|
-
const
|
|
18263
|
+
const _hoisted_9 = ["id"];
|
|
18517
18264
|
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
18518
18265
|
return (0, vue.openBlock)(), (0, vue.createElementBlock)("div", {
|
|
18519
18266
|
class: (0, vue.normalizeClass)($setup.cn("a-tel-input", _ctx.$attrs.class)),
|
|
@@ -18531,31 +18278,41 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
18531
18278
|
(0, vue.renderSlot)(_ctx.$slots, "prefix", {}, void 0, true),
|
|
18532
18279
|
$setup.selectedDialCode ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("span", _hoisted_4, (0, vue.toDisplayString)($setup.selectedDialCode), 1)) : (0, vue.createCommentVNode)("v-if", true),
|
|
18533
18280
|
(0, vue.createElementVNode)("input", {
|
|
18281
|
+
ref: "inputRef",
|
|
18534
18282
|
value: $setup.displayValue,
|
|
18535
18283
|
type: "tel",
|
|
18536
18284
|
inputmode: "numeric",
|
|
18537
18285
|
autocomplete: "tel",
|
|
18538
18286
|
dir: "ltr",
|
|
18539
18287
|
"data-slot": "tel-input-field",
|
|
18288
|
+
name: $setup.props.name,
|
|
18540
18289
|
disabled: $setup.props.disabled || $setup.props.loading,
|
|
18541
18290
|
placeholder: $setup.effectivePlaceholder,
|
|
18542
18291
|
"aria-label": $setup.messages.phoneInputLabel,
|
|
18543
18292
|
"aria-invalid": $setup.visibleValidationState === "error" || void 0,
|
|
18544
18293
|
"aria-describedby": $setup.describedBy,
|
|
18294
|
+
"aria-errormessage": $setup.visibleValidationState === "error" ? $setup.helperId : void 0,
|
|
18295
|
+
"aria-busy": $setup.props.validating || void 0,
|
|
18545
18296
|
class: (0, vue.normalizeClass)($setup.cn("a-tel-input__input", $setup.props.inputClass)),
|
|
18546
18297
|
"data-has-dial": $setup.selectedDialCode ? "" : void 0,
|
|
18547
18298
|
onInput: $setup.handlePhoneInput,
|
|
18548
|
-
onChange: $setup.handlePhoneChange
|
|
18299
|
+
onChange: $setup.handlePhoneChange,
|
|
18300
|
+
onBlur: $setup.handleBlur,
|
|
18301
|
+
onFocus: $setup.handleFocus
|
|
18549
18302
|
}, null, 42, _hoisted_5),
|
|
18303
|
+
(0, vue.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. "),
|
|
18304
|
+
(0, vue.createVNode)(vue.Transition, { name: "a-tell-detect" }, {
|
|
18305
|
+
default: (0, vue.withCtx)(() => [$setup.props.validating ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_6, [(0, vue.renderSlot)(_ctx.$slots, "validating", {}, () => [(0, vue.createVNode)($setup["SpinnerIcon"], { class: "a-tel-input__detecting-icon" })], true)])) : (0, vue.createCommentVNode)("v-if", true)]),
|
|
18306
|
+
_: 3
|
|
18307
|
+
}),
|
|
18550
18308
|
(0, vue.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. "),
|
|
18551
18309
|
(0, vue.createVNode)(vue.Transition, { name: "a-tell-detect" }, {
|
|
18552
|
-
default: (0, vue.withCtx)(() => [$setup.isDetecting && !$setup.selectedIso2 && !$setup.detectionAttempted ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div",
|
|
18310
|
+
default: (0, vue.withCtx)(() => [$setup.isDetecting && !$setup.selectedIso2 && !$setup.detectionAttempted ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_7, [(0, vue.renderSlot)(_ctx.$slots, "detecting", {}, () => [(0, vue.createVNode)($setup["SpinnerIcon"], { class: "a-tel-input__detecting-icon" })], true)])) : (0, vue.createCommentVNode)("v-if", true)]),
|
|
18553
18311
|
_: 3
|
|
18554
18312
|
}),
|
|
18555
18313
|
(0, vue.createVNode)(vue.Transition, { name: "a-tell-country" }, {
|
|
18556
|
-
default: (0, vue.withCtx)(() => [!$setup.props.detectFromInput || $setup.selectedIso2 || $setup.detectionAttempted ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div",
|
|
18314
|
+
default: (0, vue.withCtx)(() => [!$setup.props.detectFromInput || $setup.selectedIso2 || $setup.detectionAttempted ? ((0, vue.openBlock)(), (0, vue.createElementBlock)("div", _hoisted_8, [(0, vue.createVNode)($setup["ACountrySelect"], {
|
|
18557
18315
|
selected: $setup.selectedIso2,
|
|
18558
|
-
"onUpdate:selected": _cache[0] || (_cache[0] = ($event) => $setup.selectedIso2 = $event),
|
|
18559
18316
|
"allowed-dial-codes": $setup.props.allowedDialCodes,
|
|
18560
18317
|
disabled: $setup.props.disabled || $setup.props.loading,
|
|
18561
18318
|
size: $setup.props.size,
|
|
@@ -18566,6 +18323,7 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
18566
18323
|
"suggested-label": $setup.messages.suggestedLabel,
|
|
18567
18324
|
"all-countries-label": $setup.messages.allCountriesLabel,
|
|
18568
18325
|
"country-label": $setup.messages.countryLabel,
|
|
18326
|
+
"onUpdate:selected": $setup.onPickerPick,
|
|
18569
18327
|
"select-country-label": $setup.messages.selectCountryLabel,
|
|
18570
18328
|
"flag-url": $setup.props.flagUrl,
|
|
18571
18329
|
searcher: $setup.props.searcher,
|
|
@@ -18672,7 +18430,7 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
18672
18430
|
}, () => [(0, vue.createElementVNode)("p", {
|
|
18673
18431
|
"data-slot": "tel-input-hint",
|
|
18674
18432
|
class: (0, vue.normalizeClass)($setup.cn("a-tel-input__hint", $setup.props.hintClass))
|
|
18675
|
-
}, (0, vue.toDisplayString)($setup.required.format_hint), 3)], true) : (0, vue.createCommentVNode)("v-if", true)], 8,
|
|
18433
|
+
}, (0, vue.toDisplayString)($setup.required.format_hint), 3)], true) : (0, vue.createCommentVNode)("v-if", true)], 8, _hoisted_9)
|
|
18676
18434
|
], 10, _hoisted_1);
|
|
18677
18435
|
}
|
|
18678
18436
|
var ATelInput_default = /* @__PURE__ */ export_helper_default(_sfc_main, [
|
|
@@ -18685,19 +18443,22 @@ exports.ACountryFlag = ACountryFlag_default;
|
|
|
18685
18443
|
exports.ACountrySelect = ACountrySelect_default;
|
|
18686
18444
|
exports.ATelInput = ATelInput_default;
|
|
18687
18445
|
exports.COUNTRY_RECENTS_KEY = COUNTRY_RECENTS_KEY;
|
|
18688
|
-
exports.DEFAULT_ERROR_MESSAGES = DEFAULT_ERROR_MESSAGES;
|
|
18689
|
-
exports.DEFAULT_MESSAGES = DEFAULT_MESSAGES;
|
|
18446
|
+
exports.DEFAULT_ERROR_MESSAGES = require_types.DEFAULT_ERROR_MESSAGES;
|
|
18447
|
+
exports.DEFAULT_MESSAGES = require_types.DEFAULT_MESSAGES;
|
|
18690
18448
|
exports.DIAL_TO_ISO2_FALLBACK = DIAL_TO_ISO2_FALLBACK;
|
|
18691
|
-
exports.
|
|
18692
|
-
exports.
|
|
18449
|
+
exports.FALLBACK_ISO2_LIST = FALLBACK_ISO2_LIST;
|
|
18450
|
+
exports.LOCALE_DIGIT_RANGES = require_usePhoneValidation.LOCALE_DIGIT_RANGES;
|
|
18451
|
+
exports.aTelInputVariants = require_types.aTelInputVariants;
|
|
18693
18452
|
exports.defaultFlagUrl = defaultFlagUrl;
|
|
18694
18453
|
exports.detectCountry = detectCountry;
|
|
18695
|
-
exports.localizeCountries = localizeCountries;
|
|
18696
|
-
exports.normalizeDigits = normalizeDigits;
|
|
18697
|
-
exports.resolveMessages = resolveMessages;
|
|
18454
|
+
exports.localizeCountries = require_usePhoneValidation.localizeCountries;
|
|
18455
|
+
exports.normalizeDigits = require_usePhoneValidation.normalizeDigits;
|
|
18456
|
+
exports.resolveMessages = require_types.resolveMessages;
|
|
18698
18457
|
exports.useCountryDetection = useCountryDetection;
|
|
18699
18458
|
exports.useCountryMatching = useCountryMatching;
|
|
18700
|
-
exports.
|
|
18459
|
+
exports.useCountrySelection = useCountrySelection;
|
|
18460
|
+
exports.usePhoneValidation = require_usePhoneValidation.usePhoneValidation;
|
|
18461
|
+
exports.useSyncedModel = useSyncedModel;
|
|
18701
18462
|
exports.useTelInputValidation = useTelInputValidation;
|
|
18702
18463
|
exports.useTypingPhase = useTypingPhase;
|
|
18703
18464
|
|