@desource/phone-mask-vue 0.3.0 → 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.
Files changed (44) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +157 -11
  3. package/dist/index.cjs +868 -978
  4. package/dist/index.js +868 -978
  5. package/dist/index.mjs +869 -979
  6. package/dist/phone-mask-vue.css +64 -64
  7. package/dist/types/components/PhoneInput.vue.d.ts +7 -7
  8. package/dist/types/components/PhoneInput.vue.d.ts.map +1 -1
  9. package/dist/types/composables/internal/useCopyAction.d.ts +14 -0
  10. package/dist/types/composables/internal/useCopyAction.d.ts.map +1 -0
  11. package/dist/types/composables/internal/useCountry.d.ts +22 -0
  12. package/dist/types/composables/internal/useCountry.d.ts.map +1 -0
  13. package/dist/types/composables/internal/useCountrySelector.d.ts +30 -0
  14. package/dist/types/composables/internal/useCountrySelector.d.ts.map +1 -0
  15. package/dist/types/composables/internal/useFormatter.d.ts +46 -0
  16. package/dist/types/composables/internal/useFormatter.d.ts.map +1 -0
  17. package/dist/types/composables/internal/useInputHandlers.d.ts +20 -0
  18. package/dist/types/composables/internal/useInputHandlers.d.ts.map +1 -0
  19. package/dist/types/composables/internal/useTheme.d.ts +10 -0
  20. package/dist/types/composables/internal/useTheme.d.ts.map +1 -0
  21. package/dist/types/composables/internal/useValidationHint.d.ts +6 -0
  22. package/dist/types/composables/internal/useValidationHint.d.ts.map +1 -0
  23. package/dist/types/composables/usePhoneMask.d.ts +8 -0
  24. package/dist/types/composables/usePhoneMask.d.ts.map +1 -0
  25. package/dist/types/composables/{useClipboard.d.ts → utility/useClipboard.d.ts} +1 -2
  26. package/dist/types/composables/utility/useClipboard.d.ts.map +1 -0
  27. package/dist/types/composables/utility/useTimer.d.ts +9 -0
  28. package/dist/types/composables/utility/useTimer.d.ts.map +1 -0
  29. package/dist/types/directives/vPhoneMask.d.ts +1 -2
  30. package/dist/types/directives/vPhoneMask.d.ts.map +1 -1
  31. package/dist/types/index.d.ts +7 -5
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/dist/types/types.d.ts +33 -9
  34. package/dist/types/types.d.ts.map +1 -1
  35. package/package.json +17 -11
  36. package/dist/types/composables/useClipboard.d.ts.map +0 -1
  37. package/dist/types/composables/useCountrySelector.d.ts +0 -21
  38. package/dist/types/composables/useCountrySelector.d.ts.map +0 -1
  39. package/dist/types/composables/useMask.d.ts +0 -20
  40. package/dist/types/composables/useMask.d.ts.map +0 -1
  41. package/dist/types/composables/usePhoneFormatter.d.ts +0 -16
  42. package/dist/types/composables/usePhoneFormatter.d.ts.map +0 -1
  43. package/dist/types/consts.d.ts +0 -4
  44. package/dist/types/consts.d.ts.map +0 -1
package/dist/index.cjs CHANGED
@@ -10,67 +10,92 @@ const t$1 = /^[a-z]{2}$/i, countryCodeEmoji = (o2) => {
10
10
  const e2 = [...o2.toUpperCase()].map((t2) => (t2.codePointAt(0) ?? 0) + 127397);
11
11
  return String.fromCodePoint(...e2);
12
12
  };
