@alikhalilll/a-tel-input 1.0.2 → 1.1.1
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/.media/README.md +3 -0
- package/.media/hero.png +0 -0
- package/README.md +597 -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 +471 -695
- 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 +454 -658
- package/dist/index.js.map +1 -1
- package/dist/styles.css +20 -5
- 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 +34 -3
- package/src/components/ACountrySelect.vue +17 -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;
|
|
@@ -16951,11 +16603,6 @@ const _sfc_main$2$2 = /* @__PURE__ */ defineComponent({
|
|
|
16951
16603
|
const open = useModel(__props, "open");
|
|
16952
16604
|
const isDesktop = useMediaQuery(() => props.breakpoint);
|
|
16953
16605
|
/**
|
|
16954
|
-
* Pre-imported on both branches — do NOT lazy-load. Switching the component identity at runtime
|
|
16955
|
-
* means we still hydrate the right tree client-side.
|
|
16956
|
-
*/
|
|
16957
|
-
const Root = computed(() => isDesktop.value ? APopover_default : ADrawer_default);
|
|
16958
|
-
/**
|
|
16959
16606
|
* Per-branch `modal` resolution — the two roots interpret the prop differently:
|
|
16960
16607
|
*
|
|
16961
16608
|
* APopover (desktop, reka-ui): `modal=true` triggers `PopoverContentModal` + its
|
|
@@ -16972,7 +16619,7 @@ const _sfc_main$2$2 = /* @__PURE__ */ defineComponent({
|
|
|
16972
16619
|
return props.scrollLock === "body";
|
|
16973
16620
|
});
|
|
16974
16621
|
const drawerModal = computed(() => props.modal !== false);
|
|
16975
|
-
const
|
|
16622
|
+
const drawerNoBodyStyles = computed(() => props.scrollLock !== "body");
|
|
16976
16623
|
provideResponsivePopoverContext({
|
|
16977
16624
|
open: computed(() => open.value ?? false),
|
|
16978
16625
|
isDesktop: computed(() => isDesktop.value),
|
|
@@ -16982,10 +16629,15 @@ const _sfc_main$2$2 = /* @__PURE__ */ defineComponent({
|
|
|
16982
16629
|
props,
|
|
16983
16630
|
open,
|
|
16984
16631
|
isDesktop,
|
|
16985
|
-
Root,
|
|
16986
16632
|
rekaModal,
|
|
16987
16633
|
drawerModal,
|
|
16988
|
-
|
|
16634
|
+
drawerNoBodyStyles,
|
|
16635
|
+
get APopover() {
|
|
16636
|
+
return APopover_default;
|
|
16637
|
+
},
|
|
16638
|
+
get ADrawer() {
|
|
16639
|
+
return ADrawer_default;
|
|
16640
|
+
}
|
|
16989
16641
|
};
|
|
16990
16642
|
Object.defineProperty(__returned__, "__isScriptSetup", {
|
|
16991
16643
|
enumerable: false,
|
|
@@ -16995,15 +16647,30 @@ const _sfc_main$2$2 = /* @__PURE__ */ defineComponent({
|
|
|
16995
16647
|
}
|
|
16996
16648
|
});
|
|
16997
16649
|
function _sfc_render$2$2(_ctx, _cache, $props, $setup, $data, $options) {
|
|
16998
|
-
return openBlock(), createBlock(
|
|
16650
|
+
return $setup.isDesktop ? (openBlock(), createBlock($setup["APopover"], {
|
|
16651
|
+
key: 0,
|
|
16999
16652
|
open: $setup.open,
|
|
17000
16653
|
"onUpdate:open": _cache[0] || (_cache[0] = ($event) => $setup.open = $event),
|
|
17001
|
-
modal: $setup.
|
|
16654
|
+
modal: $setup.rekaModal,
|
|
17002
16655
|
"data-slot": "responsive-popover"
|
|
17003
16656
|
}, {
|
|
17004
|
-
default: withCtx(() => [renderSlot(_ctx.$slots, "default", { isDesktop:
|
|
16657
|
+
default: withCtx(() => [renderSlot(_ctx.$slots, "default", { isDesktop: true })]),
|
|
17005
16658
|
_: 3
|
|
17006
|
-
},
|
|
16659
|
+
}, 8, ["open", "modal"])) : (openBlock(), createBlock($setup["ADrawer"], {
|
|
16660
|
+
key: 1,
|
|
16661
|
+
open: $setup.open,
|
|
16662
|
+
"onUpdate:open": _cache[1] || (_cache[1] = ($event) => $setup.open = $event),
|
|
16663
|
+
modal: $setup.drawerModal,
|
|
16664
|
+
"no-body-styles": $setup.drawerNoBodyStyles,
|
|
16665
|
+
"data-slot": "responsive-popover"
|
|
16666
|
+
}, {
|
|
16667
|
+
default: withCtx(() => [renderSlot(_ctx.$slots, "default", { isDesktop: false })]),
|
|
16668
|
+
_: 3
|
|
16669
|
+
}, 8, [
|
|
16670
|
+
"open",
|
|
16671
|
+
"modal",
|
|
16672
|
+
"no-body-styles"
|
|
16673
|
+
]));
|
|
17007
16674
|
}
|
|
17008
16675
|
var AResponsivePopover_default = /* @__PURE__ */ export_helper_default$3(_sfc_main$2$2, [["render", _sfc_render$2$2], ["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/AResponsivePopover/src/components/AResponsivePopover.vue"]]);
|
|
17009
16676
|
const _sfc_main$1$2 = /* @__PURE__ */ defineComponent({
|
|
@@ -17116,7 +16783,7 @@ const _sfc_main$9 = /* @__PURE__ */ defineComponent({
|
|
|
17116
16783
|
if (typeof document === "undefined") return [];
|
|
17117
16784
|
return Array.from(document.querySelectorAll("[data-responsive-popover-scroll-container=\"true\"]"));
|
|
17118
16785
|
},
|
|
17119
|
-
active: computed(() => !!ctx?.open.value &&
|
|
16786
|
+
active: computed(() => !!ctx?.open.value && scrollLockMode.value === "events")
|
|
17120
16787
|
});
|
|
17121
16788
|
const __returned__ = {
|
|
17122
16789
|
props,
|
|
@@ -17162,7 +16829,8 @@ function _sfc_render$9(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
17162
16829
|
])) : (openBlock(), createBlock($setup["ADrawerContent"], {
|
|
17163
16830
|
key: 1,
|
|
17164
16831
|
class: normalizeClass($setup.mergedClass),
|
|
17165
|
-
"data-slot": "responsive-popover-content"
|
|
16832
|
+
"data-slot": "responsive-popover-content",
|
|
16833
|
+
"data-responsive-popover-scroll-container": "true"
|
|
17166
16834
|
}, {
|
|
17167
16835
|
default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
|
|
17168
16836
|
_: 3
|
|
@@ -17821,7 +17489,7 @@ const _hoisted_6$1 = {
|
|
|
17821
17489
|
};
|
|
17822
17490
|
const _hoisted_7$1 = { class: "a-country-select__list" };
|
|
17823
17491
|
const _hoisted_8$1 = { class: "a-country-select__loading" };
|
|
17824
|
-
const _hoisted_9 = { class: "a-country-select__empty" };
|
|
17492
|
+
const _hoisted_9$1 = { class: "a-country-select__empty" };
|
|
17825
17493
|
const _hoisted_10 = {
|
|
17826
17494
|
key: 0,
|
|
17827
17495
|
"data-slot": "country-select-group",
|
|
@@ -17919,7 +17587,7 @@ function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
17919
17587
|
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
17588
|
key: 1,
|
|
17921
17589
|
query: $setup.search
|
|
17922
|
-
}, () => [createElementVNode("div", _hoisted_9, toDisplayString($setup.props.emptyText), 1)], true) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [
|
|
17590
|
+
}, () => [createElementVNode("div", _hoisted_9$1, toDisplayString($setup.props.emptyText), 1)], true) : (openBlock(), createElementBlock(Fragment, { key: 2 }, [
|
|
17923
17591
|
createCommentVNode(" Suggested group "),
|
|
17924
17592
|
$setup.suggested.length > 0 ? (openBlock(), createElementBlock("section", _hoisted_10, [renderSlot(_ctx.$slots, "group-header", {
|
|
17925
17593
|
label: $setup.props.suggestedLabel,
|
|
@@ -18048,6 +17716,27 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18048
17716
|
required: false,
|
|
18049
17717
|
skipCheck: true
|
|
18050
17718
|
},
|
|
17719
|
+
modelValue: {
|
|
17720
|
+
type: String,
|
|
17721
|
+
required: false
|
|
17722
|
+
},
|
|
17723
|
+
name: {
|
|
17724
|
+
type: String,
|
|
17725
|
+
required: false
|
|
17726
|
+
},
|
|
17727
|
+
error: {
|
|
17728
|
+
type: [String, null],
|
|
17729
|
+
required: false
|
|
17730
|
+
},
|
|
17731
|
+
validating: {
|
|
17732
|
+
type: Boolean,
|
|
17733
|
+
required: false
|
|
17734
|
+
},
|
|
17735
|
+
validateOn: {
|
|
17736
|
+
type: String,
|
|
17737
|
+
required: false,
|
|
17738
|
+
default: "change"
|
|
17739
|
+
},
|
|
18051
17740
|
placeholder: {
|
|
18052
17741
|
type: String,
|
|
18053
17742
|
required: false,
|
|
@@ -18190,28 +17879,65 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18190
17879
|
type: [Number, null],
|
|
18191
17880
|
default: null
|
|
18192
17881
|
},
|
|
18193
|
-
"countryModifiers": {}
|
|
17882
|
+
"countryModifiers": {},
|
|
17883
|
+
"modelValue": {
|
|
17884
|
+
type: String,
|
|
17885
|
+
default: ""
|
|
17886
|
+
},
|
|
17887
|
+
"modelModifiers": {}
|
|
18194
17888
|
}),
|
|
18195
|
-
emits: [
|
|
18196
|
-
|
|
17889
|
+
emits: /* @__PURE__ */ mergeModels([
|
|
17890
|
+
"update:modelValue",
|
|
17891
|
+
"update:phone",
|
|
17892
|
+
"update:country",
|
|
17893
|
+
"blur",
|
|
17894
|
+
"focus"
|
|
17895
|
+
], [
|
|
17896
|
+
"update:phone",
|
|
17897
|
+
"update:country",
|
|
17898
|
+
"update:modelValue"
|
|
17899
|
+
]),
|
|
17900
|
+
setup(__props, { expose: __expose, emit: __emit }) {
|
|
18197
17901
|
const props = __props;
|
|
17902
|
+
const emit = __emit;
|
|
18198
17903
|
const phone = useModel(__props, "phone");
|
|
18199
17904
|
/** Public `v-model:country` — the **dial number** (e.g. `20` for Egypt, `44` for the UK,
|
|
18200
17905
|
* `1` for the NANP block). `null` means no country selected. Internally the component
|
|
18201
17906
|
* tracks a richer ISO2 code (`selectedIso2`) because dial codes alone can't disambiguate
|
|
18202
17907
|
* NANP (`+1` covers 25+ countries) — the picker still needs an exact country. */
|
|
18203
17908
|
const country = useModel(__props, "country");
|
|
18204
|
-
/**
|
|
18205
|
-
*
|
|
18206
|
-
|
|
17909
|
+
/**
|
|
17910
|
+
* Default v-model — the canonical **E.164** string (e.g. `'+201066105963'`).
|
|
17911
|
+
*
|
|
17912
|
+
* Single-string contract for VeeValidate's `<Field v-slot="{ field }">` pattern
|
|
17913
|
+
* (`v-bind="field"`), native `<form>` submission, or any `v-model="phoneE164"`
|
|
17914
|
+
* consumer. Bind it with:
|
|
17915
|
+
*
|
|
17916
|
+
* <ATelInput v-model="phoneE164" />
|
|
17917
|
+
*
|
|
17918
|
+
* <VeeField v-slot="{ field, errors }" name="phone">
|
|
17919
|
+
* <ATelInput v-bind="field" :error="errors[0]" />
|
|
17920
|
+
* </VeeField>
|
|
17921
|
+
*
|
|
17922
|
+
* When set externally, the value is parsed via libphonenumber-js → the country
|
|
17923
|
+
* picker and the digits-only `phone` model are derived from it. When the user
|
|
17924
|
+
* types or picks a country, the composed E.164 is written back out. Stays in
|
|
17925
|
+
* sync with `v-model:phone` / `v-model:country` — you can use either contract.
|
|
17926
|
+
*/
|
|
17927
|
+
const modelValue = useModel(__props, "modelValue");
|
|
17928
|
+
/** The picker selection state machine — `iso2` is the internal source of truth, `source`
|
|
17929
|
+
* records where the current selection came from, `detectionLocked` answers "should
|
|
17930
|
+
* typed-input detection re-route the picker on the next burst?". Single mutator: `set`.
|
|
17931
|
+
* Replaces the historical flag soup (`userPickedCountry` / `autoSettingCountry` /
|
|
17932
|
+
* `inputDetectionApplied`). */
|
|
17933
|
+
const selection = useCountrySelection();
|
|
17934
|
+
const selectedIso2 = selection.iso2;
|
|
18207
17935
|
const { getCountries, validate, getRequiredInfo, getCountryByValue, getCountriesByDial } = usePhoneValidation();
|
|
18208
17936
|
const { resolveCountryIdentifier, dialNumberFor, matchLeadingDialCode } = useCountryMatching({
|
|
18209
17937
|
getCountryByValue,
|
|
18210
17938
|
getCountriesByDial
|
|
18211
17939
|
});
|
|
18212
17940
|
getCountries();
|
|
18213
|
-
const userPickedCountry = ref(false);
|
|
18214
|
-
const autoSettingCountry = ref(false);
|
|
18215
17941
|
/** Silently resolved via IP/timezone/locale when `detectFromInput` is on — used as a hint
|
|
18216
17942
|
* so local-format numbers (e.g. Egyptian `01066105963`) can be parsed without a `+` prefix.
|
|
18217
17943
|
* Seeded from `defaultCountry` so it has a usable value before async detection resolves. */
|
|
@@ -18225,18 +17951,28 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18225
17951
|
currentIso2: selectedIso2.value
|
|
18226
17952
|
});
|
|
18227
17953
|
}
|
|
17954
|
+
/** User explicitly picked a country from the picker — locks the selection so subsequent
|
|
17955
|
+
* typed-input detection cannot churn the picker. */
|
|
17956
|
+
function onPickerPick(iso2) {
|
|
17957
|
+
selection.set(iso2, "picker");
|
|
17958
|
+
}
|
|
18228
17959
|
const typing = useTypingPhase({
|
|
18229
17960
|
debounceMs: computed(() => Math.max(0, props.detectDebounceMs)),
|
|
18230
17961
|
onSettle: () => {
|
|
18231
17962
|
if (!props.detectFromInput) return;
|
|
18232
|
-
if (
|
|
17963
|
+
if (selection.detectionLocked.value) return;
|
|
18233
17964
|
const current = phone.value;
|
|
18234
17965
|
if (!current) return;
|
|
17966
|
+
const typedInternational = (displayValue.value ?? "").trimStart().startsWith("+");
|
|
17967
|
+
if (selectedIso2.value && !typedInternational) return;
|
|
18235
17968
|
typing.markDetectionAttempt();
|
|
18236
17969
|
const match = tryMatchPhone(current);
|
|
18237
17970
|
if (!match) return;
|
|
18238
|
-
|
|
18239
|
-
|
|
17971
|
+
if (match.country.value === selectedIso2.value && match.nationalNumber === phone.value) {
|
|
17972
|
+
selection.source.value = "input";
|
|
17973
|
+
return;
|
|
17974
|
+
}
|
|
17975
|
+
selection.set(match.country.value, "input");
|
|
18240
17976
|
phone.value = match.nationalNumber;
|
|
18241
17977
|
}
|
|
18242
17978
|
});
|
|
@@ -18247,8 +17983,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18247
17983
|
const seed = resolveCountryIdentifier(props.defaultCountry);
|
|
18248
17984
|
if (seed) {
|
|
18249
17985
|
inferredCountry.value = seed;
|
|
18250
|
-
|
|
18251
|
-
selectedIso2.value = seed;
|
|
17986
|
+
selection.set(seed, "default");
|
|
18252
17987
|
return;
|
|
18253
17988
|
}
|
|
18254
17989
|
}
|
|
@@ -18267,43 +18002,31 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18267
18002
|
const iso2 = detected ? detected.toUpperCase() : "";
|
|
18268
18003
|
if (props.detectFromInput) {
|
|
18269
18004
|
inferredCountry.value = iso2;
|
|
18270
|
-
if (phone.value && !
|
|
18005
|
+
if (phone.value && !selection.detectionLocked.value && !selectedIso2.value) {
|
|
18271
18006
|
const match = tryMatchPhone(phone.value);
|
|
18272
18007
|
if (match) {
|
|
18273
|
-
|
|
18274
|
-
selectedIso2.value = match.country.value;
|
|
18008
|
+
selection.set(match.country.value, "input");
|
|
18275
18009
|
phone.value = match.nationalNumber;
|
|
18276
18010
|
}
|
|
18277
18011
|
}
|
|
18278
18012
|
return;
|
|
18279
18013
|
}
|
|
18280
|
-
if (!selectedIso2.value && iso2)
|
|
18281
|
-
autoSettingCountry.value = true;
|
|
18282
|
-
selectedIso2.value = iso2;
|
|
18283
|
-
}
|
|
18014
|
+
if (!selectedIso2.value && iso2) selection.set(iso2, "env");
|
|
18284
18015
|
});
|
|
18285
|
-
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
|
|
18289
|
-
|
|
18290
|
-
if (
|
|
18291
|
-
|
|
18016
|
+
useSyncedModel({
|
|
18017
|
+
model: country,
|
|
18018
|
+
triggers: [selectedIso2],
|
|
18019
|
+
compose: () => selectedIso2.value ? dialNumberFor(selectedIso2.value) : null,
|
|
18020
|
+
apply: (next) => {
|
|
18021
|
+
if (next == null) {
|
|
18022
|
+
selection.clear();
|
|
18023
|
+
return;
|
|
18024
|
+
}
|
|
18025
|
+
if (dialNumberFor(selectedIso2.value) === next) return;
|
|
18026
|
+
const iso2 = resolveCountryIdentifier(String(next));
|
|
18027
|
+
if (iso2) selection.set(iso2, "external");
|
|
18292
18028
|
}
|
|
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" });
|
|
18029
|
+
});
|
|
18307
18030
|
/** The string shown in the `<input>`. Deliberately decoupled from `phone` (the digits-only
|
|
18308
18031
|
* model) so the visible field is NOT rewritten mid-edit — non-digits / alternative numerals
|
|
18309
18032
|
* are normalized into `phone` immediately, but the displayed value is only cleaned up once
|
|
@@ -18312,6 +18035,29 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18312
18035
|
/** Set when the in-flight `phone` change came from the user typing — tells the `phone`
|
|
18313
18036
|
* watcher to leave `displayValue` alone (the user is still editing it). */
|
|
18314
18037
|
let phoneEditedByInput = false;
|
|
18038
|
+
useSyncedModel({
|
|
18039
|
+
model: modelValue,
|
|
18040
|
+
triggers: [phone, selectedIso2],
|
|
18041
|
+
compose: () => {
|
|
18042
|
+
if (!selectedIso2.value || !phone.value) return "";
|
|
18043
|
+
return validate({
|
|
18044
|
+
country: { iso2: selectedIso2.value },
|
|
18045
|
+
phone: phone.value
|
|
18046
|
+
}).full_phone ?? "";
|
|
18047
|
+
},
|
|
18048
|
+
apply: (next) => {
|
|
18049
|
+
const trimmed = String(next ?? "").trim();
|
|
18050
|
+
if (!trimmed) {
|
|
18051
|
+
if (phone.value !== "") phone.value = "";
|
|
18052
|
+
if (selectedIso2.value !== "") selection.clear();
|
|
18053
|
+
return;
|
|
18054
|
+
}
|
|
18055
|
+
const parsed = parsePhoneNumberFromString(trimmed.startsWith("+") ? trimmed : `+${trimmed.replace(/^\+/, "")}`);
|
|
18056
|
+
if (!parsed || !parsed.country) return;
|
|
18057
|
+
if (selectedIso2.value !== parsed.country) selection.set(parsed.country, "external");
|
|
18058
|
+
if (phone.value !== parsed.nationalNumber) phone.value = parsed.nationalNumber;
|
|
18059
|
+
}
|
|
18060
|
+
});
|
|
18315
18061
|
function commitPhone(value) {
|
|
18316
18062
|
phoneEditedByInput = true;
|
|
18317
18063
|
phone.value = value;
|
|
@@ -18322,11 +18068,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18322
18068
|
const cleaned = normalizeDigits(target.value).replace(/\D/g, "");
|
|
18323
18069
|
if (!cleaned) {
|
|
18324
18070
|
typing.reset();
|
|
18325
|
-
if (props.detectFromInput)
|
|
18326
|
-
autoSettingCountry.value = true;
|
|
18327
|
-
selectedIso2.value = "";
|
|
18328
|
-
userPickedCountry.value = false;
|
|
18329
|
-
}
|
|
18071
|
+
if (props.detectFromInput) selection.clear();
|
|
18330
18072
|
commitPhone("");
|
|
18331
18073
|
return;
|
|
18332
18074
|
}
|
|
@@ -18359,6 +18101,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18359
18101
|
* `undefined` so it inherits from the page. The field row itself is always LTR so the
|
|
18360
18102
|
* dial prefix / digits / flag trigger keep a consistent order. */
|
|
18361
18103
|
const dirAttr = computed(() => props.dir === "ltr" || props.dir === "rtl" ? props.dir : void 0);
|
|
18104
|
+
/** Set to `true` the first time the input is blurred. Drives `validateOn: 'blur'`. */
|
|
18105
|
+
const hasBlurred = ref(false);
|
|
18362
18106
|
const { validation, required, validationState, visibleValidationState, errorMessage, showError, showHint, selectedDialCode } = useTelInputValidation({
|
|
18363
18107
|
validate,
|
|
18364
18108
|
getRequiredInfo,
|
|
@@ -18367,15 +18111,35 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18367
18111
|
phone,
|
|
18368
18112
|
selectedIso2,
|
|
18369
18113
|
hasFinishedTyping,
|
|
18114
|
+
hasBlurred,
|
|
18370
18115
|
messages
|
|
18371
18116
|
}, {
|
|
18372
18117
|
locale: () => props.locale,
|
|
18373
18118
|
showValidation: () => props.showValidation,
|
|
18374
|
-
errorMessages: () => props.errorMessages
|
|
18119
|
+
errorMessages: () => props.errorMessages,
|
|
18120
|
+
validateOn: () => props.validateOn,
|
|
18121
|
+
externalError: () => props.error
|
|
18375
18122
|
});
|
|
18376
18123
|
const effectivePlaceholder = computed(() => props.placeholder || required.value?.format_hint || messages.value.phoneInputLabel);
|
|
18377
18124
|
const helperId = useId();
|
|
18378
18125
|
const describedBy = computed(() => showError.value || showHint.value ? helperId : void 0);
|
|
18126
|
+
const inputRef = ref(null);
|
|
18127
|
+
function handleBlur(e) {
|
|
18128
|
+
hasBlurred.value = true;
|
|
18129
|
+
emit("blur", e);
|
|
18130
|
+
}
|
|
18131
|
+
function handleFocus(e) {
|
|
18132
|
+
emit("focus", e);
|
|
18133
|
+
}
|
|
18134
|
+
function focus(options) {
|
|
18135
|
+
inputRef.value?.focus(options);
|
|
18136
|
+
}
|
|
18137
|
+
function blur() {
|
|
18138
|
+
inputRef.value?.blur();
|
|
18139
|
+
}
|
|
18140
|
+
function select() {
|
|
18141
|
+
inputRef.value?.select();
|
|
18142
|
+
}
|
|
18379
18143
|
__expose({
|
|
18380
18144
|
validation,
|
|
18381
18145
|
required,
|
|
@@ -18384,12 +18148,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18384
18148
|
visibleValidationState,
|
|
18385
18149
|
isDetecting,
|
|
18386
18150
|
hasFinishedTyping,
|
|
18387
|
-
detectionAttempted
|
|
18151
|
+
detectionAttempted,
|
|
18152
|
+
focus,
|
|
18153
|
+
blur,
|
|
18154
|
+
select
|
|
18388
18155
|
});
|
|
18389
18156
|
const __returned__ = {
|
|
18390
18157
|
props,
|
|
18158
|
+
emit,
|
|
18391
18159
|
phone,
|
|
18392
18160
|
country,
|
|
18161
|
+
modelValue,
|
|
18162
|
+
selection,
|
|
18393
18163
|
selectedIso2,
|
|
18394
18164
|
getCountries,
|
|
18395
18165
|
validate,
|
|
@@ -18399,10 +18169,9 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18399
18169
|
resolveCountryIdentifier,
|
|
18400
18170
|
dialNumberFor,
|
|
18401
18171
|
matchLeadingDialCode,
|
|
18402
|
-
userPickedCountry,
|
|
18403
|
-
autoSettingCountry,
|
|
18404
18172
|
inferredCountry,
|
|
18405
18173
|
tryMatchPhone,
|
|
18174
|
+
onPickerPick,
|
|
18406
18175
|
typing,
|
|
18407
18176
|
isDetecting,
|
|
18408
18177
|
hasFinishedTyping,
|
|
@@ -18419,6 +18188,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18419
18188
|
handlePhoneChange,
|
|
18420
18189
|
messages,
|
|
18421
18190
|
dirAttr,
|
|
18191
|
+
hasBlurred,
|
|
18422
18192
|
validation,
|
|
18423
18193
|
required,
|
|
18424
18194
|
validationState,
|
|
@@ -18430,6 +18200,12 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
18430
18200
|
effectivePlaceholder,
|
|
18431
18201
|
helperId,
|
|
18432
18202
|
describedBy,
|
|
18203
|
+
inputRef,
|
|
18204
|
+
handleBlur,
|
|
18205
|
+
handleFocus,
|
|
18206
|
+
focus,
|
|
18207
|
+
blur,
|
|
18208
|
+
select,
|
|
18433
18209
|
get cn() {
|
|
18434
18210
|
return cn$2;
|
|
18435
18211
|
},
|
|
@@ -18471,25 +18247,34 @@ const _hoisted_4 = {
|
|
|
18471
18247
|
};
|
|
18472
18248
|
const _hoisted_5 = [
|
|
18473
18249
|
"value",
|
|
18250
|
+
"name",
|
|
18474
18251
|
"disabled",
|
|
18475
18252
|
"placeholder",
|
|
18476
18253
|
"aria-label",
|
|
18477
18254
|
"aria-invalid",
|
|
18478
18255
|
"aria-describedby",
|
|
18256
|
+
"aria-errormessage",
|
|
18257
|
+
"aria-busy",
|
|
18479
18258
|
"data-has-dial"
|
|
18480
18259
|
];
|
|
18481
18260
|
const _hoisted_6 = {
|
|
18261
|
+
key: 0,
|
|
18262
|
+
class: "a-tel-input__validating",
|
|
18263
|
+
"data-slot": "tel-input-validating",
|
|
18264
|
+
"aria-hidden": "true"
|
|
18265
|
+
};
|
|
18266
|
+
const _hoisted_7 = {
|
|
18482
18267
|
key: 0,
|
|
18483
18268
|
class: "a-tel-input__detecting",
|
|
18484
18269
|
"aria-hidden": "true",
|
|
18485
18270
|
"data-slot": "tel-input-detecting"
|
|
18486
18271
|
};
|
|
18487
|
-
const
|
|
18272
|
+
const _hoisted_8 = {
|
|
18488
18273
|
key: 0,
|
|
18489
18274
|
class: "a-tel-input__country-wrapper",
|
|
18490
18275
|
"data-slot": "tel-input-country-wrapper"
|
|
18491
18276
|
};
|
|
18492
|
-
const
|
|
18277
|
+
const _hoisted_9 = ["id"];
|
|
18493
18278
|
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
18494
18279
|
return openBlock(), createElementBlock("div", {
|
|
18495
18280
|
class: normalizeClass($setup.cn("a-tel-input", _ctx.$attrs.class)),
|
|
@@ -18507,31 +18292,41 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
18507
18292
|
renderSlot(_ctx.$slots, "prefix", {}, void 0, true),
|
|
18508
18293
|
$setup.selectedDialCode ? (openBlock(), createElementBlock("span", _hoisted_4, toDisplayString($setup.selectedDialCode), 1)) : createCommentVNode("v-if", true),
|
|
18509
18294
|
createElementVNode("input", {
|
|
18295
|
+
ref: "inputRef",
|
|
18510
18296
|
value: $setup.displayValue,
|
|
18511
18297
|
type: "tel",
|
|
18512
18298
|
inputmode: "numeric",
|
|
18513
18299
|
autocomplete: "tel",
|
|
18514
18300
|
dir: "ltr",
|
|
18515
18301
|
"data-slot": "tel-input-field",
|
|
18302
|
+
name: $setup.props.name,
|
|
18516
18303
|
disabled: $setup.props.disabled || $setup.props.loading,
|
|
18517
18304
|
placeholder: $setup.effectivePlaceholder,
|
|
18518
18305
|
"aria-label": $setup.messages.phoneInputLabel,
|
|
18519
18306
|
"aria-invalid": $setup.visibleValidationState === "error" || void 0,
|
|
18520
18307
|
"aria-describedby": $setup.describedBy,
|
|
18308
|
+
"aria-errormessage": $setup.visibleValidationState === "error" ? $setup.helperId : void 0,
|
|
18309
|
+
"aria-busy": $setup.props.validating || void 0,
|
|
18521
18310
|
class: normalizeClass($setup.cn("a-tel-input__input", $setup.props.inputClass)),
|
|
18522
18311
|
"data-has-dial": $setup.selectedDialCode ? "" : void 0,
|
|
18523
18312
|
onInput: $setup.handlePhoneInput,
|
|
18524
|
-
onChange: $setup.handlePhoneChange
|
|
18313
|
+
onChange: $setup.handlePhoneChange,
|
|
18314
|
+
onBlur: $setup.handleBlur,
|
|
18315
|
+
onFocus: $setup.handleFocus
|
|
18525
18316
|
}, null, 42, _hoisted_5),
|
|
18317
|
+
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. "),
|
|
18318
|
+
createVNode(Transition, { name: "a-tell-detect" }, {
|
|
18319
|
+
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)]),
|
|
18320
|
+
_: 3
|
|
18321
|
+
}),
|
|
18526
18322
|
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
18323
|
createVNode(Transition, { name: "a-tell-detect" }, {
|
|
18528
|
-
default: withCtx(() => [$setup.isDetecting && !$setup.selectedIso2 && !$setup.detectionAttempted ? (openBlock(), createElementBlock("div",
|
|
18324
|
+
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
18325
|
_: 3
|
|
18530
18326
|
}),
|
|
18531
18327
|
createVNode(Transition, { name: "a-tell-country" }, {
|
|
18532
|
-
default: withCtx(() => [!$setup.props.detectFromInput || $setup.selectedIso2 || $setup.detectionAttempted ? (openBlock(), createElementBlock("div",
|
|
18328
|
+
default: withCtx(() => [!$setup.props.detectFromInput || $setup.selectedIso2 || $setup.detectionAttempted ? (openBlock(), createElementBlock("div", _hoisted_8, [createVNode($setup["ACountrySelect"], {
|
|
18533
18329
|
selected: $setup.selectedIso2,
|
|
18534
|
-
"onUpdate:selected": _cache[0] || (_cache[0] = ($event) => $setup.selectedIso2 = $event),
|
|
18535
18330
|
"allowed-dial-codes": $setup.props.allowedDialCodes,
|
|
18536
18331
|
disabled: $setup.props.disabled || $setup.props.loading,
|
|
18537
18332
|
size: $setup.props.size,
|
|
@@ -18542,6 +18337,7 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
18542
18337
|
"suggested-label": $setup.messages.suggestedLabel,
|
|
18543
18338
|
"all-countries-label": $setup.messages.allCountriesLabel,
|
|
18544
18339
|
"country-label": $setup.messages.countryLabel,
|
|
18340
|
+
"onUpdate:selected": $setup.onPickerPick,
|
|
18545
18341
|
"select-country-label": $setup.messages.selectCountryLabel,
|
|
18546
18342
|
"flag-url": $setup.props.flagUrl,
|
|
18547
18343
|
searcher: $setup.props.searcher,
|
|
@@ -18648,7 +18444,7 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
18648
18444
|
}, () => [createElementVNode("p", {
|
|
18649
18445
|
"data-slot": "tel-input-hint",
|
|
18650
18446
|
class: normalizeClass($setup.cn("a-tel-input__hint", $setup.props.hintClass))
|
|
18651
|
-
}, toDisplayString($setup.required.format_hint), 3)], true) : createCommentVNode("v-if", true)], 8,
|
|
18447
|
+
}, toDisplayString($setup.required.format_hint), 3)], true) : createCommentVNode("v-if", true)], 8, _hoisted_9)
|
|
18652
18448
|
], 10, _hoisted_1);
|
|
18653
18449
|
}
|
|
18654
18450
|
var ATelInput_default = /* @__PURE__ */ export_helper_default(_sfc_main, [
|
|
@@ -18657,6 +18453,6 @@ var ATelInput_default = /* @__PURE__ */ export_helper_default(_sfc_main, [
|
|
|
18657
18453
|
["__file", "/Users/alikhalill/Desktop/my-projects/ali-nuxt-toolkit/packages/ui-components/ATelInput/src/components/ATelInput.vue"]
|
|
18658
18454
|
]);
|
|
18659
18455
|
//#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 };
|
|
18456
|
+
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
18457
|
|
|
18662
18458
|
//# sourceMappingURL=index.js.map
|