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