13
- const t = Object.entries(M), divideMask = (e2) => e2.split(/ (.*)/s);
13
+ const o$1 = "en", n$1 = /* @__PURE__ */ new Map(), getDisplayNames = (e2) => {
14
+ const t2 = e2.toLowerCase(), s2 = n$1.get(t2);
15
+ if (s2) return s2;
16
+ const r = new Intl.DisplayNames([e2], { type: "region" });
17
+ if (n$1.size >= 10) {
18
+ for (const e3 of n$1.keys()) if (e3 !== o$1) {
19
+ n$1.delete(e3);
20
+ break;
21
+ }
22
+ }
23
+ return n$1.set(t2, r), r;
24
+ }, s$1 = Object.entries(M), divideMask = (e2) => e2.split(/ (.*)/s);
14
25
  function getCodeAndMask(e2) {
15
- let n = "", t2 = "";
26
+ let t2 = "", o2 = "";
16
27
  if (Array.isArray(e2)) {
17
- const o2 = [];
18
- for (const t3 of e2) {
19
- const [e3, s2] = divideMask(t3);
20
- n || (n = e3), o2.push(s2);
28
+ const n2 = [];
29
+ for (const o3 of e2) {
30
+ const [e3, s2] = divideMask(o3);
31
+ t2 || (t2 = e3), n2.push(s2);
21
32
  }
22
- t2 = o2;
33
+ o2 = n2;
23
34
  } else {
24
- const [o2, s2] = divideMask(e2);
25
- n = o2, t2 = s2;
35
+ const [n2, s2] = divideMask(e2);
36
+ t2 = n2, o2 = s2;
26
37
  }
27
- return [n, t2];
38
+ return [t2, o2];
28
39
  }
29
- t.map(([e2, n]) => ({ id: e2, mask: n }));
30
- t.reduce((e2, [n, t2]) => {
31
- const [o2, s2] = getCodeAndMask(t2);
32
- return e2[n] = { code: o2, mask: s2 }, e2;
40
+ s$1.map(([e2, t2]) => ({ id: e2, mask: t2 }));
41
+ s$1.reduce((e2, [t2, o2]) => {
42
+ const [n2, s2] = getCodeAndMask(o2);
43
+ return e2[t2] = { code: n2, mask: s2 }, e2;
33
44
  }, {});
34
- t.map(([e2, n]) => {
35
- const [t2, o2] = getCodeAndMask(n);
36
- return { id: e2, code: t2, mask: o2 };
45
+ s$1.map(([e2, t2]) => {
46
+ const [o2, n2] = getCodeAndMask(t2);
47
+ return { id: e2, code: o2, mask: n2 };
37
48
  });
38
- t.reduce((e2, [t2, o2]) => {
39
- const [s2, a] = getCodeAndMask(o2);
40
- return e2[t2] = { code: s2, mask: a, flag: countryCodeEmoji(t2) }, e2;
49
+ s$1.reduce((e2, [o2, n2]) => {
50
+ const [s2, r] = getCodeAndMask(n2);
51
+ return e2[o2] = { code: s2, mask: r, flag: countryCodeEmoji(o2) }, e2;
41
52
  }, {});
42
- t.map(([e2, t2]) => {
43
- const [o2, s2] = getCodeAndMask(t2);
44
- return { id: e2, code: o2, mask: s2, flag: countryCodeEmoji(e2) };
53
+ s$1.map(([e2, o2]) => {
54
+ const [n2, s2] = getCodeAndMask(o2);
55
+ return { id: e2, code: n2, mask: s2, flag: countryCodeEmoji(e2) };
45
56
  });
46
57
  const MasksFullMap = (e2) => {
47
- const o2 = new Intl.DisplayNames([e2], { type: "region" });
48
- return t.reduce((e3, [t2, s2]) => {
49
- const [a, r] = getCodeAndMask(s2), d = o2.of(t2) ?? "";
50
- return e3[t2] = { code: a, mask: r, name: d, flag: countryCodeEmoji(t2) }, e3;
58
+ const o2 = getDisplayNames(e2);
59
+ return s$1.reduce((e3, [n2, s2]) => {
60
+ const [r, a] = getCodeAndMask(s2), d = o2.of(n2) ?? "";
61
+ return e3[n2] = { code: r, mask: a, name: d, flag: countryCodeEmoji(n2) }, e3;
51
62
  }, {});
52
63
  }, MasksFull = (e2) => {
53
- const o2 = new Intl.DisplayNames([e2], { type: "region" });
54
- return t.map(([e3, t2]) => {
55
- const [s2, a] = getCodeAndMask(t2);
56
- return { id: e3, code: s2, mask: a, name: o2.of(e3) ?? "", flag: countryCodeEmoji(e3) };
64
+ const o2 = getDisplayNames(e2);
65
+ return s$1.map(([e3, n2]) => {
66
+ const [s2, r] = getCodeAndMask(n2);
67
+ return { id: e3, code: s2, mask: r, name: o2.of(e3) ?? "", flag: countryCodeEmoji(e3) };
57
68
  });
58
- }, m = t.reduce((e2, [t2, o2]) => {
59
- const [s2, a] = getCodeAndMask(o2), r = new Intl.DisplayNames(["en"], { type: "region" });
60
- return e2[t2] = { code: s2, mask: a, name: r.of(t2) ?? "", flag: countryCodeEmoji(t2) }, e2;
61
- }, {}), i = t.map(([e2, t2]) => {
62
- const [o2, s2] = getCodeAndMask(t2);
63
- return { id: e2, code: o2, mask: s2, name: new Intl.DisplayNames(["en"], { type: "region" }).of(e2) ?? "", flag: countryCodeEmoji(e2) };
64
- }), g = countryCodeEmoji;
69
+ }, f = MasksFullMap(o$1);
70
+ MasksFull(o$1);
71
+ const k = countryCodeEmoji;
65
72
  function getNavigatorLang() {
66
- return "undefined" != typeof navigator && navigator.language || "en";
73
+ return globalThis.navigator?.language || "en";
67
74
  }
68
- function getMasksFullMapByLocale(e2) {
69
- return e2.toLowerCase().startsWith("en") ? m : MasksFullMap(e2);
75
+ function detectCountryFromLocale() {
76
+ try {
77
+ const t2 = getNavigatorLang();
78
+ try {
79
+ const r2 = new Intl.Locale(t2);
80
+ if (r2.region) return r2.region.toUpperCase();
81
+ } catch {
82
+ }
83
+ const r = t2.split(/[-_]/);
84
+ if (r.length > 1) return r[1]?.toUpperCase() || null;
85
+ } catch {
86
+ }
87
+ return null;
70
88
  }
71
- function getCountry(t2, n) {
72
- const e2 = getMasksFullMapByLocale(n), r = t2.toUpperCase();
73
- return r in e2 ? { id: r, ...e2[r] } : { id: "US", ...e2.US };
89
+ function hasCountry(t2) {
90
+ const e2 = f;
91
+ return t2.toUpperCase() in e2;
92
+ }
93
+ function getCountry(r, e2) {
94
+ const n2 = MasksFullMap(e2), o2 = r.toUpperCase();
95
+ return o2 in n2 ? { id: o2, ...n2[o2] } : { id: "US", ...n2.US };
96
+ }
97
+ function parseCountryCode(t2, r) {
98
+ return t2 && hasCountry(t2) ? t2.toUpperCase() : r || "";
74
99
  }
75
100
  function toArray(t2) {
76
101
  return Array.isArray(t2) ? t2 : [t2];
@@ -81,480 +106,478 @@ function countPlaceholders(t2) {
81
106
  function removeCountryCodePrefix(t2) {
82
107
  return t2.replace(/^\+\d+\s?/, "");
83
108
  }
84
- function pickMaskVariant(t2, n) {
109
+ function pickMaskVariant(t2, r) {
110
+ if (!t2.length) return "";
85
111
  if (1 === t2.length) return t2[0];
86
- const e2 = t2.map((t3) => ({ mask: t3, count: countPlaceholders(t3) })), r = e2.filter((t3) => t3.count >= n).sort((t3, n2) => t3.count - n2.count);
87
- if (r.length > 0) return r[0].mask;
88
- const o2 = e2.sort((t3, n2) => n2.count - t3.count)[0];
112
+ const e2 = t2.map((t3) => ({ mask: t3, count: countPlaceholders(t3) })), n2 = e2.filter((t3) => t3.count >= r).sort((t3, r2) => t3.count - r2.count);
113
+ if (n2.length > 0) return n2[0].mask;
114
+ const o2 = e2.sort((t3, r2) => r2.count - t3.count)[0];
89
115
  return o2 ? o2.mask : t2[0];
90
116
  }
91
- function formatDigitsWithMap(t2, n) {
117
+ function formatDigitsWithMap(t2, r) {
92
118
  let e2 = "";
93
- const r = [];
119
+ const n2 = [];
94
120
  let o2 = 0;
95
- const a = n.length, i2 = t2.length;
96
- for (let u = 0; u < i2; u++) {
97
- const i3 = t2[u];
98
- if ("#" === i3) {
121
+ const a = r.length, s2 = t2.length;
122
+ for (let c = 0; c < s2; c++) {
123
+ const s3 = t2[c];
124
+ if ("#" === s3) {
99
125
  if (!(o2 < a)) break;
100
- e2 += n[o2], r.push(o2), o2++;
126
+ e2 += r[o2], n2.push(o2), o2++;
101
127
  } else {
102
- const n2 = -1 !== t2.indexOf("#", u + 1) && o2 < a;
103
- (e2.length > 0 || n2) && (e2 += i3, r.push(-1));
128
+ const r2 = -1 !== t2.indexOf("#", c + 1) && o2 < a;
129
+ (e2.length > 0 || r2) && (e2 += s3, n2.push(-1));
130
+ }
131
+ }
132
+ return { display: e2, map: n2 };
133
+ }
134
+ function filterCountries(t2, r) {
135
+ const e2 = r.trim().toUpperCase();
136
+ if (!e2) return t2;
137
+ const n2 = e2.replace(/\D/g, ""), o2 = n2.length > 0;
138
+ return t2.map((t3) => {
139
+ const r2 = t3.name.toUpperCase(), a = t3.id.toUpperCase(), s2 = t3.code.toUpperCase(), c = t3.code.replace(/\D/g, "");
140
+ let i = 0;
141
+ return r2.startsWith(e2) ? i = 1e3 : r2.includes(e2) && (i = 500), s2.startsWith(e2) ? i += 100 : s2.includes(e2) && (i += 50), a === e2 ? i += 200 : a.startsWith(e2) && (i += 150), o2 && c.startsWith(n2) ? i += 80 : o2 && c.includes(n2) && (i += 40), { country: t3, score: i };
142
+ }).filter((t3) => t3.score > 0).sort((t3, r2) => r2.score === t3.score ? t3.country.name.localeCompare(r2.country.name) : r2.score - t3.score).map((t3) => t3.country);
143
+ }
144
+ const e$1 = [" ", "-", "(", ")"], t = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "Tab"], n = /[^\d\s\-()]/;
145
+ function removeDigitsRange(e2, t2, n2) {
146
+ return { newDigits: e2.slice(0, t2) + e2.slice(n2), caretDigitIndex: t2 };
147
+ }
148
+ function removeSelectedDigits(e2, t2, n2, i) {
149
+ if (n2 === i) return;
150
+ const r = t2.getDigitRange(e2, n2, i);
151
+ if (!r) return;
152
+ const [s2, g] = r;
153
+ return removeDigitsRange(e2, s2, g);
154
+ }
155
+ function extractDigits(e2, t2) {
156
+ const n2 = e2.replace(/\D/g, "");
157
+ return t2 ? n2.slice(0, t2) : n2;
158
+ }
159
+ function getSelection(e2) {
160
+ return e2 ? [e2.selectionStart ?? 0, e2.selectionEnd ?? 0] : [0, 0];
161
+ }
162
+ function setCaret(e2, t2) {
163
+ if (e2) try {
164
+ e2.setSelectionRange(t2, t2);
165
+ } catch {
166
+ }
167
+ }
168
+ function processBeforeInput(e2) {
169
+ if (!e2.target) return;
170
+ const t2 = e2.target, i = e2.data;
171
+ "insertText" === e2.inputType && i && (n.test(i) || " " === i && t2.value.endsWith(" ")) && e2.preventDefault();
172
+ }
173
+ function processInput(e2, t2) {
174
+ if (!e2.target) return;
175
+ const n2 = e2.target, { formatter: i } = t2, r = i.getMaxDigits(), s2 = extractDigits(n2.value, r);
176
+ return { newDigits: s2, caretDigitIndex: s2.length };
177
+ }
178
+ function processKeydown(n2, i) {
179
+ if (!n2.target) return;
180
+ const r = n2.target, { digits: s2, formatter: g } = i;
181
+ if ((function shouldIgnoreKeydown(e2) {
182
+ return e2.ctrlKey || e2.metaKey || e2.altKey || t.includes(e2.key);
183
+ })(n2)) return;
184
+ const [o2, c] = getSelection(r), a = r.value;
185
+ return "Backspace" === n2.key ? (n2.preventDefault(), removeSelectedDigits(s2, g, o2, c) ?? (function removePreviousDigit(t2, n3, i2, r2) {
186
+ if (r2 <= 0) return;
187
+ let s3 = r2 - 1;
188
+ for (; s3 >= 0 && e$1.includes(i2[s3]); ) s3--;
189
+ if (s3 < 0) return;
190
+ const g2 = n3.getDigitRange(t2, s3, s3 + 1);
191
+ if (!g2) return;
192
+ const [o3] = g2;
193
+ return removeDigitsRange(t2, o3, o3 + 1);
194
+ })(s2, g, a, o2)) : "Delete" === n2.key ? (n2.preventDefault(), removeSelectedDigits(s2, g, o2, c) ?? (function removeNextDigit(e2, t2, n3, i2) {
195
+ if (i2 >= n3.length) return;
196
+ const r2 = t2.getDigitRange(e2, i2, n3.length);
197
+ if (!r2) return;
198
+ const [s3] = r2;
199
+ return removeDigitsRange(e2, s3, s3 + 1);
200
+ })(s2, g, a, o2)) : void (/^\d$/.test(n2.key) ? s2.length >= g.getMaxDigits() && n2.preventDefault() : 1 === n2.key.length && n2.preventDefault());
201
+ }
202
+ function processPaste(e2, t2) {
203
+ if (!e2.target) return;
204
+ e2.preventDefault();
205
+ const n2 = e2.target, { digits: i, formatter: r } = t2, s2 = e2.clipboardData?.getData("text") || "", g = r.getMaxDigits(), o2 = extractDigits(s2, g);
206
+ if (0 === o2.length) return;
207
+ const [c, a] = getSelection(n2);
208
+ if (c !== a) {
209
+ const e3 = r.getDigitRange(i, c, a);
210
+ if (e3) {
211
+ const [t3, n3] = e3;
212
+ return { newDigits: extractDigits(i.slice(0, t3) + o2 + i.slice(n3), g), caretDigitIndex: t3 + o2.length };
104
213
  }
105
214
  }
106
- return { display: e2, map: r };
215
+ const u = r.getDigitRange(i, 0, c), l = u ? u[1] : 0;
216
+ return { newDigits: extractDigits(i.slice(0, l) + o2 + i.slice(l), g), caretDigitIndex: l + o2.length };
217
+ }
218
+ function createPhoneFormatter(o2) {
219
+ const i = toArray(o2.mask), l = i.map((n2) => countPlaceholders(removeCountryCodePrefix(n2))), s2 = Math.max(...l), getMask = (t2) => {
220
+ const n2 = pickMaskVariant(i, t2);
221
+ return removeCountryCodePrefix(n2);
222
+ };
223
+ return { formatDisplay: (t2) => {
224
+ const e2 = getMask(t2.length);
225
+ return formatDigitsWithMap(e2, t2).display;
226
+ }, getMaxDigits: () => s2, getPlaceholder: () => getMask(0), getCaretPosition: (t2) => {
227
+ const e2 = Math.max(0, t2);
228
+ if (0 === e2) return 0;
229
+ const r = getMask(e2), { display: a } = formatDigitsWithMap(r, "0".repeat(e2));
230
+ return a.length;
231
+ }, getDigitRange: (t2, e2, r) => {
232
+ const a = getMask(t2.length), { map: o3 } = formatDigitsWithMap(a, t2);
233
+ let i2 = 1 / 0, l2 = -1 / 0;
234
+ for (let t3 = e2; t3 < r && t3 < o3.length; t3++) {
235
+ const e3 = o3[t3];
236
+ void 0 !== e3 && e3 >= 0 && (i2 = Math.min(i2, e3), l2 = Math.max(l2, e3));
237
+ }
238
+ return i2 === 1 / 0 ? null : [i2, l2 + 1];
239
+ }, isComplete: (t2) => l.includes(t2.length) };
107
240
  }
108
241
  const o = "https://ipapi.co/json/", e = 1500, p = "@desource/phone-mask:geo", s = 864e5;
109
- async function detectCountryFromGeoIP(e$1 = o, r = e) {
110
- const c = new AbortController(), n = setTimeout(() => c.abort(), r);
242
+ async function detectCountryFromGeoIP(t2 = o, r = e) {
243
+ const n2 = new AbortController(), c = setTimeout(() => n2.abort(), r);
111
244
  try {
112
- const t2 = await fetch(e$1, { signal: c.signal, headers: { Accept: "application/json" } });
113
- if (clearTimeout(n), !t2.ok) return null;
114
- const o2 = await t2.json();
115
- return (o2.country || o2.country_code || o2.countryCode || o2.country_code2 || "").toString().toUpperCase() || null;
245
+ const o2 = await fetch(t2, { signal: n2.signal, headers: { Accept: "application/json" } });
246
+ if (clearTimeout(c), !o2.ok) return null;
247
+ const e2 = await o2.json();
248
+ return (e2.country || e2.country_code || e2.countryCode || e2.country_code2 || "").toString().toUpperCase() || null;
116
249
  } catch {
117
- return clearTimeout(n), null;
250
+ return clearTimeout(c), null;
118
251
  }
119
252
  }
120
- async function detectByGeoIp(t2) {
253
+ async function detectByGeoIp() {
121
254
  try {
122
255
  const o3 = localStorage.getItem(p);
123
256
  if (o3) {
124
- const c = JSON.parse(o3), n = Date.now() - c.ts > s;
125
- if (!n && c.country_code && t2(c.country_code)) return c.country_code.toUpperCase();
126
- n && localStorage.removeItem(p);
257
+ const e3 = JSON.parse(o3), c = Date.now() - e3.ts > s, a = parseCountryCode(e3.country_code);
258
+ if (a && !c) return a;
259
+ localStorage.removeItem(p);
127
260
  }
128
261
  } catch {
129
262
  }
130
- const o2 = await detectCountryFromGeoIP();
131
- if (o2 && t2(o2)) {
263
+ const o2 = await detectCountryFromGeoIP(), e2 = parseCountryCode(o2);
264
+ if (e2) {
132
265
  try {
133
- localStorage.setItem(p, JSON.stringify({ country_code: o2, ts: Date.now() }));
266
+ const t2 = JSON.stringify({ country_code: e2, ts: Date.now() });
267
+ localStorage.setItem(p, t2);
134
268
  } catch {
135
269
  }
136
- return o2;
270
+ return e2;
137
271
  }
138
272
  return null;
139
273
  }
140
- const emptyCountry = { id: "", code: "", mask: "", flag: "", name: "" };
141
- function useCountrySelector(usedLocale) {
142
- const isEnLocale = vue.computed(() => usedLocale.value.toLowerCase().startsWith("en"));
143
- const countries = vue.computed(() => isEnLocale.value ? i : MasksFull(usedLocale.value));
144
- const countriesMap = vue.computed(() => isEnLocale.value ? m : MasksFullMap(usedLocale.value));
145
- const selectedId = vue.ref(countries.value[0]?.id || "");
146
- const dropdownOpened = vue.ref(false);
147
- const hasDropdown = vue.ref(true);
148
- const search = vue.ref("");
149
- const focusedIndex = vue.ref(0);
150
- const selected = vue.computed(() => {
151
- const id = selectedId.value;
152
- const found = countriesMap.value[id];
153
- return found ? { id, ...found } : countries.value[0] || emptyCountry;
154
- });
155
- const hasCountry = (id) => {
156
- const _id = id.toUpperCase();
157
- return !!countriesMap.value[_id];
274
+ function useCountry({
275
+ country: countryOption,
276
+ locale: localeOption,
277
+ detect,
278
+ onCountryChange
279
+ } = {}) {
280
+ const locale = vue.computed(() => vue.toValue(localeOption) || getNavigatorLang());
281
+ const countryCode = vue.ref(parseCountryCode(vue.toValue(countryOption), "US"));
282
+ const country = vue.computed(() => getCountry(countryCode.value, locale.value));
283
+ const setCountry2 = (code) => {
284
+ const parsed = parseCountryCode(code);
285
+ if (parsed) {
286
+ countryCode.value = parsed;
287
+ return true;
288
+ }
289
+ return false;
290
+ };
291
+ const detectCountry = async () => {
292
+ const geoCountry = await detectByGeoIp();
293
+ if (setCountry2(geoCountry)) return;
294
+ const localeCountry = detectCountryFromLocale();
295
+ setCountry2(localeCountry);
158
296
  };
159
- const filteredCountries = vue.computed(() => {
160
- const q = search.value.trim().toUpperCase();
161
- if (!q) return countries.value;
162
- const qCodeDigits = q.replace(/\D/g, "");
163
- const isNumericSearch = qCodeDigits.length > 0;
164
- return countries.value.map((c) => {
165
- const nameUpper = c.name.toUpperCase();
166
- const idUpper = c.id.toUpperCase();
167
- const codeDigits = c.code.replace(/\D/g, "");
168
- let score = 0;
169
- if (nameUpper.startsWith(q)) score = 1e3;
170
- else if (nameUpper.includes(q)) score = 500;
171
- if (c.code.startsWith(q)) score += 100;
172
- else if (c.code.includes(q)) score += 50;
173
- if (idUpper === q) score += 200;
174
- else if (idUpper.startsWith(q)) score += 150;
175
- if (isNumericSearch && codeDigits.startsWith(qCodeDigits)) score += 80;
176
- else if (isNumericSearch && codeDigits.includes(qCodeDigits)) score += 40;
177
- return { country: c, score };
178
- }).filter(({ score }) => score > 0).sort((a, b) => {
179
- if (b.score !== a.score) return b.score - a.score;
180
- return a.country.name.localeCompare(b.country.name);
181
- }).map(({ country }) => country);
297
+ vue.watchEffect(() => {
298
+ const newCountry = vue.toValue(countryOption);
299
+ if (newCountry && newCountry !== countryCode.value) {
300
+ setCountry2(newCountry);
301
+ }
182
302
  });
183
- const selectCountry = (id) => {
184
- selectedId.value = id;
185
- closeDropdown();
303
+ vue.watchEffect(() => {
304
+ if (vue.toValue(detect) && !vue.toValue(countryOption)) {
305
+ detectCountry();
306
+ }
307
+ });
308
+ vue.watchEffect(() => {
309
+ onCountryChange?.(country.value);
310
+ });
311
+ return { country, setCountry: setCountry2, locale };
312
+ }
313
+ function useFormatter({
314
+ country,
315
+ value,
316
+ onChange,
317
+ onPhoneChange,
318
+ onValidationChange
319
+ }) {
320
+ const formatter = vue.computed(() => createPhoneFormatter(vue.toValue(country)));
321
+ const maxDigits = vue.computed(() => formatter.value.getMaxDigits());
322
+ const digits = vue.computed(() => extractDigits(vue.toValue(value), maxDigits.value));
323
+ const displayPlaceholder = vue.computed(() => formatter.value.getPlaceholder());
324
+ const displayValue = vue.computed(() => formatter.value.formatDisplay(digits.value));
325
+ const full = vue.computed(() => digits.value ? `${vue.toValue(country).code}${digits.value}` : "");
326
+ const fullFormatted = vue.computed(() => displayValue.value ? `${vue.toValue(country).code} ${displayValue.value}` : "");
327
+ const isComplete = vue.computed(() => formatter.value.isComplete(digits.value));
328
+ const isEmpty = vue.computed(() => digits.value.length === 0);
329
+ const shouldShowWarn = vue.computed(() => !isEmpty.value && !isComplete.value);
330
+ const phoneData = vue.computed(() => ({
331
+ full: full.value,
332
+ fullFormatted: fullFormatted.value,
333
+ digits: digits.value
334
+ }));
335
+ vue.watchEffect(() => {
336
+ if (vue.toValue(value) !== digits.value) {
337
+ onChange(digits.value);
338
+ }
339
+ });
340
+ vue.watchEffect(() => {
341
+ onPhoneChange?.(phoneData.value);
342
+ });
343
+ vue.watchEffect(() => {
344
+ onValidationChange?.(isComplete.value);
345
+ });
346
+ return {
347
+ digits,
348
+ formatter,
349
+ displayPlaceholder,
350
+ displayValue,
351
+ full,
352
+ fullFormatted,
353
+ isComplete,
354
+ isEmpty,
355
+ shouldShowWarn
186
356
  };
187
- const toggleDropdown = async (searchRef) => {
188
- dropdownOpened.value = !dropdownOpened.value;
189
- if (!dropdownOpened.value) return;
190
- await vue.nextTick();
191
- searchRef.value?.focus({ preventScroll: true });
192
- focusedIndex.value = 0;
357
+ }
358
+ function useTimer() {
359
+ let timerRef = null;
360
+ const clear = () => {
361
+ if (timerRef) {
362
+ clearTimeout(timerRef);
363
+ timerRef = null;
364
+ }
193
365
  };
194
- const closeDropdown = () => {
195
- dropdownOpened.value = false;
366
+ const set = (callback, delay) => {
367
+ clear();
368
+ timerRef = setTimeout(callback, delay);
196
369
  };
197
- const focusNextOption = (scrollFn) => {
198
- if (filteredCountries.value.length === 0) return;
199
- focusedIndex.value = Math.min(filteredCountries.value.length - 1, focusedIndex.value + 1);
200
- scrollFn?.();
370
+ vue.onUnmounted(clear);
371
+ return { set, clear };
372
+ }
373
+ function useValidationHint() {
374
+ const showValidationHint = vue.ref(false);
375
+ const validationTimer = useTimer();
376
+ const clearValidationHint = (hideHint = true) => {
377
+ if (hideHint) showValidationHint.value = false;
378
+ validationTimer.clear();
201
379
  };
202
- const focusPrevOption = (scrollFn) => {
203
- if (filteredCountries.value.length === 0) return;
204
- focusedIndex.value = Math.max(0, focusedIndex.value - 1);
205
- scrollFn?.();
380
+ const scheduleValidationHint = (delay) => {
381
+ showValidationHint.value = false;
382
+ validationTimer.set(() => {
383
+ showValidationHint.value = true;
384
+ }, delay);
206
385
  };
207
- const chooseFocusedOption = () => {
208
- const item = filteredCountries.value[focusedIndex.value];
209
- if (item) selectCountry(item.id);
386
+ return { showValidationHint, clearValidationHint, scheduleValidationHint };
387
+ }
388
+ const HINT_DELAY_INPUT = 500;
389
+ const HINT_DELAY_ACTION = 300;
390
+ function useInputHandlers(options) {
391
+ const { formatter, digits, inactive, onChange, scheduleValidationHint } = options;
392
+ const scheduleCaretUpdate = (el, digitIndex) => {
393
+ vue.nextTick(() => {
394
+ if (!el) return;
395
+ const pos = vue.toValue(formatter).getCaretPosition(digitIndex);
396
+ setCaret(el, pos);
397
+ });
210
398
  };
211
- const detectFromLocale = () => {
212
- try {
213
- const lang = getNavigatorLang();
214
- try {
215
- const loc = new Intl.Locale(lang);
216
- if (loc.region && hasCountry(loc.region)) return loc.region.toUpperCase();
217
- } catch {
218
- }
219
- const parts = lang.split(/[-_]/);
220
- if (parts.length > 1 && hasCountry(parts[1])) return parts[1].toUpperCase();
221
- } catch {
222
- }
223
- return null;
399
+ const handleBeforeInput = (e2) => {
400
+ processBeforeInput(e2);
224
401
  };
225
- const selectInitialCountry = (id, emitFn) => {
226
- const previousId = selectedId.value;
227
- selectedId.value = id;
228
- if (previousId !== selectedId.value && emitFn) vue.nextTick(emitFn);
402
+ const handleInput = (e2) => {
403
+ if (vue.toValue(inactive)) return;
404
+ const result = processInput(e2, { formatter: vue.toValue(formatter) });
405
+ if (!result) return;
406
+ onChange?.(result.newDigits);
407
+ scheduleCaretUpdate(e2.target, result.caretDigitIndex);
408
+ scheduleValidationHint?.(HINT_DELAY_INPUT);
229
409
  };
230
- const initCountry = async (predefined, detect, emitFn) => {
231
- hasDropdown.value = !predefined && countries.value.length > 1;
232
- if (predefined && hasCountry(predefined)) {
233
- selectInitialCountry(predefined.toUpperCase(), emitFn);
234
- return;
235
- }
236
- if (!detect) return;
237
- const geo = await detectByGeoIp(hasCountry);
238
- if (geo) {
239
- selectInitialCountry(geo, emitFn);
240
- return;
241
- }
242
- const loc = detectFromLocale();
243
- if (loc) {
244
- selectInitialCountry(loc, emitFn);
245
- return;
246
- }
410
+ const handleKeydown = (e2) => {
411
+ if (vue.toValue(inactive)) return;
412
+ const result = processKeydown(e2, { digits: vue.toValue(digits), formatter: vue.toValue(formatter) });
413
+ if (!result) return;
414
+ onChange?.(result.newDigits);
415
+ scheduleCaretUpdate(e2.target, result.caretDigitIndex);
416
+ scheduleValidationHint?.(HINT_DELAY_ACTION);
417
+ };
418
+ const handlePaste = (e2) => {
419
+ if (vue.toValue(inactive)) return;
420
+ const result = processPaste(e2, { digits: vue.toValue(digits), formatter: vue.toValue(formatter) });
421
+ if (!result) return;
422
+ onChange?.(result.newDigits);
423
+ scheduleCaretUpdate(e2.target, result.caretDigitIndex);
424
+ scheduleValidationHint?.(HINT_DELAY_ACTION);
247
425
  };
248
426
  return {
249
- countries,
250
- selectedId,
251
- selected,
252
- hasCountry,
253
- // Dropdown
254
- hasDropdown,
255
- dropdownOpened,
256
- search,
257
- focusedIndex,
258
- filteredCountries,
259
- selectCountry,
260
- toggleDropdown,
261
- closeDropdown,
262
- focusNextOption,
263
- focusPrevOption,
264
- chooseFocusedOption,
265
- // Country Detection
266
- initCountry
427
+ handleBeforeInput,
428
+ handleInput,
429
+ handleKeydown,
430
+ handlePaste
267
431
  };
268
432
  }
269
- function createPhoneFormatter(country) {
270
- const variants = toArray(country.mask);
271
- const variantsDigits = variants.map((m2) => countPlaceholders(removeCountryCodePrefix(m2)));
272
- const maxDigits = Math.max(...variantsDigits);
273
- const getMask = (digitLength) => {
274
- const mask = pickMaskVariant(variants, digitLength);
275
- return removeCountryCodePrefix(mask);
433
+ function useCountrySelector({
434
+ rootRef,
435
+ dropdownRef,
436
+ searchRef,
437
+ selectorRef,
438
+ locale,
439
+ countryOption,
440
+ inactive,
441
+ onSelectCountry,
442
+ onAfterSelect
443
+ }) {
444
+ const search = vue.ref("");
445
+ const dropdownOpen = vue.ref(false);
446
+ const dropdownStyle = vue.shallowRef({});
447
+ const focusedIndex = vue.ref(0);
448
+ const countries = vue.computed(() => MasksFull(vue.toValue(locale)));
449
+ const filteredCountries = vue.computed(() => filterCountries(countries.value, search.value));
450
+ const hasDropdown = vue.computed(() => !vue.toValue(countryOption) && countries.value.length > 1);
451
+ const setFocusedIndex = (index2) => {
452
+ focusedIndex.value = index2;
276
453
  };
277
- return {
278
- formatDisplay: (digits) => {
279
- const template = getMask(digits.length);
280
- return formatDigitsWithMap(template, digits).display;
281
- },
282
- getMaxDigits: () => maxDigits,
283
- getPlaceholder: () => {
284
- const template = getMask(0);
285
- return template;
286
- },
287
- getCaretPosition: (digitIndex) => {
288
- const template = getMask(digitIndex);
289
- const { display, map } = formatDigitsWithMap(template, "0".repeat(digitIndex));
290
- for (let i2 = 0; i2 < map.length; i2++) {
291
- if (map[i2] === digitIndex) return i2;
292
- }
293
- if (digitIndex >= map.length) return display.length;
294
- for (let i2 = 0; i2 < map.length; i2++) {
295
- if (map[i2] > digitIndex) return i2;
296
- }
297
- return display.length;
298
- },
299
- getDigitRange: (digits, selStart, selEnd) => {
300
- const template = getMask(digits.length);
301
- const { map } = formatDigitsWithMap(template, digits);
302
- let min = Infinity;
303
- let max = -Infinity;
304
- for (let i2 = selStart; i2 < selEnd && i2 < map.length; i2++) {
305
- const digitIdx = map[i2];
306
- if (digitIdx !== void 0 && digitIdx >= 0) {
307
- min = Math.min(min, digitIdx);
308
- max = Math.max(max, digitIdx);
309
- }
310
- }
311
- return min === Infinity ? null : [min, max + 1];
312
- },
313
- isComplete: (digits) => {
314
- return variantsDigits.includes(digits.length);
454
+ const focusSearch = () => {
455
+ vue.nextTick(() => searchRef.value?.focus({ preventScroll: true }));
456
+ };
457
+ const closeDropdown = () => {
458
+ dropdownOpen.value = false;
459
+ };
460
+ const openDropdown = () => {
461
+ dropdownOpen.value = true;
462
+ setFocusedIndex(0);
463
+ focusSearch();
464
+ };
465
+ const toggleDropdown = () => {
466
+ if (vue.toValue(inactive) || !hasDropdown.value) return;
467
+ if (dropdownOpen.value) {
468
+ closeDropdown();
469
+ } else {
470
+ openDropdown();
315
471
  }
316
472
  };
317
- }
318
- function setCaret(el, position) {
319
- if (!el) return;
320
- try {
321
- el.setSelectionRange(position, position);
322
- } catch {
323
- }
324
- }
325
- function extractDigits(value, maxLength) {
326
- const digits = value.replace(/\D/g, "");
327
- return maxLength ? digits.slice(0, maxLength) : digits;
328
- }
329
- function getSelection(el) {
330
- if (!el) return [0, 0];
331
- return [el.selectionStart ?? 0, el.selectionEnd ?? 0];
332
- }
333
- const Delimiters = [" ", "-", "(", ")"];
334
- const NavigationKeys = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "Tab"];
335
- const InvalidPattern = /[^\d\s\-()]/;
336
- function useMask(selected, telRef) {
337
- const digits = vue.ref("");
338
- const displayValue = vue.ref("");
339
- const validationTimer = vue.ref(null);
340
- const showValidationHint = vue.ref(false);
341
- const formatter = vue.computed(() => createPhoneFormatter(selected.value));
342
- const displayPlaceholder = vue.computed(() => formatter.value.getPlaceholder());
343
- const isComplete = vue.computed(() => formatter.value.isComplete(digits.value));
344
- const isEmpty = vue.computed(() => digits.value.length === 0);
345
- const maxDigits = vue.computed(() => formatter.value.getMaxDigits());
346
- const shouldShowWarn = vue.computed(() => showValidationHint.value && !isEmpty.value && !isComplete.value);
347
- const fullFormatted = vue.computed(() => {
348
- if (!displayValue.value) return "";
349
- return `${selected.value.code} ${displayValue.value}`;
350
- });
351
- const full = vue.computed(() => {
352
- if (!digits.value) return "";
353
- return `${selected.value.code}${digits.value}`;
354
- });
355
- const updateDisplay2 = () => {
356
- displayValue.value = formatter.value.formatDisplay(digits.value);
473
+ const selectCountry = (code) => {
474
+ onSelectCountry(code);
475
+ closeDropdown();
476
+ search.value = "";
477
+ setFocusedIndex(0);
478
+ onAfterSelect?.();
357
479
  };
358
- const setCaretToDigitPosition = (digitIndex) => {
359
- const pos = formatter.value.getCaretPosition(digitIndex);
360
- setCaret(telRef.value, pos);
480
+ const handleSearchChange = (e2) => {
481
+ search.value = e2.target.value;
482
+ setFocusedIndex(0);
361
483
  };
362
- const removeDigitsRange = (startIdx, endIdx) => {
363
- if (startIdx >= endIdx) return;
364
- digits.value = digits.value.slice(0, startIdx) + digits.value.slice(endIdx);
484
+ const onDocClick = (ev) => {
485
+ const target = ev.target;
486
+ const dropdownEl = dropdownRef.value;
487
+ const selectorEl = selectorRef.value;
488
+ if (!target) return;
489
+ if (dropdownEl?.contains(target)) return;
490
+ if (selectorEl?.contains(target)) return;
491
+ closeDropdown();
365
492
  };
366
- const handleBeforeInput = (e2) => {
367
- const el = e2.target;
368
- if (!el) return;
369
- const data = e2.data;
370
- if (e2.inputType !== "insertText" || !data) return;
371
- if (InvalidPattern.test(data) || data === " " && el.value.endsWith(" ")) {
372
- e2.preventDefault();
373
- }
493
+ const positionDropdown = (e2) => {
494
+ if (e2?.type === "scroll" && e2.target && dropdownRef.value?.contains(e2.target)) return;
495
+ if (!rootRef.value) return;
496
+ const rect = rootRef.value.getBoundingClientRect();
497
+ dropdownStyle.value = {
498
+ top: `${rect.bottom + globalThis.scrollY + 8}px`,
499
+ left: `${rect.left + globalThis.scrollX}px`,
500
+ width: `${rect.width}px`
501
+ };
374
502
  };
375
- const handleInput = (e2) => {
376
- const el = e2.target;
377
- if (!el) return;
378
- const newDigits = extractDigits(el.value, maxDigits.value);
379
- showValidationHint.value = false;
380
- if (validationTimer.value) {
381
- clearTimeout(validationTimer.value);
382
- }
383
- digits.value = newDigits;
384
- updateDisplay2();
385
- if (newDigits.length > 0) {
386
- validationTimer.value = setTimeout(() => {
387
- showValidationHint.value = true;
388
- }, 500);
389
- }
503
+ const scrollFocusedIntoView = () => {
390
504
  vue.nextTick(() => {
391
- setCaretToDigitPosition(digits.value.length);
505
+ const list = dropdownRef.value?.lastElementChild;
506
+ const option = list?.children[focusedIndex.value];
507
+ if (!list || !option) return;
508
+ const listRect = list.getBoundingClientRect();
509
+ const optionRect = option.getBoundingClientRect();
510
+ let scrollAmount = 0;
511
+ if (optionRect.top < listRect.top) {
512
+ scrollAmount = list.scrollTop - (listRect.top - optionRect.top);
513
+ } else if (optionRect.bottom > listRect.bottom) {
514
+ scrollAmount = list.scrollTop + (optionRect.bottom - listRect.bottom);
515
+ } else {
516
+ return;
517
+ }
518
+ list.scrollTo({ top: scrollAmount, behavior: "smooth" });
392
519
  });
393
520
  };
394
- const handleKeydownInternal = (e2) => {
395
- const el = telRef.value ?? e2.target;
396
- if (!el) return;
397
- if (e2.ctrlKey || e2.metaKey || e2.altKey || NavigationKeys.includes(e2.key)) return;
398
- const [selStart, selEnd] = getSelection(el);
399
- if (e2.key === "Backspace") {
521
+ const handleSearchKeydown = (e2) => {
522
+ if (e2.key === "ArrowDown") {
400
523
  e2.preventDefault();
401
- if (selStart !== selEnd) {
402
- const range = formatter.value.getDigitRange(digits.value, selStart, selEnd);
403
- if (range) {
404
- const [start, end] = range;
405
- removeDigitsRange(start, end);
406
- updateDisplay2();
407
- vue.nextTick(() => setCaretToDigitPosition(start));
408
- }
409
- return;
410
- }
411
- if (selStart > 0) {
412
- const displayStr = displayValue.value;
413
- let prevPos = selStart - 1;
414
- while (prevPos >= 0 && Delimiters.includes(displayStr[prevPos])) {
415
- prevPos--;
416
- }
417
- if (prevPos >= 0) {
418
- const range = formatter.value.getDigitRange(digits.value, prevPos, prevPos + 1);
419
- if (range) {
420
- const [start] = range;
421
- removeDigitsRange(start, start + 1);
422
- updateDisplay2();
423
- vue.nextTick(() => setCaretToDigitPosition(start));
424
- }
425
- }
426
- }
427
- return;
428
- }
429
- if (e2.key === "Delete") {
524
+ setFocusedIndex(Math.min(focusedIndex.value + 1, filteredCountries.value.length - 1));
525
+ scrollFocusedIntoView();
526
+ } else if (e2.key === "ArrowUp") {
430
527
  e2.preventDefault();
431
- if (selStart !== selEnd) {
432
- const range = formatter.value.getDigitRange(digits.value, selStart, selEnd);
433
- if (range) {
434
- const [start, end] = range;
435
- removeDigitsRange(start, end);
436
- updateDisplay2();
437
- vue.nextTick(() => setCaretToDigitPosition(start));
438
- }
439
- return;
440
- }
441
- if (selStart < displayValue.value.length) {
442
- const range = formatter.value.getDigitRange(digits.value, selStart, selStart + 1);
443
- if (range) {
444
- const [start] = range;
445
- removeDigitsRange(start, start + 1);
446
- updateDisplay2();
447
- vue.nextTick(() => setCaretToDigitPosition(start));
448
- }
449
- }
450
- return;
451
- }
452
- if (/^[0-9]$/.test(e2.key)) {
453
- if (digits.value.length >= maxDigits.value) {
454
- e2.preventDefault();
455
- }
456
- return;
457
- }
458
- if (e2.key.length === 1) {
528
+ setFocusedIndex(Math.max(focusedIndex.value - 1, 0));
529
+ scrollFocusedIntoView();
530
+ } else if (e2.key === "Enter" && filteredCountries.value[focusedIndex.value]) {
459
531
  e2.preventDefault();
532
+ selectCountry(filteredCountries.value[focusedIndex.value].id);
533
+ } else if (e2.key === "Escape") {
534
+ closeDropdown();
460
535
  }
461
536
  };
462
- const handleKeydown = (e2) => {
463
- showValidationHint.value = false;
464
- if (validationTimer.value) {
465
- clearTimeout(validationTimer.value);
466
- }
467
- handleKeydownInternal(e2);
468
- if (validationTimer.value) clearTimeout(validationTimer.value);
469
- validationTimer.value = setTimeout(() => {
470
- if (!isComplete.value && !isEmpty.value) showValidationHint.value = true;
471
- }, 300);
472
- };
473
- const handlePaste = (e2) => {
474
- e2.preventDefault();
475
- const text = e2.clipboardData?.getData("text") || "";
476
- const pastedDigits = extractDigits(text, maxDigits.value);
477
- if (pastedDigits.length === 0) return;
478
- const el = telRef.value;
479
- if (!el) return;
480
- const [selStart, selEnd] = getSelection(el);
481
- if (selStart !== selEnd) {
482
- const range2 = formatter.value.getDigitRange(digits.value, selStart, selEnd);
483
- if (range2) {
484
- const [start, end] = range2;
485
- const left2 = digits.value.slice(0, start);
486
- const right2 = digits.value.slice(end);
487
- digits.value = extractDigits(left2 + pastedDigits + right2, maxDigits.value);
488
- updateDisplay2();
489
- vue.nextTick(() => setCaretToDigitPosition(start + pastedDigits.length));
490
- return;
491
- }
492
- }
493
- const range = formatter.value.getDigitRange(digits.value, selStart, selStart);
494
- const insertIndex = range ? range[0] : digits.value.length;
495
- const left = digits.value.slice(0, insertIndex);
496
- const right = digits.value.slice(insertIndex);
497
- digits.value = extractDigits(left + pastedDigits + right, maxDigits.value);
498
- updateDisplay2();
499
- if (validationTimer.value) clearTimeout(validationTimer.value);
500
- validationTimer.value = setTimeout(() => {
501
- if (!isComplete.value && !isEmpty.value) showValidationHint.value = true;
502
- }, 300);
503
- vue.nextTick(() => setCaretToDigitPosition(insertIndex + pastedDigits.length));
537
+ const removeListeners = () => {
538
+ globalThis.removeEventListener("resize", positionDropdown);
539
+ globalThis.removeEventListener("scroll", positionDropdown, true);
540
+ globalThis.removeEventListener("click", onDocClick, true);
504
541
  };
505
- const handleFocus = () => {
506
- if (validationTimer.value) {
507
- clearTimeout(validationTimer.value);
542
+ vue.watch(hasDropdown, (dropdownExists) => {
543
+ if (!dropdownExists && dropdownOpen.value) {
544
+ closeDropdown();
508
545
  }
509
- };
510
- const clear = () => {
511
- digits.value = "";
512
- displayValue.value = "";
513
- showValidationHint.value = false;
514
- if (validationTimer.value) {
515
- clearTimeout(validationTimer.value);
516
- validationTimer.value = null;
517
- }
518
- };
519
- vue.watch(selected, () => {
520
- if (digits.value.length > maxDigits.value) {
521
- digits.value = digits.value.slice(0, maxDigits.value);
546
+ });
547
+ vue.watch(dropdownOpen, (isOpen) => {
548
+ if (!isOpen) {
549
+ removeListeners();
550
+ return;
522
551
  }
523
- updateDisplay2();
552
+ positionDropdown();
553
+ globalThis.addEventListener("resize", positionDropdown);
554
+ globalThis.addEventListener("scroll", positionDropdown, true);
555
+ globalThis.addEventListener("click", onDocClick, true);
524
556
  });
525
- updateDisplay2();
557
+ vue.onBeforeUnmount(removeListeners);
526
558
  return {
527
559
  // State
528
- digits,
529
- displayValue,
530
- // Computed
531
- displayPlaceholder,
532
- isComplete,
533
- isEmpty,
534
- shouldShowWarn,
535
- fullFormatted,
536
- full,
537
- // Handlers
538
- handleBeforeInput,
539
- handleInput,
540
- handleKeydown,
541
- handlePaste,
542
- handleFocus,
543
- // Methods
544
- updateDisplayFromDigits: updateDisplay2,
545
- clear
560
+ dropdownOpen,
561
+ search,
562
+ focusedIndex,
563
+ dropdownStyle,
564
+ // Derived
565
+ filteredCountries,
566
+ hasDropdown,
567
+ // Actions
568
+ openDropdown,
569
+ closeDropdown,
570
+ toggleDropdown,
571
+ selectCountry,
572
+ setFocusedIndex,
573
+ handleSearchChange,
574
+ handleSearchKeydown
546
575
  };
547
576
  }
548
- function useClipboard() {
577
+ function useClipboard(delay = 1800) {
549
578
  const copied = vue.ref(false);
550
579
  const isCopying = vue.ref(false);
551
- let copyTimer = null;
552
- const clearTimer = () => {
553
- if (copyTimer) {
554
- clearTimeout(copyTimer);
555
- copyTimer = null;
556
- }
557
- };
580
+ const copyTimer = useTimer();
558
581
  const copy = async (text) => {
559
582
  if (isCopying.value) return false;
560
583
  const trimmedText = text.trim();
@@ -563,11 +586,9 @@ function useClipboard() {
563
586
  try {
564
587
  await navigator.clipboard.writeText(trimmedText);
565
588
  copied.value = true;
566
- clearTimer();
567
- copyTimer = setTimeout(() => {
589
+ copyTimer.set(() => {
568
590
  copied.value = false;
569
- copyTimer = null;
570
- }, 1800);
591
+ }, delay);
571
592
  return true;
572
593
  } catch (err) {
573
594
  console.warn("Copy failed", err);
@@ -576,24 +597,74 @@ function useClipboard() {
576
597
  isCopying.value = false;
577
598
  }
578
599
  };
579
- const onUnmount = () => {
580
- clearTimer();
600
+ return { copied, isCopying, copy };
601
+ }
602
+ const DELAY = 1800;
603
+ function useCopyAction({ liveRef, fullFormatted, onCopy }) {
604
+ const liveTimer = useTimer();
605
+ const { copied, copy } = useClipboard(DELAY);
606
+ const copyAriaLabel = vue.computed(() => copied.value ? "Copied" : `Copy ${fullFormatted.value}`);
607
+ const copyButtonTitle = vue.computed(() => copied.value ? "Copied" : "Copy phone number");
608
+ const announceToScreenReader = (message) => {
609
+ if (!liveRef?.value) return;
610
+ liveRef.value.textContent = message;
611
+ liveTimer.set(() => {
612
+ if (liveRef.value) liveRef.value.textContent = "";
613
+ }, DELAY);
614
+ };
615
+ const onCopyClick = async () => {
616
+ const valueToCopy = fullFormatted.value.trim();
617
+ const success = await copy(valueToCopy);
618
+ if (success) {
619
+ onCopy?.(valueToCopy);
620
+ announceToScreenReader("Phone number copied to clipboard");
621
+ }
622
+ };
623
+ return {
624
+ copied,
625
+ copyAriaLabel,
626
+ copyButtonTitle,
627
+ onCopyClick
581
628
  };
582
- return { copied, isCopying, copy, onUnmount };
583
629
  }
584
- const _hoisted_1 = { class: "pi-selector" };
585
- const _hoisted_2 = ["disabled", "tabindex", "aria-label", "aria-expanded", "aria-haspopup"];
586
- const _hoisted_3 = ["aria-label"];
587
- const _hoisted_4 = { class: "pi-code" };
588
- const _hoisted_5 = { class: "pi-input-wrap" };
589
- const _hoisted_6 = ["placeholder", "value", "disabled", "readonly", "aria-invalid"];
590
- const _hoisted_7 = {
630
+ function useTheme({ theme }) {
631
+ const systemDark = vue.ref(false);
632
+ const themeClass = vue.computed(() => {
633
+ const resolvedTheme = vue.toValue(theme);
634
+ if (resolvedTheme === "auto") {
635
+ return systemDark.value ? "theme-dark" : "theme-light";
636
+ }
637
+ return `theme-${resolvedTheme}`;
638
+ });
639
+ let mq = null;
640
+ const handler = (e2) => {
641
+ systemDark.value = e2.matches;
642
+ };
643
+ vue.onBeforeMount(() => {
644
+ mq = globalThis.matchMedia?.("(prefers-color-scheme: dark)") ?? null;
645
+ if (!mq) return;
646
+ systemDark.value = mq.matches;
647
+ mq.addEventListener("change", handler);
648
+ });
649
+ vue.onBeforeUnmount(() => {
650
+ mq?.removeEventListener("change", handler);
651
+ });
652
+ return {
653
+ themeClass
654
+ };
655
+ }
656
+ const _hoisted_1 = ["disabled", "tabindex", "aria-label", "aria-expanded", "aria-haspopup"];
657
+ const _hoisted_2 = ["aria-label"];
658
+ const _hoisted_3 = { class: "pi-code" };
659
+ const _hoisted_4 = { class: "pi-input-wrap" };
660
+ const _hoisted_5 = ["placeholder", "value", "disabled", "readonly", "aria-invalid"];
661
+ const _hoisted_6 = {
591
662
  class: "pi-actions",
592
663
  role: "toolbar",
593
664
  "aria-label": "Phone input actions"
594
665
  };
595
- const _hoisted_8 = ["aria-label", "title"];
596
- const _hoisted_9 = {
666
+ const _hoisted_7 = ["aria-label", "title"];
667
+ const _hoisted_8 = {
597
668
  key: 1,
598
669
  width: "16",
599
670
  height: "16",
@@ -601,7 +672,7 @@ const _hoisted_9 = {
601
672
  fill: "none",
602
673
  "aria-hidden": "true"
603
674
  };
604
- const _hoisted_10 = {
675
+ const _hoisted_9 = {
605
676
  key: 2,
606
677
  width: "16",
607
678
  height: "16",
@@ -609,8 +680,8 @@ const _hoisted_10 = {
609
680
  fill: "none",
610
681
  "aria-hidden": "true"
611
682
  };
612
- const _hoisted_11 = ["aria-label", "title"];
613
- const _hoisted_12 = {
683
+ const _hoisted_10 = ["aria-label", "title"];
684
+ const _hoisted_11 = {
614
685
  key: 0,
615
686
  width: "11",
616
687
  height: "11",
@@ -618,14 +689,13 @@ const _hoisted_12 = {
618
689
  fill: "none",
619
690
  "aria-hidden": "true"
620
691
  };
621
- const _hoisted_13 = { class: "pi-search-wrap" };
622
- const _hoisted_14 = ["placeholder"];
623
- const _hoisted_15 = ["aria-activedescendant"];
624
- const _hoisted_16 = ["id", "aria-selected", "title", "onClick", "onMouseenter"];
625
- const _hoisted_17 = ["aria-label"];
626
- const _hoisted_18 = { class: "pi-opt-name" };
627
- const _hoisted_19 = { class: "pi-opt-code" };
628
- const _hoisted_20 = {
692
+ const _hoisted_12 = { class: "pi-search-wrap" };
693
+ const _hoisted_13 = ["value", "aria-activedescendant", "placeholder"];
694
+ const _hoisted_14 = ["id", "aria-selected", "title", "onClick", "onMouseenter"];
695
+ const _hoisted_15 = ["aria-label"];
696
+ const _hoisted_16 = { class: "pi-opt-name" };
697
+ const _hoisted_17 = { class: "pi-opt-code" };
698
+ const _hoisted_18 = {
629
699
  key: 0,
630
700
  class: "pi-empty"
631
701
  };
@@ -648,238 +718,139 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
648
718
  dropdownClass: {},
649
719
  disableDefaultStyles: { type: Boolean, default: false }
650
720
  }, {
651
- "modelValue": {},
721
+ "modelValue": { default: "" },
652
722
  "modelModifiers": {}
653
723
  }),
654
724
  emits: /* @__PURE__ */ vue.mergeModels(["change", "country-change", "validation-change", "focus", "blur", "copy", "clear"], ["update:modelValue"]),
655
725
  setup(__props, { expose: __expose, emit: __emit }) {
656
726
  const props = __props;
657
727
  const slots = vue.useSlots();
658
- const model = vue.useModel(__props, "modelValue");
659
728
  const emit = __emit;
729
+ const model = vue.useModel(__props, "modelValue");
730
+ const onChange = (v) => {
731
+ model.value = v;
732
+ };
733
+ const { country, setCountry: setCountry2, locale } = useCountry({
734
+ country: () => props.country,
735
+ locale: () => props.locale,
736
+ detect: () => props.detect,
737
+ onCountryChange: (c) => emit("country-change", c)
738
+ });
739
+ const {
740
+ digits,
741
+ formatter,
742
+ displayPlaceholder,
743
+ displayValue,
744
+ full,
745
+ fullFormatted,
746
+ isComplete,
747
+ isEmpty,
748
+ shouldShowWarn
749
+ } = useFormatter({
750
+ country,
751
+ value: model,
752
+ onChange,
753
+ onPhoneChange: (data) => emit("change", data),
754
+ onValidationChange: (complete) => emit("validation-change", complete)
755
+ });
756
+ const { showValidationHint, clearValidationHint, scheduleValidationHint } = useValidationHint();
660
757
  const rootRef = vue.useTemplateRef("rootRef");
661
758
  const telRef = vue.useTemplateRef("telRef");
662
- const searchRef = vue.useTemplateRef("searchRef");
663
759
  const liveRef = vue.useTemplateRef("liveRef");
664
760
  const dropdownRef = vue.useTemplateRef("dropdownRef");
665
- const usedLocale = vue.computed(() => {
666
- return props.locale || getNavigatorLang();
761
+ const searchRef = vue.useTemplateRef("searchRef");
762
+ const selectorRef = vue.useTemplateRef("selectorRef");
763
+ const dropdownId = vue.getCurrentInstance()?.uid ?? 0;
764
+ const listboxId = `pi-options-${dropdownId}`;
765
+ const getOptionId = (idx) => `pi-option-${dropdownId}-${idx}`;
766
+ const inactive = vue.computed(() => props.disabled || props.readonly);
767
+ const incomplete = vue.computed(() => showValidationHint.value && shouldShowWarn.value);
768
+ const showCopyButton = vue.computed(() => props.showCopy && !isEmpty.value && !props.disabled);
769
+ const showClearButton = vue.computed(() => props.showClear && !isEmpty.value && !inactive.value);
770
+ const { copied, copyAriaLabel, copyButtonTitle, onCopyClick } = useCopyAction({
771
+ liveRef,
772
+ fullFormatted,
773
+ onCopy: (v) => emit("copy", v)
667
774
  });
668
- const dropdownStyle = vue.shallowRef({});
669
- const countrySelector = useCountrySelector(usedLocale);
775
+ const focusInput = () => vue.nextTick(() => telRef.value?.focus());
670
776
  const {
777
+ dropdownOpen,
671
778
  search,
672
- filteredCountries,
673
779
  focusedIndex,
674
- selected,
675
- dropdownOpened,
780
+ dropdownStyle,
781
+ filteredCountries,
676
782
  hasDropdown,
677
- focusNextOption,
678
- focusPrevOption,
679
- chooseFocusedOption,
680
- closeDropdown
681
- } = countrySelector;
682
- const mask = useMask(selected, telRef);
683
- const { digits, displayValue, displayPlaceholder, isComplete, isEmpty, shouldShowWarn, full, fullFormatted } = mask;
684
- const { copied, copy, onUnmount: onClipboardUnmount } = useClipboard();
685
- const inactive = vue.computed(() => props.disabled || props.readonly);
686
- const showCopyButton = vue.computed(() => props.showCopy && !isEmpty.value && !props.disabled);
687
- const showClearButton = vue.computed(() => props.showClear && !isEmpty.value && !inactive.value);
688
- const copyAriaLabel = vue.computed(() => copied.value ? "Copied" : `Copy ${selected.value.code} ${displayValue.value}`);
689
- const copyButtonTitle = vue.computed(() => copied.value ? "Copied" : "Copy phone number");
690
- const copyMessage = vue.computed(() => copied.value ? "Phone number copied to clipboard" : "");
691
- const sizeClass = vue.computed(() => `size-${props.size}`);
692
- const themeClass = vue.computed(() => {
693
- if (props.theme !== "auto") return `theme-${props.theme}`;
694
- if (typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches) {
695
- return "theme-dark";
696
- }
697
- return "theme-light";
783
+ closeDropdown,
784
+ toggleDropdown,
785
+ selectCountry,
786
+ setFocusedIndex,
787
+ handleSearchChange,
788
+ handleSearchKeydown
789
+ } = useCountrySelector({
790
+ rootRef,
791
+ dropdownRef,
792
+ searchRef,
793
+ selectorRef,
794
+ locale,
795
+ countryOption: () => props.country,
796
+ inactive,
797
+ onSelectCountry: setCountry2,
798
+ onAfterSelect: focusInput
698
799
  });
699
- const rootClasses = vue.computed(() => [
700
- "phone-input",
701
- sizeClass.value,
702
- themeClass.value,
703
- {
704
- "is-disabled": props.disabled,
705
- "is-readonly": props.readonly,
706
- "is-unstyled": props.disableDefaultStyles,
707
- "is-incomplete": props.withValidity && shouldShowWarn.value,
708
- "is-complete": props.withValidity && isComplete.value
709
- }
710
- ]);
711
- const rootStyles = vue.computed(() => ({
712
- "--pi-actions-count": +showCopyButton.value + +showClearButton.value + (slots["actions-before"] ? 1 : 0)
713
- }));
714
- const emitModelUpdate = () => {
715
- if (model.value === digits.value) return;
716
- model.value = digits.value;
717
- emit("change", {
718
- full: full.value,
719
- fullFormatted: fullFormatted.value,
720
- digits: digits.value
721
- });
722
- };
723
- const onInput = async (e2) => {
724
- if (inactive.value) return;
725
- mask.handleInput(e2);
726
- await vue.nextTick();
727
- emitModelUpdate();
728
- };
729
- const onKeydown = async (e2) => {
730
- if (inactive.value) return;
731
- mask.handleKeydown(e2);
732
- await vue.nextTick();
733
- emitModelUpdate();
734
- };
735
- const onPaste = async (e2) => {
736
- if (inactive.value) return;
737
- mask.handlePaste(e2);
738
- await vue.nextTick();
739
- emitModelUpdate();
740
- };
741
- const onFocus = (e2) => {
742
- mask.handleFocus();
743
- dropdownOpened.value = false;
800
+ const activeOptionId = vue.computed(
801
+ () => dropdownOpen.value && filteredCountries.value[focusedIndex.value] ? getOptionId(focusedIndex.value) : void 0
802
+ );
803
+ const { handleBeforeInput, handleInput, handleKeydown, handlePaste } = useInputHandlers({
804
+ formatter,
805
+ digits,
806
+ inactive,
807
+ onChange,
808
+ scheduleValidationHint
809
+ });
810
+ const handleFocus = (e2) => {
811
+ clearValidationHint(false);
812
+ closeDropdown();
744
813
  emit("focus", e2);
745
814
  };
746
- const onBlur = (e2) => emit("blur", e2);
747
- const onSelectCountry = async (countryId) => {
748
- countrySelector.selectCountry(countryId);
749
- emit("country-change", selected.value);
750
- await vue.nextTick();
751
- telRef.value?.focus();
752
- };
753
- const onCopyClick = async () => {
754
- const valueToCopy = fullFormatted.value;
755
- const success = await copy(valueToCopy);
756
- if (success) {
757
- emit("copy", valueToCopy);
758
- }
759
- };
760
- const onClearClick = async () => {
761
- mask.clear();
762
- model.value = "";
763
- emit("change", {
764
- full: "",
765
- fullFormatted: "",
766
- digits: ""
767
- });
815
+ const handleBlur = (e2) => emit("blur", e2);
816
+ const clear = () => {
817
+ onChange("");
818
+ clearValidationHint();
768
819
  emit("clear");
769
- await vue.nextTick();
770
- telRef.value?.focus();
771
- };
772
- const positionDropdown = (e2) => {
773
- if (e2?.type === "scroll" && e2.target && dropdownRef.value?.contains(e2.target)) return;
774
- const root = rootRef.value;
775
- if (!root) return;
776
- const rect = root.getBoundingClientRect();
777
- dropdownStyle.value = {
778
- top: `${rect.bottom + window.scrollY + 8}px`,
779
- left: `${rect.left + window.scrollX}px`,
780
- width: `${rect.width}px`
781
- };
782
820
  };
783
- const removeDropdownListeners = () => {
784
- window.removeEventListener("scroll", positionDropdown, true);
785
- window.removeEventListener("click", onDocClick, true);
786
- window.removeEventListener("resize", positionDropdown);
821
+ const onClearClick = () => {
822
+ clear();
823
+ focusInput();
787
824
  };
788
- const toggleDropdown = async () => {
789
- if (inactive.value || !hasDropdown.value) return;
790
- await countrySelector.toggleDropdown(searchRef);
791
- if (dropdownOpened.value) {
792
- positionDropdown();
793
- window.addEventListener("scroll", positionDropdown, true);
794
- window.addEventListener("click", onDocClick, true);
795
- window.addEventListener("resize", positionDropdown);
796
- } else {
797
- removeDropdownListeners();
798
- }
799
- };
800
- const scrollFocusedIntoView = async () => {
801
- await vue.nextTick();
802
- const list = dropdownRef.value?.lastElementChild;
803
- if (!list) return;
804
- const option = list.children[focusedIndex.value];
805
- if (!option) return;
806
- const listRect = list.getBoundingClientRect();
807
- const optionRect = option.getBoundingClientRect();
808
- let scrollAmount = 0;
809
- if (optionRect.top < listRect.top) {
810
- scrollAmount = list.scrollTop - (listRect.top - optionRect.top);
811
- } else if (optionRect.bottom > listRect.bottom) {
812
- scrollAmount = list.scrollTop + (optionRect.bottom - listRect.bottom);
813
- } else {
814
- return;
815
- }
816
- list.scrollTo({ top: scrollAmount, behavior: "smooth" });
817
- };
818
- const onDocClick = (ev) => {
819
- const dropdown = dropdownRef.value;
820
- const selector = rootRef.value?.firstChild;
821
- if (!(dropdown || selector)) return;
822
- const target = ev.target;
823
- if (!target || dropdown?.contains(target) || selector?.contains(target)) return;
824
- dropdownOpened.value = false;
825
- };
826
- vue.watch(
827
- model,
828
- (newValue) => {
829
- if (!newValue) {
830
- if (!isEmpty.value) mask.clear();
831
- return;
832
- }
833
- const currentDigits = digits.value;
834
- if (newValue !== currentDigits) {
835
- const incomingDigits = newValue.replace(/\D/g, "");
836
- if (incomingDigits !== currentDigits) {
837
- digits.value = incomingDigits;
838
- mask.updateDisplayFromDigits();
839
- }
840
- }
841
- },
842
- { immediate: true }
843
- );
844
- vue.watch(
845
- [() => props.country, () => props.detect],
846
- async ([country, detect]) => {
847
- await vue.nextTick();
848
- await countrySelector.initCountry(country, detect, () => emit("country-change", selected.value));
849
- },
850
- { immediate: true }
851
- );
852
- vue.watch(
853
- copyMessage,
854
- (val) => {
855
- if (liveRef.value && val) {
856
- liveRef.value.textContent = val;
857
- }
858
- },
859
- { flush: "post" }
860
- );
861
- vue.watch(
862
- isComplete,
863
- (valid) => {
864
- emit("validation-change", valid);
865
- },
866
- { flush: "post" }
867
- );
868
- vue.onBeforeUnmount(() => {
869
- removeDropdownListeners();
870
- onClipboardUnmount();
871
- });
872
825
  __expose({
873
- focus: () => telRef.value?.focus(),
826
+ focus: focusInput,
874
827
  blur: () => telRef.value?.blur(),
875
- clear: mask.clear,
876
- selectCountry: countrySelector.selectCountry,
828
+ clear,
829
+ selectCountry,
877
830
  getFullNumber: () => full.value,
878
831
  getFullFormattedNumber: () => fullFormatted.value,
879
832
  getDigits: () => digits.value,
880
833
  isValid: () => isComplete.value,
881
834
  isComplete: () => isComplete.value
882
835
  });
836
+ const { themeClass } = useTheme({
837
+ theme: () => props.theme
838
+ });
839
+ const rootClasses = vue.computed(() => [
840
+ "phone-input",
841
+ `size-${props.size}`,
842
+ themeClass.value,
843
+ {
844
+ "is-disabled": props.disabled,
845
+ "is-readonly": props.readonly,
846
+ "is-unstyled": props.disableDefaultStyles,
847
+ "is-incomplete": props.withValidity && incomplete.value,
848
+ "is-complete": props.withValidity && isComplete.value
849
+ }
850
+ ]);
851
+ const rootStyles = vue.computed(() => ({
852
+ "--pi-actions-count": +showCopyButton.value + +showClearButton.value + (slots["actions-before"] ? 1 : 0)
853
+ }));
883
854
  return (_ctx, _cache) => {
884
855
  return vue.openBlock(), vue.createElementBlock("div", {
885
856
  ref_key: "rootRef",
@@ -889,36 +860,41 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
889
860
  class: vue.normalizeClass(rootClasses.value),
890
861
  style: vue.normalizeStyle(rootStyles.value)
891
862
  }, [
892
- vue.createElementVNode("div", _hoisted_1, [
863
+ vue.createElementVNode("div", {
864
+ ref_key: "selectorRef",
865
+ ref: selectorRef,
866
+ class: "pi-selector"
867
+ }, [
893
868
  vue.createElementVNode("button", {
894
869
  type: "button",
895
870
  class: vue.normalizeClass(["pi-selector-btn", { "no-dropdown": !vue.unref(hasDropdown) || __props.readonly }]),
896
871
  disabled: __props.disabled,
897
872
  tabindex: inactive.value || !vue.unref(hasDropdown) ? -1 : void 0,
898
- "aria-label": `Selected country: ${vue.unref(selected).name}`,
899
- "aria-expanded": vue.unref(dropdownOpened),
873
+ "aria-label": `Selected country: ${vue.unref(country).name}`,
874
+ "aria-expanded": vue.unref(dropdownOpen),
900
875
  "aria-haspopup": vue.unref(hasDropdown) ? "listbox" : void 0,
901
- onClick: toggleDropdown
876
+ onClick: _cache[0] || (_cache[0] = //@ts-ignore
877
+ (...args) => vue.unref(toggleDropdown) && vue.unref(toggleDropdown)(...args))
902
878
  }, [
903
879
  vue.createElementVNode("span", {
904
880
  class: "pi-flag",
905
881
  role: "img",
906
- "aria-label": `${vue.unref(selected).name} flag`
882
+ "aria-label": `${vue.unref(country).name} flag`
907
883
  }, [
908
- vue.renderSlot(_ctx.$slots, "flag", { country: vue.unref(selected) }, () => [
909
- vue.createTextVNode(vue.toDisplayString(vue.unref(selected).flag), 1)
884
+ vue.renderSlot(_ctx.$slots, "flag", { country: vue.unref(country) }, () => [
885
+ vue.createTextVNode(vue.toDisplayString(vue.unref(country).flag), 1)
910
886
  ], true)
911
- ], 8, _hoisted_3),
912
- vue.createElementVNode("span", _hoisted_4, vue.toDisplayString(vue.unref(selected).code), 1),
887
+ ], 8, _hoisted_2),
888
+ vue.createElementVNode("span", _hoisted_3, vue.toDisplayString(vue.unref(country).code), 1),
913
889
  !inactive.value && vue.unref(hasDropdown) ? (vue.openBlock(), vue.createElementBlock("svg", {
914
890
  key: 0,
915
- class: vue.normalizeClass(["pi-chevron", { "is-open": vue.unref(dropdownOpened) }]),
891
+ class: vue.normalizeClass(["pi-chevron", { "is-open": vue.unref(dropdownOpen) }]),
916
892
  width: "12",
917
893
  height: "12",
918
894
  viewBox: "0 0 12 12",
919
895
  fill: "none",
920
896
  "aria-hidden": "true"
921
- }, [..._cache[6] || (_cache[6] = [
897
+ }, [..._cache[8] || (_cache[8] = [
922
898
  vue.createElementVNode("path", {
923
899
  d: "M2.5 4.5L6 8L9.5 4.5",
924
900
  stroke: "currentColor",
@@ -927,9 +903,9 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
927
903
  "stroke-linejoin": "round"
928
904
  }, null, -1)
929
905
  ])], 2)) : vue.createCommentVNode("", true)
930
- ], 10, _hoisted_2)
931
- ]),
932
- vue.createElementVNode("div", _hoisted_5, [
906
+ ], 10, _hoisted_1)
907
+ ], 512),
908
+ vue.createElementVNode("div", _hoisted_4, [
933
909
  vue.createElementVNode("input", {
934
910
  ref_key: "telRef",
935
911
  ref: telRef,
@@ -944,16 +920,19 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
944
920
  value: vue.unref(displayValue),
945
921
  disabled: __props.disabled,
946
922
  readonly: __props.readonly,
947
- "aria-invalid": vue.unref(shouldShowWarn),
948
- onBeforeinput: _cache[0] || (_cache[0] = //@ts-ignore
949
- (...args) => vue.unref(mask).handleBeforeInput && vue.unref(mask).handleBeforeInput(...args)),
950
- onInput,
951
- onKeydown,
952
- onPaste,
953
- onFocus,
954
- onBlur
955
- }, null, 40, _hoisted_6),
956
- vue.createElementVNode("div", _hoisted_7, [
923
+ "aria-invalid": incomplete.value,
924
+ onBeforeinput: _cache[1] || (_cache[1] = //@ts-ignore
925
+ (...args) => vue.unref(handleBeforeInput) && vue.unref(handleBeforeInput)(...args)),
926
+ onInput: _cache[2] || (_cache[2] = //@ts-ignore
927
+ (...args) => vue.unref(handleInput) && vue.unref(handleInput)(...args)),
928
+ onKeydown: _cache[3] || (_cache[3] = //@ts-ignore
929
+ (...args) => vue.unref(handleKeydown) && vue.unref(handleKeydown)(...args)),
930
+ onPaste: _cache[4] || (_cache[4] = //@ts-ignore
931
+ (...args) => vue.unref(handlePaste) && vue.unref(handlePaste)(...args)),
932
+ onFocus: handleFocus,
933
+ onBlur: handleBlur
934
+ }, null, 40, _hoisted_5),
935
+ vue.createElementVNode("div", _hoisted_6, [
957
936
  vue.createVNode(vue.Transition, { name: "fade-scale" }, {
958
937
  default: vue.withCtx(() => [
959
938
  vue.renderSlot(_ctx.$slots, "actions-before", {}, void 0, true)
@@ -965,26 +944,27 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
965
944
  showCopyButton.value ? (vue.openBlock(), vue.createElementBlock("button", {
966
945
  key: 0,
967
946
  type: "button",
968
- class: vue.normalizeClass(["pi-btn", { "is-copied": vue.unref(copied) }]),
969
- "aria-label": copyAriaLabel.value,
970
- title: copyButtonTitle.value,
971
- onClick: onCopyClick
947
+ class: vue.normalizeClass(["pi-btn", "pi-btn-copy", { "is-copied": vue.unref(copied) }]),
948
+ "aria-label": vue.unref(copyAriaLabel),
949
+ title: vue.unref(copyButtonTitle),
950
+ onClick: _cache[5] || (_cache[5] = //@ts-ignore
951
+ (...args) => vue.unref(onCopyClick) && vue.unref(onCopyClick)(...args))
972
952
  }, [
973
953
  slots["copy-svg"] ? vue.renderSlot(_ctx.$slots, "copy-svg", {
974
954
  key: 0,
975
955
  copied: vue.unref(copied)
976
- }, void 0, true) : !vue.unref(copied) ? (vue.openBlock(), vue.createElementBlock("svg", _hoisted_9, [..._cache[7] || (_cache[7] = [
956
+ }, void 0, true) : !vue.unref(copied) ? (vue.openBlock(), vue.createElementBlock("svg", _hoisted_8, [..._cache[9] || (_cache[9] = [
977
957
  vue.createElementVNode("path", {
978
958
  d: "M13.5 5.5V13.5H5.5V5.5H13.5ZM13.5 4H5.5C4.67 4 4 4.67 4 5.5V13.5C4 14.33 4.67 15 5.5 15H13.5C14.33 15 15 14.33 15 13.5V5.5C15 4.67 14.33 4 13.5 4ZM10.5 1H2.5V11H4V2.5H10.5V1Z",
979
959
  fill: "currentColor"
980
960
  }, null, -1)
981
- ])])) : (vue.openBlock(), vue.createElementBlock("svg", _hoisted_10, [..._cache[8] || (_cache[8] = [
961
+ ])])) : (vue.openBlock(), vue.createElementBlock("svg", _hoisted_9, [..._cache[10] || (_cache[10] = [
982
962
  vue.createElementVNode("path", {
983
963
  d: "M6.5 11.5L3 8L4.06 6.94L6.5 9.38L11.94 3.94L13 5L6.5 11.5Z",
984
964
  fill: "currentColor"
985
965
  }, null, -1)
986
966
  ])]))
987
- ], 10, _hoisted_8)) : vue.createCommentVNode("", true)
967
+ ], 10, _hoisted_7)) : vue.createCommentVNode("", true)
988
968
  ]),
989
969
  _: 3
990
970
  }),
@@ -993,18 +973,18 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
993
973
  showClearButton.value ? (vue.openBlock(), vue.createElementBlock("button", {
994
974
  key: 0,
995
975
  type: "button",
996
- class: "pi-btn",
976
+ class: "pi-btn pi-btn-clear",
997
977
  "aria-label": __props.clearButtonLabel,
998
978
  title: __props.clearButtonLabel,
999
979
  onClick: onClearClick
1000
980
  }, [
1001
- !slots["clear-svg"] ? (vue.openBlock(), vue.createElementBlock("svg", _hoisted_12, [..._cache[9] || (_cache[9] = [
981
+ !slots["clear-svg"] ? (vue.openBlock(), vue.createElementBlock("svg", _hoisted_11, [..._cache[11] || (_cache[11] = [
1002
982
  vue.createElementVNode("path", {
1003
983
  d: "M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z",
1004
984
  fill: "currentColor"
1005
985
  }, null, -1)
1006
986
  ])])) : vue.renderSlot(_ctx.$slots, "clear-svg", { key: 1 }, void 0, true)
1007
- ], 8, _hoisted_11)) : vue.createCommentVNode("", true)
987
+ ], 8, _hoisted_10)) : vue.createCommentVNode("", true)
1008
988
  ]),
1009
989
  _: 3
1010
990
  })
@@ -1013,65 +993,55 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
1013
993
  (vue.openBlock(), vue.createBlock(vue.Teleport, { to: "body" }, [
1014
994
  vue.createVNode(vue.Transition, { name: "dropdown" }, {
1015
995
  default: vue.withCtx(() => [
1016
- vue.unref(dropdownOpened) ? (vue.openBlock(), vue.createElementBlock("div", {
996
+ vue.unref(dropdownOpen) ? (vue.openBlock(), vue.createElementBlock("div", {
1017
997
  key: 0,
1018
998
  ref_key: "dropdownRef",
1019
999
  ref: dropdownRef,
1020
- class: vue.normalizeClass(["phone-dropdown", [__props.dropdownClass, themeClass.value]]),
1000
+ class: vue.normalizeClass(["phone-dropdown", [__props.dropdownClass, vue.unref(themeClass)]]),
1021
1001
  role: "dialog",
1022
1002
  "aria-modal": "false",
1023
1003
  "aria-label": "Select country",
1024
- style: vue.normalizeStyle(dropdownStyle.value)
1004
+ style: vue.normalizeStyle(vue.unref(dropdownStyle))
1025
1005
  }, [
1026
- vue.createElementVNode("div", _hoisted_13, [
1027
- vue.withDirectives(vue.createElementVNode("input", {
1006
+ vue.createElementVNode("div", _hoisted_12, [
1007
+ vue.createElementVNode("input", {
1028
1008
  ref_key: "searchRef",
1029
1009
  ref: searchRef,
1030
- "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => vue.isRef(search) ? search.value = $event : null),
1010
+ value: vue.unref(search),
1031
1011
  type: "search",
1032
1012
  class: "pi-search",
1033
1013
  "aria-label": "Search countries",
1014
+ "aria-controls": listboxId,
1015
+ "aria-activedescendant": activeOptionId.value,
1034
1016
  placeholder: __props.searchPlaceholder,
1035
- onKeydown: [
1036
- _cache[2] || (_cache[2] = vue.withKeys(vue.withModifiers(($event) => vue.unref(focusNextOption)(scrollFocusedIntoView), ["prevent"]), ["down"])),
1037
- _cache[3] || (_cache[3] = vue.withKeys(vue.withModifiers(($event) => vue.unref(focusPrevOption)(scrollFocusedIntoView), ["prevent"]), ["up"])),
1038
- _cache[4] || (_cache[4] = vue.withKeys(vue.withModifiers(
1039
- //@ts-ignore
1040
- (...args) => vue.unref(chooseFocusedOption) && vue.unref(chooseFocusedOption)(...args),
1041
- ["prevent"]
1042
- ), ["enter"])),
1043
- _cache[5] || (_cache[5] = vue.withKeys(
1044
- //@ts-ignore
1045
- (...args) => vue.unref(closeDropdown) && vue.unref(closeDropdown)(...args),
1046
- ["escape"]
1047
- ))
1048
- ]
1049
- }, null, 40, _hoisted_14), [
1050
- [vue.vModelText, vue.unref(search)]
1051
- ])
1017
+ onKeydown: _cache[6] || (_cache[6] = //@ts-ignore
1018
+ (...args) => vue.unref(handleSearchKeydown) && vue.unref(handleSearchKeydown)(...args)),
1019
+ onInput: _cache[7] || (_cache[7] = //@ts-ignore
1020
+ (...args) => vue.unref(handleSearchChange) && vue.unref(handleSearchChange)(...args))
1021
+ }, null, 40, _hoisted_13)
1052
1022
  ]),
1053
1023
  vue.createElementVNode("ul", {
1024
+ id: listboxId,
1054
1025
  class: "pi-options",
1055
1026
  role: "listbox",
1056
- "aria-activedescendant": `option-${vue.unref(focusedIndex)}`,
1057
1027
  tabindex: "-1"
1058
1028
  }, [
1059
1029
  (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(filteredCountries), (c, idx) => {
1060
1030
  return vue.openBlock(), vue.createElementBlock("li", {
1061
- id: `option-${idx}`,
1031
+ id: getOptionId(idx),
1062
1032
  key: c.id,
1063
1033
  role: "option",
1064
1034
  class: vue.normalizeClass([
1065
1035
  "pi-option",
1066
1036
  {
1067
1037
  "is-focused": idx === vue.unref(focusedIndex),
1068
- "is-selected": c.id === vue.unref(selected).id
1038
+ "is-selected": c.id === vue.unref(country).id
1069
1039
  }
1070
1040
  ]),
1071
- "aria-selected": c.id === vue.unref(selected).id,
1041
+ "aria-selected": c.id === vue.unref(country).id,
1072
1042
  title: c.name,
1073
- onClick: ($event) => onSelectCountry(c.id),
1074
- onMouseenter: ($event) => focusedIndex.value = idx
1043
+ onClick: ($event) => vue.unref(selectCountry)(c.id),
1044
+ onMouseenter: ($event) => vue.unref(setFocusedIndex)(idx)
1075
1045
  }, [
1076
1046
  vue.createElementVNode("span", {
1077
1047
  class: "pi-flag",
@@ -1081,13 +1051,13 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
1081
1051
  vue.renderSlot(_ctx.$slots, "flag", { country: c }, () => [
1082
1052
  vue.createTextVNode(vue.toDisplayString(c.flag), 1)
1083
1053
  ], true)
1084
- ], 8, _hoisted_17),
1085
- vue.createElementVNode("span", _hoisted_18, vue.toDisplayString(c.name), 1),
1086
- vue.createElementVNode("span", _hoisted_19, vue.toDisplayString(c.code), 1)
1087
- ], 42, _hoisted_16);
1054
+ ], 8, _hoisted_15),
1055
+ vue.createElementVNode("span", _hoisted_16, vue.toDisplayString(c.name), 1),
1056
+ vue.createElementVNode("span", _hoisted_17, vue.toDisplayString(c.code), 1)
1057
+ ], 42, _hoisted_14);
1088
1058
  }), 128)),
1089
- vue.unref(filteredCountries).length === 0 ? (vue.openBlock(), vue.createElementBlock("li", _hoisted_20, vue.toDisplayString(__props.noResultsText), 1)) : vue.createCommentVNode("", true)
1090
- ], 8, _hoisted_15)
1059
+ vue.unref(filteredCountries).length === 0 ? (vue.openBlock(), vue.createElementBlock("li", _hoisted_18, vue.toDisplayString(__props.noResultsText), 1)) : vue.createCommentVNode("", true)
1060
+ ])
1091
1061
  ], 6)) : vue.createCommentVNode("", true)
1092
1062
  ]),
1093
1063
  _: 3
@@ -1112,61 +1082,18 @@ const _export_sfc = (sfc, props) => {
1112
1082
  }
1113
1083
  return target;
1114
1084
  };
1115
- const PhoneInput = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-755b15ee"]]);
1116
- function detectCountryFromLocale() {
1117
- try {
1118
- const lang = getNavigatorLang();
1119
- try {
1120
- const loc = new Intl.Locale(lang);
1121
- if (loc.region) return loc.region.toUpperCase();
1122
- } catch {
1123
- }
1124
- const parts = lang.split(/[-_]/);
1125
- if (parts.length > 1) return parts[1]?.toUpperCase() || null;
1126
- } catch {
1127
- }
1128
- return null;
1085
+ const PhoneInput = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-d730aa54"]]);
1086
+ function parseParams(params) {
1087
+ if (typeof params === "string") return { country: params };
1088
+ if (params && typeof params === "object") return params;
1089
+ return {};
1129
1090
  }
1130
- async function initState(binding) {
1131
- const value = binding.value;
1132
- let options = {};
1133
- if (typeof value === "string") {
1134
- options = { country: value };
1135
- } else if (typeof value === "object" && value !== null) {
1136
- options = value;
1137
- }
1138
- const locale = options.locale || getNavigatorLang();
1139
- let country;
1140
- if (options.country) {
1141
- country = getCountry(options.country, locale);
1142
- } else if (options.detect) {
1143
- const geoCountry = await detectCountryFromGeoIP();
1144
- if (geoCountry) {
1145
- country = getCountry(geoCountry, locale);
1146
- } else {
1147
- const localeCountry = detectCountryFromLocale();
1148
- if (localeCountry) {
1149
- country = getCountry(localeCountry, locale);
1150
- } else {
1151
- country = getCountry("US", locale);
1152
- }
1153
- }
1154
- } else {
1155
- country = getCountry("US", locale);
1156
- }
1157
- return {
1158
- country,
1159
- formatter: createPhoneFormatter(country),
1160
- digits: "",
1161
- locale,
1162
- options
1163
- };
1164
- }
1165
- function updateDisplay(el, state) {
1091
+ function updateDigits(el, state, digits) {
1092
+ state.digits = digits;
1166
1093
  el.value = state.formatter.formatDisplay(state.digits);
1167
1094
  if (state.options.onChange) {
1168
- const fullNumberFormatted = `${state.country.code} ${el.value}`;
1169
- const fullNumber = `${state.country.code}${state.digits}`;
1095
+ const fullNumberFormatted = el.value ? `${state.country.code} ${el.value}` : "";
1096
+ const fullNumber = state.digits ? `${state.country.code}${state.digits}` : "";
1170
1097
  state.options.onChange({
1171
1098
  full: fullNumber,
1172
1099
  fullFormatted: fullNumberFormatted,
@@ -1174,207 +1101,92 @@ function updateDisplay(el, state) {
1174
1101
  });
1175
1102
  }
1176
1103
  }
1177
- function createBeforeInputHandler(el) {
1178
- return (e2) => {
1179
- const data = e2.data;
1180
- if (e2.inputType !== "insertText" || !data) return;
1181
- if (InvalidPattern.test(data) || data === " " && el.value.endsWith(" ")) {
1182
- e2.preventDefault();
1183
- }
1184
- };
1185
- }
1186
- function createInputHandler(el, state) {
1187
- return (e2) => {
1188
- const target = e2.target;
1189
- if (!target) return;
1190
- const raw = target.value || "";
1191
- const maxDigits = state.formatter.getMaxDigits();
1192
- state.digits = extractDigits(raw, maxDigits);
1193
- updateDisplay(el, state);
1194
- vue.nextTick(() => {
1195
- const pos = state.formatter.getCaretPosition(state.digits.length);
1196
- setCaret(el, pos);
1197
- });
1198
- };
1104
+ function checkDigitsUpdate(el, state) {
1105
+ const maxDigits = state.formatter.getMaxDigits();
1106
+ const digits = extractDigits(el.value, maxDigits);
1107
+ const displayValue = state.formatter.formatDisplay(digits);
1108
+ if (digits !== state.digits || el.value !== displayValue) {
1109
+ updateDigits(el, state, digits);
1110
+ }
1199
1111
  }
1200
- function createKeydownHandler(el, state) {
1201
- return (e2) => {
1202
- if (e2.ctrlKey || e2.metaKey || e2.altKey || NavigationKeys.includes(e2.key)) return;
1203
- const [selStart, selEnd] = getSelection(el);
1204
- if (e2.key === "Backspace") {
1205
- e2.preventDefault();
1206
- if (selStart !== selEnd) {
1207
- const range = state.formatter.getDigitRange(state.digits, selStart, selEnd);
1208
- if (range) {
1209
- const [start, end] = range;
1210
- state.digits = state.digits.slice(0, start) + state.digits.slice(end);
1211
- updateDisplay(el, state);
1212
- vue.nextTick(() => {
1213
- const pos = state.formatter.getCaretPosition(start);
1214
- setCaret(el, pos);
1215
- });
1216
- }
1217
- return;
1218
- }
1219
- if (selStart > 0) {
1220
- const displayStr = el.value;
1221
- let prevPos = selStart - 1;
1222
- while (prevPos >= 0 && Delimiters.includes(displayStr[prevPos])) {
1223
- prevPos--;
1224
- }
1225
- if (prevPos >= 0) {
1226
- const range = state.formatter.getDigitRange(state.digits, prevPos, prevPos + 1);
1227
- if (range) {
1228
- const [start] = range;
1229
- state.digits = state.digits.slice(0, start) + state.digits.slice(start + 1);
1230
- updateDisplay(el, state);
1231
- vue.nextTick(() => {
1232
- const pos = state.formatter.getCaretPosition(start);
1233
- setCaret(el, pos);
1234
- });
1235
- }
1236
- }
1237
- }
1238
- return;
1239
- }
1240
- if (e2.key === "Delete") {
1241
- e2.preventDefault();
1242
- if (selStart !== selEnd) {
1243
- const range = state.formatter.getDigitRange(state.digits, selStart, selEnd);
1244
- if (range) {
1245
- const [start, end] = range;
1246
- state.digits = state.digits.slice(0, start) + state.digits.slice(end);
1247
- updateDisplay(el, state);
1248
- vue.nextTick(() => {
1249
- const pos = state.formatter.getCaretPosition(start);
1250
- setCaret(el, pos);
1251
- });
1252
- }
1253
- return;
1254
- }
1255
- if (selStart < el.value.length) {
1256
- const range = state.formatter.getDigitRange(state.digits, selStart, selStart + 1);
1257
- if (range) {
1258
- const [start] = range;
1259
- state.digits = state.digits.slice(0, start) + state.digits.slice(start + 1);
1260
- updateDisplay(el, state);
1261
- vue.nextTick(() => {
1262
- const pos = state.formatter.getCaretPosition(start);
1263
- setCaret(el, pos);
1264
- });
1265
- }
1266
- }
1267
- return;
1268
- }
1269
- if (/^[0-9]$/.test(e2.key)) {
1270
- if (state.digits.length >= state.formatter.getMaxDigits()) {
1271
- e2.preventDefault();
1272
- }
1273
- return;
1274
- }
1275
- if (e2.key.length === 1) {
1276
- e2.preventDefault();
1277
- }
1278
- };
1112
+ function checkCountryUpdate(el, state) {
1113
+ const oldCountry = state.country.id;
1114
+ const newCountry = parseCountryCode(state.options.country);
1115
+ if (newCountry && newCountry !== oldCountry) {
1116
+ setCountry(el, state, newCountry);
1117
+ }
1279
1118
  }
1280
- function createPasteHandler(el, state) {
1119
+ function createHandler(el, state, handler) {
1281
1120
  return (e2) => {
1282
- e2.preventDefault();
1283
- const text = e2.clipboardData?.getData("text") || "";
1284
- const maxDigits = state.formatter.getMaxDigits();
1285
- const pastedDigits = extractDigits(text, maxDigits);
1286
- if (pastedDigits.length === 0) return;
1287
- const [selStart, selEnd] = getSelection(el);
1288
- if (selStart !== selEnd) {
1289
- const range2 = state.formatter.getDigitRange(state.digits, selStart, selEnd);
1290
- if (range2) {
1291
- const [start, end] = range2;
1292
- const left2 = state.digits.slice(0, start);
1293
- const right2 = state.digits.slice(end);
1294
- state.digits = extractDigits(left2 + pastedDigits + right2, maxDigits);
1295
- updateDisplay(el, state);
1296
- vue.nextTick(() => {
1297
- const pos = state.formatter.getCaretPosition(start + pastedDigits.length);
1298
- setCaret(el, pos);
1299
- });
1300
- return;
1301
- }
1302
- }
1303
- const range = state.formatter.getDigitRange(state.digits, selStart, selStart);
1304
- const insertIndex = range ? range[0] : state.digits.length;
1305
- const left = state.digits.slice(0, insertIndex);
1306
- const right = state.digits.slice(insertIndex);
1307
- state.digits = extractDigits(left + pastedDigits + right, maxDigits);
1308
- updateDisplay(el, state);
1121
+ const result = handler(e2, state);
1122
+ if (!result) return;
1123
+ updateDigits(el, state, result.newDigits);
1309
1124
  vue.nextTick(() => {
1310
- const pos = state.formatter.getCaretPosition(insertIndex + pastedDigits.length);
1125
+ const pos = state.formatter.getCaretPosition(result.caretDigitIndex);
1311
1126
  setCaret(el, pos);
1312
1127
  });
1313
1128
  };
1314
1129
  }
1315
- async function updateCountry(el, state, newCountryCode) {
1316
- const newCountry = getCountry(newCountryCode, state.locale);
1130
+ async function detectInitialCountry(options) {
1131
+ const countryOption = parseCountryCode(options.country);
1132
+ if (countryOption) return countryOption;
1133
+ if (options.detect) {
1134
+ const geoCountry = parseCountryCode(await detectByGeoIp());
1135
+ if (geoCountry) return geoCountry;
1136
+ const localeCountry = parseCountryCode(detectCountryFromLocale());
1137
+ if (localeCountry) return localeCountry;
1138
+ }
1139
+ return "US";
1140
+ }
1141
+ function setCountry(el, state, newCountryCode) {
1142
+ const parsed = parseCountryCode(newCountryCode);
1143
+ if (!parsed) return;
1144
+ const newCountry = getCountry(parsed, state.locale);
1317
1145
  state.country = newCountry;
1146
+ state.options.onCountryChange?.(newCountry);
1318
1147
  state.formatter = createPhoneFormatter(newCountry);
1319
1148
  el.placeholder = state.formatter.getPlaceholder();
1320
- const maxDigits = state.formatter.getMaxDigits();
1321
- if (state.digits.length > maxDigits) {
1322
- state.digits = state.digits.slice(0, maxDigits);
1323
- }
1324
- updateDisplay(el, state);
1325
- if (state.options.onCountryChange) {
1326
- state.options.onCountryChange(newCountry);
1327
- }
1149
+ checkDigitsUpdate(el, state);
1328
1150
  }
1329
1151
  const vPhoneMask = {
1330
- async mounted(el, binding) {
1152
+ mounted(el, binding) {
1331
1153
  if (el.tagName !== "INPUT") {
1332
1154
  console.warn("[v-phone-mask] Directive can only be used on input elements");
1333
1155
  return;
1334
1156
  }
1335
1157
  el.setAttribute("type", "tel");
1336
1158
  el.setAttribute("inputmode", "tel");
1337
- const state = await initState(binding);
1159
+ el.setAttribute("placeholder", "");
1160
+ const options = parseParams(binding.value);
1161
+ const locale = options.locale || getNavigatorLang();
1162
+ const country = getCountry(parseCountryCode(options.country, "US"), locale);
1163
+ const state = {
1164
+ country,
1165
+ formatter: createPhoneFormatter(country),
1166
+ digits: "",
1167
+ locale,
1168
+ options
1169
+ };
1338
1170
  el.__phoneMaskState = state;
1339
- state.inputHandler = createInputHandler(el, state);
1340
- state.keydownHandler = createKeydownHandler(el, state);
1341
- state.pasteHandler = createPasteHandler(el, state);
1342
- state.beforeInputHandler = createBeforeInputHandler(el);
1171
+ state.inputHandler = createHandler(el, state, processInput);
1172
+ state.keydownHandler = createHandler(el, state, processKeydown);
1173
+ state.pasteHandler = createHandler(el, state, processPaste);
1174
+ state.beforeInputHandler = processBeforeInput;
1343
1175
  el.addEventListener("beforeinput", state.beforeInputHandler);
1344
1176
  el.addEventListener("input", state.inputHandler);
1345
1177
  el.addEventListener("keydown", state.keydownHandler);
1346
1178
  el.addEventListener("paste", state.pasteHandler);
1347
- el.setAttribute("placeholder", state.formatter.getPlaceholder());
1348
- if (state.options.onCountryChange) {
1349
- state.options.onCountryChange(state.country);
1350
- }
1351
- if (el.value) {
1352
- const maxDigits = state.formatter.getMaxDigits();
1353
- state.digits = extractDigits(el.value, maxDigits);
1354
- updateDisplay(el, state);
1355
- }
1179
+ detectInitialCountry(options).then((countryCode) => {
1180
+ if (el.__phoneMaskState !== state) return;
1181
+ setCountry(el, state, countryCode);
1182
+ });
1356
1183
  },
1357
- async updated(el, binding) {
1184
+ updated(el, binding) {
1358
1185
  const state = el.__phoneMaskState;
1359
1186
  if (!state) return;
1360
- const value = binding.value;
1361
- let newOptions = {};
1362
- if (typeof value === "string") {
1363
- newOptions = { country: value };
1364
- } else if (typeof value === "object" && value !== null) {
1365
- newOptions = value;
1366
- }
1367
- const oldCountry = state.options.country;
1368
- state.options = newOptions;
1369
- const newCountry = newOptions.country;
1370
- if (newCountry && newCountry !== oldCountry) {
1371
- await updateCountry(el, state, newCountry);
1372
- }
1373
- const newDigits = extractDigits(el.value);
1374
- if (newDigits !== state.digits) {
1375
- state.digits = newDigits;
1376
- updateDisplay(el, state);
1377
- }
1187
+ state.options = parseParams(binding.value);
1188
+ checkCountryUpdate(el, state);
1189
+ checkDigitsUpdate(el, state);
1378
1190
  },
1379
1191
  unmounted(el) {
1380
1192
  const state = el.__phoneMaskState;
@@ -1386,6 +1198,83 @@ const vPhoneMask = {
1386
1198
  delete el.__phoneMaskState;
1387
1199
  }
1388
1200
  };
1201
+ function usePhoneMask(options) {
1202
+ const inputRef = vue.shallowRef(null);
1203
+ const { country, setCountry: setCountry2 } = useCountry({
1204
+ country: options.country,
1205
+ locale: options.locale,
1206
+ detect: options.detect,
1207
+ onCountryChange: options.onCountryChange
1208
+ });
1209
+ const {
1210
+ digits,
1211
+ formatter,
1212
+ displayPlaceholder,
1213
+ displayValue,
1214
+ full,
1215
+ fullFormatted,
1216
+ isComplete,
1217
+ isEmpty,
1218
+ shouldShowWarn
1219
+ } = useFormatter({
1220
+ country,
1221
+ value: options.value,
1222
+ onChange: options.onChange,
1223
+ onPhoneChange: options.onPhoneChange
1224
+ });
1225
+ const { handleBeforeInput, handleInput, handleKeydown, handlePaste } = useInputHandlers({
1226
+ formatter,
1227
+ digits,
1228
+ onChange: options.onChange
1229
+ });
1230
+ vue.onMounted(() => {
1231
+ const el = inputRef.value;
1232
+ if (!el) return;
1233
+ el.setAttribute("type", "tel");
1234
+ el.setAttribute("inputmode", "tel");
1235
+ });
1236
+ vue.watchEffect(
1237
+ () => {
1238
+ const el = inputRef.value;
1239
+ if (!el) return;
1240
+ el.value = displayValue.value;
1241
+ el.setAttribute("placeholder", displayPlaceholder.value);
1242
+ },
1243
+ { flush: "post" }
1244
+ );
1245
+ vue.onMounted(() => {
1246
+ const el = inputRef.value;
1247
+ if (!el) return;
1248
+ el.addEventListener("beforeinput", handleBeforeInput);
1249
+ el.addEventListener("input", handleInput);
1250
+ el.addEventListener("keydown", handleKeydown);
1251
+ el.addEventListener("paste", handlePaste);
1252
+ });
1253
+ vue.onUnmounted(() => {
1254
+ const el = inputRef.value;
1255
+ if (!el) return;
1256
+ el.removeEventListener("beforeinput", handleBeforeInput);
1257
+ el.removeEventListener("input", handleInput);
1258
+ el.removeEventListener("keydown", handleKeydown);
1259
+ el.removeEventListener("paste", handlePaste);
1260
+ });
1261
+ const clear = () => {
1262
+ options.onChange("");
1263
+ };
1264
+ return {
1265
+ inputRef,
1266
+ digits,
1267
+ formatter,
1268
+ full,
1269
+ fullFormatted,
1270
+ isComplete,
1271
+ isEmpty,
1272
+ shouldShowWarn,
1273
+ country,
1274
+ setCountry: setCountry2,
1275
+ clear
1276
+ };
1277
+ }
1389
1278
  function install(app) {
1390
1279
  app.component("PhoneInput", PhoneInput);
1391
1280
  app.directive("phone-mask", vPhoneMask);
@@ -1394,7 +1283,7 @@ const index = {
1394
1283
  install
1395
1284
  };
1396
1285
  const PMaskHelpers = {
1397
- getFlagEmoji: g,
1286
+ getFlagEmoji: k,
1398
1287
  countPlaceholders,
1399
1288
  formatDigitsWithMap,
1400
1289
  pickMaskVariant,
@@ -1405,5 +1294,6 @@ exports.PMaskHelpers = PMaskHelpers;
1405
1294
  exports.PhoneInput = PhoneInput;
1406
1295
  exports.default = index;
1407
1296
  exports.install = install;
1297
+ exports.usePhoneMask = usePhoneMask;
1408
1298
  exports.vPhoneMask = vPhoneMask;
1409
- exports.vPhoneMaskSetCountry = updateCountry;
1299
+ exports.vPhoneMaskSetCountry = setCountry;