@jacksonavila/phone-lib 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +455 -0
- package/package.json +74 -0
- package/phone-lib-react.js +63 -0
- package/phone-lib-react.jsx +204 -0
- package/phone-lib.css +377 -0
- package/phone-lib.js +1251 -0
package/phone-lib.js
ADDED
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parsePhoneNumber,
|
|
3
|
+
getCountries,
|
|
4
|
+
getCountryCallingCode,
|
|
5
|
+
AsYouType
|
|
6
|
+
} from 'libphonenumber-js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Librería PhoneLib - Input de teléfono con selector de país
|
|
10
|
+
*/
|
|
11
|
+
class PhoneLib {
|
|
12
|
+
constructor(container, options = {}) {
|
|
13
|
+
this.container = typeof container === 'string'
|
|
14
|
+
? document.querySelector(container)
|
|
15
|
+
: container;
|
|
16
|
+
|
|
17
|
+
if (!this.container) {
|
|
18
|
+
throw new Error('El contenedor especificado no existe');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Opciones por defecto
|
|
22
|
+
this.options = {
|
|
23
|
+
initialCountry: options.initialCountry || 'US',
|
|
24
|
+
preferredCountries: options.preferredCountries || [],
|
|
25
|
+
showHint: options.showHint !== undefined ? options.showHint : true,
|
|
26
|
+
layout: options.layout || 'integrated', // 'integrated' o 'separated'
|
|
27
|
+
showDialCode: options.showDialCode !== undefined ? options.showDialCode : true, // Mostrar código de marcación
|
|
28
|
+
customClasses: options.customClasses || {}, // Clases CSS personalizadas
|
|
29
|
+
customStyles: options.customStyles || {}, // Estilos inline personalizados
|
|
30
|
+
// Callbacks y eventos
|
|
31
|
+
onCountryChange: options.onCountryChange || null,
|
|
32
|
+
onPhoneChange: options.onPhoneChange || null,
|
|
33
|
+
onValidationChange: options.onValidationChange || null,
|
|
34
|
+
onFocus: options.onFocus || null,
|
|
35
|
+
onBlur: options.onBlur || null,
|
|
36
|
+
// Configuración avanzada
|
|
37
|
+
autoDetectCountry: options.autoDetectCountry !== undefined ? options.autoDetectCountry : false,
|
|
38
|
+
validateOnInput: options.validateOnInput !== undefined ? options.validateOnInput : false,
|
|
39
|
+
disabledCountries: options.disabledCountries || [],
|
|
40
|
+
onlyCountries: options.onlyCountries || [],
|
|
41
|
+
excludeCountries: options.excludeCountries || [],
|
|
42
|
+
readonly: options.readonly !== undefined ? options.readonly : false,
|
|
43
|
+
disabled: options.disabled !== undefined ? options.disabled : false,
|
|
44
|
+
// Placeholders y labels personalizables
|
|
45
|
+
placeholder: options.placeholder || null,
|
|
46
|
+
countryLabel: options.countryLabel || 'País',
|
|
47
|
+
dialCodeLabel: options.dialCodeLabel || 'Código',
|
|
48
|
+
phoneLabel: options.phoneLabel || 'Número de teléfono',
|
|
49
|
+
// Mensajes personalizables
|
|
50
|
+
messages: {
|
|
51
|
+
invalid: options.messages?.invalid || 'Ingrese un número válido',
|
|
52
|
+
valid: options.messages?.valid || '✓ Número válido',
|
|
53
|
+
...options.messages
|
|
54
|
+
},
|
|
55
|
+
// ARIA labels personalizables
|
|
56
|
+
ariaLabels: {
|
|
57
|
+
dropdownButton: options.ariaLabels?.dropdownButton || 'Seleccionar país',
|
|
58
|
+
input: options.ariaLabels?.input || 'Número de teléfono',
|
|
59
|
+
dialCodeInput: options.ariaLabels?.dialCodeInput || 'Código de marcación',
|
|
60
|
+
...options.ariaLabels
|
|
61
|
+
},
|
|
62
|
+
...options
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Estado interno
|
|
66
|
+
this.selectedCountry = this.options.initialCountry;
|
|
67
|
+
this.phoneNumber = '';
|
|
68
|
+
this.isValid = false;
|
|
69
|
+
this.isDisabled = this.options.disabled;
|
|
70
|
+
this.isReadonly = this.options.readonly;
|
|
71
|
+
this.countries = this.getCountriesList();
|
|
72
|
+
|
|
73
|
+
// Inicializar
|
|
74
|
+
this.init();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Obtiene la lista de países con sus datos
|
|
79
|
+
*/
|
|
80
|
+
getCountriesList() {
|
|
81
|
+
const countries = getCountries();
|
|
82
|
+
let countriesList = countries.map(iso2 => {
|
|
83
|
+
const dialCode = getCountryCallingCode(iso2);
|
|
84
|
+
const flagHtml = this.getCountryFlag(iso2);
|
|
85
|
+
const name = this.getCountryName(iso2);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
iso2,
|
|
89
|
+
name,
|
|
90
|
+
dialCode,
|
|
91
|
+
flag: flagHtml // Ahora es HTML con imagen
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Filtrar países según configuración
|
|
96
|
+
if (this.options.onlyCountries.length > 0) {
|
|
97
|
+
countriesList = countriesList.filter(c => this.options.onlyCountries.includes(c.iso2));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (this.options.excludeCountries.length > 0) {
|
|
101
|
+
countriesList = countriesList.filter(c => !this.options.excludeCountries.includes(c.iso2));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this.options.disabledCountries.length > 0) {
|
|
105
|
+
countriesList = countriesList.map(c => ({
|
|
106
|
+
...c,
|
|
107
|
+
disabled: this.options.disabledCountries.includes(c.iso2)
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Ordenar por países preferidos primero, luego alfabéticamente
|
|
112
|
+
if (this.options.preferredCountries.length > 0) {
|
|
113
|
+
const preferred = [];
|
|
114
|
+
const others = [];
|
|
115
|
+
|
|
116
|
+
countriesList.forEach(country => {
|
|
117
|
+
if (this.options.preferredCountries.includes(country.iso2)) {
|
|
118
|
+
preferred.push(country);
|
|
119
|
+
} else {
|
|
120
|
+
others.push(country);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Ordenar preferidos según el orden especificado
|
|
125
|
+
preferred.sort((a, b) => {
|
|
126
|
+
const indexA = this.options.preferredCountries.indexOf(a.iso2);
|
|
127
|
+
const indexB = this.options.preferredCountries.indexOf(b.iso2);
|
|
128
|
+
return indexA - indexB;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Ordenar otros alfabéticamente
|
|
132
|
+
others.sort((a, b) => a.name.localeCompare(b.name));
|
|
133
|
+
|
|
134
|
+
return [...preferred, ...others];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return countriesList.sort((a, b) => a.name.localeCompare(b.name));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Obtiene la bandera del país usando imágenes PNG desde CDN
|
|
142
|
+
* Compatible con todos los navegadores, incluyendo Brave
|
|
143
|
+
* Usa múltiples CDNs como fallback
|
|
144
|
+
*/
|
|
145
|
+
getCountryFlag(iso2) {
|
|
146
|
+
if (!iso2 || iso2.length !== 2) {
|
|
147
|
+
return this.getFlagImage('UN'); // Bandera de la ONU como fallback
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const upperIso2 = iso2.toUpperCase();
|
|
151
|
+
const lowerIso2 = upperIso2.toLowerCase();
|
|
152
|
+
|
|
153
|
+
// Múltiples CDNs como fallback para compatibilidad con navegadores que bloquean contenido
|
|
154
|
+
// Usamos diferentes formatos y CDNs para maximizar compatibilidad con Brave
|
|
155
|
+
const cdnUrls = [
|
|
156
|
+
`https://flagcdn.com/w20/${lowerIso2}.png`,
|
|
157
|
+
`https://flagsapi.com/${upperIso2}/flat/32.png`,
|
|
158
|
+
`https://countryflagsapi.com/png/${lowerIso2}`,
|
|
159
|
+
`https://flagcdn.com/w20/${lowerIso2}.svg`, // SVG como alternativa
|
|
160
|
+
`data:image/svg+xml;base64,${this.getFlagSVGBase64(iso2)}` // Fallback SVG inline
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
// Retornar HTML con imagen PNG de bandera desde CDN
|
|
164
|
+
// Usamos múltiples CDNs como fallback para compatibilidad con Brave y otros navegadores
|
|
165
|
+
// El onerror intentará cargar los fallbacks en orden
|
|
166
|
+
return `<img src="${cdnUrls[0]}"
|
|
167
|
+
srcset="${cdnUrls[0].replace('w20', 'w40')} 2x"
|
|
168
|
+
alt="${upperIso2}"
|
|
169
|
+
class="phone-lib-flag-img"
|
|
170
|
+
loading="lazy"
|
|
171
|
+
crossorigin="anonymous"
|
|
172
|
+
referrerpolicy="no-referrer"
|
|
173
|
+
data-fallback-1="${cdnUrls[1]}"
|
|
174
|
+
data-fallback-2="${cdnUrls[2]}"
|
|
175
|
+
data-fallback-3="${cdnUrls[3]}"
|
|
176
|
+
data-fallback-4="${cdnUrls[4]}"
|
|
177
|
+
onerror="(function(img){img.onerror=null; if(img.dataset.fallback1 && !img.dataset.tried1) { img.src=img.dataset.fallback1; img.dataset.tried1='1'; } else if(img.dataset.fallback2 && !img.dataset.tried2) { img.src=img.dataset.fallback2; img.dataset.tried2='1'; } else if(img.dataset.fallback3 && !img.dataset.tried3) { img.src=img.dataset.fallback3; img.dataset.tried3='1'; } else if(img.dataset.fallback4) { img.src=img.dataset.fallback4; } else { img.style.display='none'; var span=document.createElement('span'); span.style.cssText='font-size:12px;color:#666;'; span.textContent='${upperIso2}'; img.parentElement.replaceChild(span, img); } })(this)">`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Obtiene la imagen de bandera como HTML
|
|
182
|
+
*/
|
|
183
|
+
getFlagImage(iso2) {
|
|
184
|
+
const upperIso2 = (iso2 || 'UN').toUpperCase();
|
|
185
|
+
const lowerIso2 = upperIso2.toLowerCase();
|
|
186
|
+
|
|
187
|
+
const cdnUrls = [
|
|
188
|
+
`https://flagcdn.com/w20/${lowerIso2}.png`,
|
|
189
|
+
`https://flagsapi.com/${upperIso2}/flat/32.png`,
|
|
190
|
+
`https://countryflagsapi.com/png/${lowerIso2}`,
|
|
191
|
+
`https://flagcdn.com/w20/${lowerIso2}.svg`,
|
|
192
|
+
`data:image/svg+xml;base64,${this.getFlagSVGBase64(iso2)}`
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
return `<img src="${cdnUrls[0]}"
|
|
196
|
+
srcset="${cdnUrls[0].replace('w20', 'w40')} 2x"
|
|
197
|
+
alt="${upperIso2}"
|
|
198
|
+
class="phone-lib-flag-img"
|
|
199
|
+
loading="lazy"
|
|
200
|
+
crossorigin="anonymous"
|
|
201
|
+
referrerpolicy="no-referrer"
|
|
202
|
+
data-fallback-1="${cdnUrls[1]}"
|
|
203
|
+
data-fallback-2="${cdnUrls[2]}"
|
|
204
|
+
data-fallback-3="${cdnUrls[3]}"
|
|
205
|
+
data-fallback-4="${cdnUrls[4]}"
|
|
206
|
+
onerror="(function(img){img.onerror=null; if(img.dataset.fallback1 && !img.dataset.tried1) { img.src=img.dataset.fallback1; img.dataset.tried1='1'; } else if(img.dataset.fallback2 && !img.dataset.tried2) { img.src=img.dataset.fallback2; img.dataset.tried2='1'; } else if(img.dataset.fallback3 && !img.dataset.tried3) { img.src=img.dataset.fallback3; img.dataset.tried3='1'; } else if(img.dataset.fallback4) { img.src=img.dataset.fallback4; } else { img.style.display='none'; var span=document.createElement('span'); span.style.cssText='font-size:12px;color:#666;'; span.textContent='${upperIso2}'; img.parentElement.replaceChild(span, img); } })(this)">`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Genera un SVG básico de bandera como fallback
|
|
211
|
+
*/
|
|
212
|
+
getFlagSVGBase64(iso2) {
|
|
213
|
+
// Generar un SVG simple con el código del país como fallback
|
|
214
|
+
const upperIso2 = (iso2 || 'UN').toUpperCase();
|
|
215
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="15" viewBox="0 0 20 15">
|
|
216
|
+
<rect width="20" height="15" fill="#e0e0e0"/>
|
|
217
|
+
<text x="10" y="11" font-family="Arial" font-size="10" fill="#666" text-anchor="middle">${upperIso2}</text>
|
|
218
|
+
</svg>`;
|
|
219
|
+
try {
|
|
220
|
+
return btoa(unescape(encodeURIComponent(svg)));
|
|
221
|
+
} catch (e) {
|
|
222
|
+
// Fallback si btoa no está disponible
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Intenta cargar la bandera usando fetch para detectar bloqueos
|
|
229
|
+
* Útil para navegadores como Brave que bloquean CDNs
|
|
230
|
+
*/
|
|
231
|
+
async checkFlagAvailability(url) {
|
|
232
|
+
try {
|
|
233
|
+
const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' });
|
|
234
|
+
return true;
|
|
235
|
+
} catch (e) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Obtiene el nombre del país en español
|
|
242
|
+
*/
|
|
243
|
+
getCountryName(iso2) {
|
|
244
|
+
const countryNames = {
|
|
245
|
+
'US': 'Estados Unidos',
|
|
246
|
+
'CO': 'Colombia',
|
|
247
|
+
'ES': 'España',
|
|
248
|
+
'MX': 'México',
|
|
249
|
+
'AR': 'Argentina',
|
|
250
|
+
'CL': 'Chile',
|
|
251
|
+
'PE': 'Perú',
|
|
252
|
+
'VE': 'Venezuela',
|
|
253
|
+
'EC': 'Ecuador',
|
|
254
|
+
'BO': 'Bolivia',
|
|
255
|
+
'PY': 'Paraguay',
|
|
256
|
+
'UY': 'Uruguay',
|
|
257
|
+
'CR': 'Costa Rica',
|
|
258
|
+
'PA': 'Panamá',
|
|
259
|
+
'GT': 'Guatemala',
|
|
260
|
+
'HN': 'Honduras',
|
|
261
|
+
'NI': 'Nicaragua',
|
|
262
|
+
'SV': 'El Salvador',
|
|
263
|
+
'DO': 'República Dominicana',
|
|
264
|
+
'CU': 'Cuba',
|
|
265
|
+
'PR': 'Puerto Rico',
|
|
266
|
+
'BR': 'Brasil',
|
|
267
|
+
'CA': 'Canadá',
|
|
268
|
+
'GB': 'Reino Unido',
|
|
269
|
+
'FR': 'Francia',
|
|
270
|
+
'DE': 'Alemania',
|
|
271
|
+
'IT': 'Italia',
|
|
272
|
+
'PT': 'Portugal',
|
|
273
|
+
'NL': 'Países Bajos',
|
|
274
|
+
'BE': 'Bélgica',
|
|
275
|
+
'CH': 'Suiza',
|
|
276
|
+
'AT': 'Austria',
|
|
277
|
+
'SE': 'Suecia',
|
|
278
|
+
'NO': 'Noruega',
|
|
279
|
+
'DK': 'Dinamarca',
|
|
280
|
+
'FI': 'Finlandia',
|
|
281
|
+
'PL': 'Polonia',
|
|
282
|
+
'CZ': 'República Checa',
|
|
283
|
+
'GR': 'Grecia',
|
|
284
|
+
'IE': 'Irlanda',
|
|
285
|
+
'NZ': 'Nueva Zelanda',
|
|
286
|
+
'AU': 'Australia',
|
|
287
|
+
'JP': 'Japón',
|
|
288
|
+
'CN': 'China',
|
|
289
|
+
'IN': 'India',
|
|
290
|
+
'KR': 'Corea del Sur',
|
|
291
|
+
'SG': 'Singapur',
|
|
292
|
+
'MY': 'Malasia',
|
|
293
|
+
'TH': 'Tailandia',
|
|
294
|
+
'PH': 'Filipinas',
|
|
295
|
+
'ID': 'Indonesia',
|
|
296
|
+
'VN': 'Vietnam',
|
|
297
|
+
'AE': 'Emiratos Árabes Unidos',
|
|
298
|
+
'SA': 'Arabia Saudí',
|
|
299
|
+
'IL': 'Israel',
|
|
300
|
+
'TR': 'Turquía',
|
|
301
|
+
'RU': 'Rusia',
|
|
302
|
+
'ZA': 'Sudáfrica',
|
|
303
|
+
'EG': 'Egipto',
|
|
304
|
+
'NG': 'Nigeria',
|
|
305
|
+
'KE': 'Kenia',
|
|
306
|
+
'GH': 'Ghana'
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
return countryNames[iso2] || iso2;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Emite un evento personalizado del DOM
|
|
314
|
+
*/
|
|
315
|
+
emitEvent(eventName, detail = {}) {
|
|
316
|
+
const event = new CustomEvent(`phoneLib:${eventName}`, {
|
|
317
|
+
detail: {
|
|
318
|
+
...detail,
|
|
319
|
+
instance: this
|
|
320
|
+
},
|
|
321
|
+
bubbles: true,
|
|
322
|
+
cancelable: true
|
|
323
|
+
});
|
|
324
|
+
this.container.dispatchEvent(event);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Ejecuta un callback si existe
|
|
329
|
+
*/
|
|
330
|
+
executeCallback(callbackName, ...args) {
|
|
331
|
+
if (this.options[callbackName] && typeof this.options[callbackName] === 'function') {
|
|
332
|
+
try {
|
|
333
|
+
this.options[callbackName](...args);
|
|
334
|
+
} catch (e) {
|
|
335
|
+
console.error(`Error en callback ${callbackName}:`, e);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Inicializa la librería
|
|
342
|
+
*/
|
|
343
|
+
init() {
|
|
344
|
+
this.render();
|
|
345
|
+
this.attachEventListeners();
|
|
346
|
+
this.updatePhoneNumber();
|
|
347
|
+
|
|
348
|
+
// Aplicar estados iniciales
|
|
349
|
+
if (this.isDisabled) {
|
|
350
|
+
this.disable();
|
|
351
|
+
}
|
|
352
|
+
if (this.isReadonly) {
|
|
353
|
+
this.setReadonly(true);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Renderiza el HTML
|
|
359
|
+
*/
|
|
360
|
+
render() {
|
|
361
|
+
if (this.options.layout === 'separated') {
|
|
362
|
+
this.renderSeparated();
|
|
363
|
+
} else {
|
|
364
|
+
this.renderIntegrated();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Aplica clases CSS personalizadas a un elemento
|
|
370
|
+
*/
|
|
371
|
+
applyCustomClasses(defaultClass, customClass) {
|
|
372
|
+
if (customClass) {
|
|
373
|
+
return `${defaultClass} ${customClass}`;
|
|
374
|
+
}
|
|
375
|
+
return defaultClass;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Aplica estilos inline personalizados
|
|
380
|
+
*/
|
|
381
|
+
applyCustomStyles(customStyles) {
|
|
382
|
+
if (!customStyles || typeof customStyles !== 'object') {
|
|
383
|
+
return '';
|
|
384
|
+
}
|
|
385
|
+
return Object.entries(customStyles)
|
|
386
|
+
.map(([key, value]) => {
|
|
387
|
+
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
388
|
+
return `${cssKey}: ${value};`;
|
|
389
|
+
})
|
|
390
|
+
.join(' ');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
renderIntegrated() {
|
|
394
|
+
const selectedCountryData = this.countries.find(c => c.iso2 === this.selectedCountry);
|
|
395
|
+
const defaultFlag = this.getFlagImage('UN');
|
|
396
|
+
|
|
397
|
+
const wrapperClass = this.applyCustomClasses(
|
|
398
|
+
'phone-lib-wrapper phone-lib-layout-integrated',
|
|
399
|
+
this.options.customClasses?.wrapper
|
|
400
|
+
);
|
|
401
|
+
const wrapperStyle = this.applyCustomStyles(this.options.customStyles?.wrapper);
|
|
402
|
+
|
|
403
|
+
const dropdownButtonClass = this.applyCustomClasses(
|
|
404
|
+
'phone-lib-dropdown-button',
|
|
405
|
+
this.options.customClasses?.dropdownButton
|
|
406
|
+
);
|
|
407
|
+
const dropdownButtonStyle = this.applyCustomStyles(this.options.customStyles?.dropdownButton);
|
|
408
|
+
|
|
409
|
+
const inputClass = this.applyCustomClasses(
|
|
410
|
+
'phone-lib-input',
|
|
411
|
+
this.options.customClasses?.input
|
|
412
|
+
);
|
|
413
|
+
const inputStyle = this.applyCustomStyles(this.options.customStyles?.input);
|
|
414
|
+
|
|
415
|
+
this.container.innerHTML = `
|
|
416
|
+
<div class="${wrapperClass}" ${wrapperStyle ? `style="${wrapperStyle}"` : ''}>
|
|
417
|
+
<div class="phone-lib-dropdown-container">
|
|
418
|
+
<button type="button" class="${dropdownButtonClass}" ${dropdownButtonStyle ? `style="${dropdownButtonStyle}"` : ''} aria-expanded="false" ${this.isDisabled ? 'disabled' : ''} aria-label="${this.options.ariaLabels.dropdownButton}">
|
|
419
|
+
<span class="phone-lib-flag">${selectedCountryData?.flag || defaultFlag}</span>
|
|
420
|
+
${this.options.showDialCode ? `<span class="phone-lib-dial-code">+${selectedCountryData?.dialCode || ''}</span>` : ''}
|
|
421
|
+
<span class="phone-lib-arrow">▼</span>
|
|
422
|
+
</button>
|
|
423
|
+
<div class="phone-lib-dropdown-menu" style="display: none;">
|
|
424
|
+
<div class="phone-lib-countries-list">
|
|
425
|
+
${this.countries.map(country => `
|
|
426
|
+
<div
|
|
427
|
+
class="phone-lib-country-item ${country.iso2 === this.selectedCountry ? 'selected' : ''} ${country.disabled ? 'disabled' : ''}"
|
|
428
|
+
data-iso2="${country.iso2}"
|
|
429
|
+
data-dial-code="${country.dialCode}"
|
|
430
|
+
${country.disabled ? 'style="opacity: 0.5; cursor: not-allowed;"' : ''}
|
|
431
|
+
role="option"
|
|
432
|
+
aria-selected="${country.iso2 === this.selectedCountry}"
|
|
433
|
+
>
|
|
434
|
+
<span class="phone-lib-flag">${country.flag}</span>
|
|
435
|
+
<span class="phone-lib-country-name">${country.name}</span>
|
|
436
|
+
${this.options.showDialCode ? `<span class="phone-lib-country-dial-code">+${country.dialCode}</span>` : ''}
|
|
437
|
+
</div>
|
|
438
|
+
`).join('')}
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
<div class="phone-lib-input-container">
|
|
443
|
+
<input
|
|
444
|
+
type="tel"
|
|
445
|
+
class="${inputClass}"
|
|
446
|
+
${inputStyle ? `style="${inputStyle}"` : ''}
|
|
447
|
+
placeholder="${this.options.placeholder || this.getPlaceholder()}"
|
|
448
|
+
autocomplete="tel"
|
|
449
|
+
${this.isDisabled ? 'disabled' : ''}
|
|
450
|
+
${this.isReadonly ? 'readonly' : ''}
|
|
451
|
+
aria-label="${this.options.ariaLabels?.input || 'Número de teléfono'}"
|
|
452
|
+
/>
|
|
453
|
+
${this.options.showHint ? `<div class="phone-lib-hint"></div>` : ''}
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
`;
|
|
457
|
+
|
|
458
|
+
this.attachReferences();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Renderiza el layout con campos separados
|
|
463
|
+
*/
|
|
464
|
+
renderSeparated() {
|
|
465
|
+
const selectedCountryData = this.countries.find(c => c.iso2 === this.selectedCountry);
|
|
466
|
+
const defaultFlag = this.getFlagImage('UN');
|
|
467
|
+
|
|
468
|
+
// Determinar las columnas del grid según si se muestra el código
|
|
469
|
+
const gridColumns = this.options.showDialCode ? '2fr 1fr 2fr' : '1fr 2fr';
|
|
470
|
+
|
|
471
|
+
const wrapperClass = this.applyCustomClasses(
|
|
472
|
+
'phone-lib-wrapper phone-lib-layout-separated',
|
|
473
|
+
this.options.customClasses?.wrapper
|
|
474
|
+
);
|
|
475
|
+
const wrapperStyle = this.applyCustomStyles(this.options.customStyles?.wrapper);
|
|
476
|
+
|
|
477
|
+
const rowStyle = this.applyCustomStyles(this.options.customStyles?.row);
|
|
478
|
+
const finalRowStyle = `grid-template-columns: ${gridColumns};${rowStyle ? ` ${rowStyle}` : ''}`;
|
|
479
|
+
|
|
480
|
+
const dropdownButtonClass = this.applyCustomClasses(
|
|
481
|
+
'phone-lib-dropdown-button-separated',
|
|
482
|
+
this.options.customClasses?.dropdownButton
|
|
483
|
+
);
|
|
484
|
+
const dropdownButtonStyle = this.applyCustomStyles(this.options.customStyles?.dropdownButton);
|
|
485
|
+
|
|
486
|
+
const dialCodeInputClass = this.applyCustomClasses(
|
|
487
|
+
'phone-lib-dial-code-input',
|
|
488
|
+
this.options.customClasses?.dialCodeInput
|
|
489
|
+
);
|
|
490
|
+
const dialCodeInputStyle = this.applyCustomStyles(this.options.customStyles?.dialCodeInput);
|
|
491
|
+
|
|
492
|
+
const inputClass = this.applyCustomClasses(
|
|
493
|
+
'phone-lib-input-separated',
|
|
494
|
+
this.options.customClasses?.input
|
|
495
|
+
);
|
|
496
|
+
const inputStyle = this.applyCustomStyles(this.options.customStyles?.input);
|
|
497
|
+
|
|
498
|
+
this.container.innerHTML = `
|
|
499
|
+
<div class="${wrapperClass}" ${wrapperStyle ? `style="${wrapperStyle}"` : ''}>
|
|
500
|
+
<div class="phone-lib-separated-row" style="${finalRowStyle}">
|
|
501
|
+
<div class="phone-lib-field-group">
|
|
502
|
+
<label class="phone-lib-label">${this.options.countryLabel}</label>
|
|
503
|
+
<div class="phone-lib-dropdown-container">
|
|
504
|
+
<button type="button" class="${dropdownButtonClass}" ${dropdownButtonStyle ? `style="${dropdownButtonStyle}"` : ''} aria-expanded="false" ${this.isDisabled ? 'disabled' : ''} aria-label="${this.options.ariaLabels.dropdownButton}">
|
|
505
|
+
<span class="phone-lib-flag">${selectedCountryData?.flag || defaultFlag}</span>
|
|
506
|
+
<span class="phone-lib-country-name-display">${selectedCountryData?.name || ''}</span>
|
|
507
|
+
<span class="phone-lib-arrow">▼</span>
|
|
508
|
+
</button>
|
|
509
|
+
<div class="phone-lib-dropdown-menu" style="display: none;">
|
|
510
|
+
<div class="phone-lib-countries-list">
|
|
511
|
+
${this.countries.map(country => `
|
|
512
|
+
<div
|
|
513
|
+
class="phone-lib-country-item ${country.iso2 === this.selectedCountry ? 'selected' : ''} ${country.disabled ? 'disabled' : ''}"
|
|
514
|
+
data-iso2="${country.iso2}"
|
|
515
|
+
data-dial-code="${country.dialCode}"
|
|
516
|
+
${country.disabled ? 'style="opacity: 0.5; cursor: not-allowed;"' : ''}
|
|
517
|
+
role="option"
|
|
518
|
+
aria-selected="${country.iso2 === this.selectedCountry}"
|
|
519
|
+
>
|
|
520
|
+
<span class="phone-lib-flag">${country.flag}</span>
|
|
521
|
+
<span class="phone-lib-country-name">${country.name}</span>
|
|
522
|
+
${this.options.showDialCode ? `<span class="phone-lib-country-dial-code">+${country.dialCode}</span>` : ''}
|
|
523
|
+
</div>
|
|
524
|
+
`).join('')}
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
${this.options.showDialCode ? `
|
|
531
|
+
<div class="phone-lib-field-group">
|
|
532
|
+
<label class="phone-lib-label">${this.options.dialCodeLabel}</label>
|
|
533
|
+
<input
|
|
534
|
+
type="text"
|
|
535
|
+
class="${dialCodeInputClass}"
|
|
536
|
+
${dialCodeInputStyle ? `style="${dialCodeInputStyle}"` : ''}
|
|
537
|
+
value="+${selectedCountryData?.dialCode || ''}"
|
|
538
|
+
readonly
|
|
539
|
+
disabled
|
|
540
|
+
aria-label="${this.options.ariaLabels?.dialCodeInput || 'Código de marcación'}"
|
|
541
|
+
/>
|
|
542
|
+
</div>
|
|
543
|
+
` : ''}
|
|
544
|
+
|
|
545
|
+
<div class="phone-lib-field-group phone-lib-field-group-phone">
|
|
546
|
+
<label class="phone-lib-label">${this.options.phoneLabel}</label>
|
|
547
|
+
<input
|
|
548
|
+
type="tel"
|
|
549
|
+
class="${inputClass}"
|
|
550
|
+
${inputStyle ? `style="${inputStyle}"` : ''}
|
|
551
|
+
placeholder="${this.options.placeholder || this.getPlaceholder()}"
|
|
552
|
+
autocomplete="tel"
|
|
553
|
+
${this.isDisabled ? 'disabled' : ''}
|
|
554
|
+
${this.isReadonly ? 'readonly' : ''}
|
|
555
|
+
aria-label="${this.options.ariaLabels.input}"
|
|
556
|
+
/>
|
|
557
|
+
${this.options.showHint ? `<div class="phone-lib-hint"></div>` : ''}
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
`;
|
|
562
|
+
|
|
563
|
+
this.attachReferences();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Adjunta las referencias a los elementos del DOM
|
|
568
|
+
*/
|
|
569
|
+
attachReferences() {
|
|
570
|
+
|
|
571
|
+
// Guardar referencias (compatibles con ambos layouts)
|
|
572
|
+
this.dropdownButton = this.container.querySelector('.phone-lib-dropdown-button') ||
|
|
573
|
+
this.container.querySelector('.phone-lib-dropdown-button-separated');
|
|
574
|
+
this.dropdownMenu = this.container.querySelector('.phone-lib-dropdown-menu');
|
|
575
|
+
this.countriesList = this.container.querySelector('.phone-lib-countries-list');
|
|
576
|
+
this.phoneInput = this.container.querySelector('.phone-lib-input') ||
|
|
577
|
+
this.container.querySelector('.phone-lib-input-separated');
|
|
578
|
+
this.hintElement = this.container.querySelector('.phone-lib-hint');
|
|
579
|
+
this.dialCodeInput = this.container.querySelector('.phone-lib-dial-code-input');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Obtiene el placeholder según el país
|
|
584
|
+
*/
|
|
585
|
+
getPlaceholder() {
|
|
586
|
+
try {
|
|
587
|
+
const formatter = new AsYouType(this.selectedCountry);
|
|
588
|
+
const example = formatter.input('1234567890');
|
|
589
|
+
return example || 'Ingrese número telefónico';
|
|
590
|
+
} catch (e) {
|
|
591
|
+
return 'Ingrese número telefónico';
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Adjunta los event listeners
|
|
597
|
+
*/
|
|
598
|
+
attachEventListeners() {
|
|
599
|
+
// Toggle dropdown
|
|
600
|
+
this.dropdownButton.addEventListener('click', (e) => {
|
|
601
|
+
if (this.isDisabled) return;
|
|
602
|
+
e.stopPropagation();
|
|
603
|
+
this.toggleDropdown();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// Cerrar dropdown al hacer click fuera
|
|
607
|
+
document.addEventListener('click', (e) => {
|
|
608
|
+
if (!this.container.contains(e.target)) {
|
|
609
|
+
this.closeDropdown();
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Seleccionar país
|
|
614
|
+
const countryItems = this.container.querySelectorAll('.phone-lib-country-item');
|
|
615
|
+
countryItems.forEach(item => {
|
|
616
|
+
item.addEventListener('click', () => {
|
|
617
|
+
if (item.classList.contains('disabled')) {
|
|
618
|
+
return; // No permitir seleccionar países deshabilitados
|
|
619
|
+
}
|
|
620
|
+
const iso2 = item.dataset.iso2;
|
|
621
|
+
const dialCode = item.dataset.dialCode;
|
|
622
|
+
this.selectCountry(iso2, dialCode);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// Input de teléfono
|
|
627
|
+
this.phoneInput.addEventListener('input', (e) => {
|
|
628
|
+
if (this.isDisabled || this.isReadonly) return;
|
|
629
|
+
this.handlePhoneInput(e.target.value);
|
|
630
|
+
|
|
631
|
+
// Validación en tiempo real si está habilitada
|
|
632
|
+
if (this.options.validateOnInput) {
|
|
633
|
+
this.validatePhone();
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
this.phoneInput.addEventListener('focus', () => {
|
|
638
|
+
this.executeCallback('onFocus');
|
|
639
|
+
this.emitEvent('focus');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
this.phoneInput.addEventListener('blur', () => {
|
|
643
|
+
const isValid = this.validatePhone();
|
|
644
|
+
this.executeCallback('onBlur', this.phoneNumber, isValid);
|
|
645
|
+
this.emitEvent('blur', { phoneNumber: this.phoneNumber, isValid });
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Navegación por teclado en dropdown
|
|
649
|
+
this.setupKeyboardNavigation();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Configura navegación por teclado
|
|
654
|
+
*/
|
|
655
|
+
setupKeyboardNavigation() {
|
|
656
|
+
if (!this.dropdownButton) return;
|
|
657
|
+
|
|
658
|
+
this.dropdownButton.addEventListener('keydown', (e) => {
|
|
659
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
660
|
+
e.preventDefault();
|
|
661
|
+
this.toggleDropdown();
|
|
662
|
+
} else if (e.key === 'Escape') {
|
|
663
|
+
this.closeDropdown();
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// Navegación con flechas en el dropdown
|
|
668
|
+
if (this.dropdownMenu) {
|
|
669
|
+
this.dropdownMenu.addEventListener('keydown', (e) => {
|
|
670
|
+
const items = Array.from(this.container.querySelectorAll('.phone-lib-country-item:not([style*="display: none"])'));
|
|
671
|
+
const currentIndex = items.findIndex(item => item.classList.contains('selected'));
|
|
672
|
+
|
|
673
|
+
if (e.key === 'ArrowDown') {
|
|
674
|
+
e.preventDefault();
|
|
675
|
+
const nextIndex = (currentIndex + 1) % items.length;
|
|
676
|
+
items[nextIndex]?.click();
|
|
677
|
+
items[nextIndex]?.scrollIntoView({ block: 'nearest' });
|
|
678
|
+
} else if (e.key === 'ArrowUp') {
|
|
679
|
+
e.preventDefault();
|
|
680
|
+
const prevIndex = currentIndex <= 0 ? items.length - 1 : currentIndex - 1;
|
|
681
|
+
items[prevIndex]?.click();
|
|
682
|
+
items[prevIndex]?.scrollIntoView({ block: 'nearest' });
|
|
683
|
+
} else if (e.key === 'Escape') {
|
|
684
|
+
this.closeDropdown();
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Maneja el input del teléfono
|
|
692
|
+
*/
|
|
693
|
+
handlePhoneInput(value) {
|
|
694
|
+
const previousNumber = this.phoneNumber;
|
|
695
|
+
this.phoneNumber = value;
|
|
696
|
+
|
|
697
|
+
// Detección automática de país si está habilitada
|
|
698
|
+
if (this.options.autoDetectCountry && value) {
|
|
699
|
+
this.autoDetectCountry(value);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
this.updatePhoneNumber();
|
|
703
|
+
|
|
704
|
+
// Ejecutar callbacks y eventos (validar sin actualizar visuals para obtener estado actual)
|
|
705
|
+
const isValid = this.phoneNumber ? this.validatePhone(false) : false;
|
|
706
|
+
this.executeCallback('onPhoneChange', this.phoneNumber, isValid, this.selectedCountry);
|
|
707
|
+
this.emitEvent('phoneChange', {
|
|
708
|
+
phoneNumber: this.phoneNumber,
|
|
709
|
+
isValid,
|
|
710
|
+
country: this.selectedCountry,
|
|
711
|
+
previousNumber
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Detecta automáticamente el país desde el número ingresado
|
|
717
|
+
*/
|
|
718
|
+
autoDetectCountry(phoneNumber) {
|
|
719
|
+
try {
|
|
720
|
+
// Intentar parsear el número sin país específico
|
|
721
|
+
// Si el número empieza con +, intentar detectar el país
|
|
722
|
+
if (phoneNumber.startsWith('+')) {
|
|
723
|
+
const parsed = parsePhoneNumber(phoneNumber);
|
|
724
|
+
if (parsed && parsed.country) {
|
|
725
|
+
const detectedCountry = parsed.country;
|
|
726
|
+
// Verificar que el país detectado esté disponible (no deshabilitado/excluido)
|
|
727
|
+
const countryAvailable = this.countries.find(c => c.iso2 === detectedCountry && !c.disabled);
|
|
728
|
+
if (countryAvailable && detectedCountry !== this.selectedCountry) {
|
|
729
|
+
const dialCode = getCountryCallingCode(detectedCountry);
|
|
730
|
+
this.selectCountry(detectedCountry, dialCode, true); // true = sin emitir evento (ya se emitirá en handlePhoneInput)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
} catch (e) {
|
|
735
|
+
// Si no se puede detectar, mantener el país actual
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Actualiza el número de teléfono y formatea
|
|
741
|
+
*/
|
|
742
|
+
updatePhoneNumber() {
|
|
743
|
+
if (!this.phoneNumber) {
|
|
744
|
+
if (this.hintElement) {
|
|
745
|
+
this.hintElement.textContent = '';
|
|
746
|
+
}
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
try {
|
|
751
|
+
const formatter = new AsYouType(this.selectedCountry);
|
|
752
|
+
const formatted = formatter.input(this.phoneNumber);
|
|
753
|
+
|
|
754
|
+
if (formatted && formatted !== this.phoneInput.value) {
|
|
755
|
+
this.phoneInput.value = formatted;
|
|
756
|
+
// Actualizar phoneNumber con el valor formateado para mantener sincronización
|
|
757
|
+
this.phoneNumber = formatted;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Actualizar hint
|
|
761
|
+
if (this.hintElement) {
|
|
762
|
+
const isValid = this.validatePhone(false); // false = no actualizar clases visuales todavía
|
|
763
|
+
this.hintElement.textContent = isValid
|
|
764
|
+
? this.options.messages.valid
|
|
765
|
+
: this.options.messages.invalid;
|
|
766
|
+
this.hintElement.className = `phone-lib-hint ${isValid ? 'valid' : 'invalid'}`;
|
|
767
|
+
}
|
|
768
|
+
} catch (e) {
|
|
769
|
+
// Ignorar errores de formato
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Valida el número de teléfono
|
|
775
|
+
*/
|
|
776
|
+
validatePhone(updateVisuals = true) {
|
|
777
|
+
if (!this.phoneNumber) {
|
|
778
|
+
this.isValid = false;
|
|
779
|
+
if (updateVisuals && this.phoneInput) {
|
|
780
|
+
this.phoneInput.classList.remove('phone-lib-input-invalid', 'phone-lib-input-valid');
|
|
781
|
+
}
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
try {
|
|
786
|
+
const phoneNumber = parsePhoneNumber(this.phoneNumber, this.selectedCountry);
|
|
787
|
+
const isValid = phoneNumber.isValid();
|
|
788
|
+
const previousValid = this.isValid;
|
|
789
|
+
this.isValid = isValid;
|
|
790
|
+
|
|
791
|
+
if (updateVisuals && this.phoneInput) {
|
|
792
|
+
this.phoneInput.classList.toggle('phone-lib-input-invalid', !isValid);
|
|
793
|
+
this.phoneInput.classList.toggle('phone-lib-input-valid', isValid);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Emitir evento de cambio de validación solo si cambió
|
|
797
|
+
if (previousValid !== isValid) {
|
|
798
|
+
this.executeCallback('onValidationChange', isValid, this.phoneNumber);
|
|
799
|
+
this.emitEvent('validationChange', { isValid, phoneNumber: this.phoneNumber });
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return isValid;
|
|
803
|
+
} catch (e) {
|
|
804
|
+
this.isValid = false;
|
|
805
|
+
if (updateVisuals && this.phoneInput) {
|
|
806
|
+
this.phoneInput.classList.add('phone-lib-input-invalid');
|
|
807
|
+
this.phoneInput.classList.remove('phone-lib-input-valid');
|
|
808
|
+
}
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Toggle del dropdown
|
|
815
|
+
*/
|
|
816
|
+
toggleDropdown() {
|
|
817
|
+
const isOpen = this.dropdownMenu.style.display !== 'none';
|
|
818
|
+
if (isOpen) {
|
|
819
|
+
this.closeDropdown();
|
|
820
|
+
} else {
|
|
821
|
+
this.openDropdown();
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Abre el dropdown
|
|
827
|
+
*/
|
|
828
|
+
openDropdown() {
|
|
829
|
+
if (this.isDisabled) return;
|
|
830
|
+
|
|
831
|
+
this.dropdownMenu.style.display = 'block';
|
|
832
|
+
this.dropdownButton.setAttribute('aria-expanded', 'true');
|
|
833
|
+
this.dropdownButton.classList.add('active');
|
|
834
|
+
|
|
835
|
+
// Scroll al país seleccionado
|
|
836
|
+
const selectedItem = this.container.querySelector('.phone-lib-country-item.selected');
|
|
837
|
+
if (selectedItem) {
|
|
838
|
+
selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Cierra el dropdown
|
|
844
|
+
*/
|
|
845
|
+
closeDropdown() {
|
|
846
|
+
this.dropdownMenu.style.display = 'none';
|
|
847
|
+
this.dropdownButton.setAttribute('aria-expanded', 'false');
|
|
848
|
+
this.dropdownButton.classList.remove('active');
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Selecciona un país
|
|
853
|
+
*/
|
|
854
|
+
selectCountry(iso2, dialCode, silent = false) {
|
|
855
|
+
const previousCountry = this.selectedCountry;
|
|
856
|
+
this.selectedCountry = iso2;
|
|
857
|
+
|
|
858
|
+
// Actualizar botón según el layout
|
|
859
|
+
const countryData = this.countries.find(c => c.iso2 === iso2);
|
|
860
|
+
|
|
861
|
+
if (this.options.layout === 'separated') {
|
|
862
|
+
// Layout separado: actualizar flag, nombre del país y código de marcación (si está visible)
|
|
863
|
+
const flagElement = this.dropdownButton.querySelector('.phone-lib-flag');
|
|
864
|
+
const nameElement = this.dropdownButton.querySelector('.phone-lib-country-name-display');
|
|
865
|
+
|
|
866
|
+
if (flagElement && countryData) {
|
|
867
|
+
flagElement.innerHTML = countryData.flag;
|
|
868
|
+
}
|
|
869
|
+
if (nameElement && countryData) {
|
|
870
|
+
nameElement.textContent = countryData.name;
|
|
871
|
+
}
|
|
872
|
+
if (this.dialCodeInput && countryData && this.options.showDialCode) {
|
|
873
|
+
this.dialCodeInput.value = `+${dialCode}`;
|
|
874
|
+
}
|
|
875
|
+
} else {
|
|
876
|
+
// Layout integrado: actualizar flag y código de marcación (si está visible)
|
|
877
|
+
const flagElement = this.dropdownButton.querySelector('.phone-lib-flag');
|
|
878
|
+
const dialCodeElement = this.dropdownButton.querySelector('.phone-lib-dial-code');
|
|
879
|
+
|
|
880
|
+
if (flagElement && countryData) {
|
|
881
|
+
flagElement.innerHTML = countryData.flag;
|
|
882
|
+
}
|
|
883
|
+
if (dialCodeElement && this.options.showDialCode) {
|
|
884
|
+
dialCodeElement.textContent = `+${dialCode}`;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Actualizar selección en lista
|
|
889
|
+
const countryItems = this.container.querySelectorAll('.phone-lib-country-item');
|
|
890
|
+
countryItems.forEach(item => {
|
|
891
|
+
item.classList.toggle('selected', item.dataset.iso2 === iso2);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
// Cerrar dropdown
|
|
895
|
+
this.closeDropdown();
|
|
896
|
+
|
|
897
|
+
// Actualizar placeholder y formato
|
|
898
|
+
this.phoneInput.placeholder = this.getPlaceholder();
|
|
899
|
+
this.updatePhoneNumber();
|
|
900
|
+
|
|
901
|
+
// Ejecutar callbacks y eventos solo si no es silencioso
|
|
902
|
+
if (!silent) {
|
|
903
|
+
this.executeCallback('onCountryChange', iso2, dialCode, countryData?.name);
|
|
904
|
+
this.emitEvent('countryChange', {
|
|
905
|
+
country: iso2,
|
|
906
|
+
dialCode: `+${dialCode}`,
|
|
907
|
+
countryName: countryData?.name,
|
|
908
|
+
previousCountry
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ========== API PÚBLICA ==========
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Devuelve el país seleccionado (ISO2)
|
|
917
|
+
*/
|
|
918
|
+
getCountry() {
|
|
919
|
+
return this.selectedCountry;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Devuelve el código de marcación del país
|
|
924
|
+
*/
|
|
925
|
+
getDialCode() {
|
|
926
|
+
const countryData = this.countries.find(c => c.iso2 === this.selectedCountry);
|
|
927
|
+
return countryData ? `+${countryData.dialCode}` : '';
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Devuelve el número ingresado sin formato (solo dígitos)
|
|
932
|
+
*/
|
|
933
|
+
getRaw() {
|
|
934
|
+
if (!this.phoneNumber) {
|
|
935
|
+
return '';
|
|
936
|
+
}
|
|
937
|
+
// Extraer solo los dígitos del número
|
|
938
|
+
return this.phoneNumber.replace(/\D/g, '');
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Devuelve el número en formato E.164
|
|
943
|
+
*/
|
|
944
|
+
getE164() {
|
|
945
|
+
if (!this.phoneNumber) {
|
|
946
|
+
return '';
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
try {
|
|
950
|
+
const phoneNumber = parsePhoneNumber(this.phoneNumber, this.selectedCountry);
|
|
951
|
+
return phoneNumber.number || '';
|
|
952
|
+
} catch (e) {
|
|
953
|
+
return '';
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Devuelve true/false según si el número es válido
|
|
959
|
+
*/
|
|
960
|
+
isValid() {
|
|
961
|
+
// Usar el estado actual si está disponible, sino validar sin actualizar visuals
|
|
962
|
+
if (this.phoneNumber) {
|
|
963
|
+
return this.validatePhone(false);
|
|
964
|
+
}
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Devuelve el número en formato internacional
|
|
970
|
+
*/
|
|
971
|
+
formatInternational() {
|
|
972
|
+
if (!this.phoneNumber) {
|
|
973
|
+
return '';
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
try {
|
|
977
|
+
const phoneNumber = parsePhoneNumber(this.phoneNumber, this.selectedCountry);
|
|
978
|
+
return phoneNumber.formatInternational() || '';
|
|
979
|
+
} catch (e) {
|
|
980
|
+
return '';
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Devuelve el número en formato nacional
|
|
986
|
+
*/
|
|
987
|
+
formatNational() {
|
|
988
|
+
if (!this.phoneNumber) {
|
|
989
|
+
return '';
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
try {
|
|
993
|
+
const phoneNumber = parsePhoneNumber(this.phoneNumber, this.selectedCountry);
|
|
994
|
+
return phoneNumber.formatNational() || '';
|
|
995
|
+
} catch (e) {
|
|
996
|
+
return '';
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Devuelve el número en formato RFC3966
|
|
1002
|
+
*/
|
|
1003
|
+
formatRFC3966() {
|
|
1004
|
+
if (!this.phoneNumber) {
|
|
1005
|
+
return '';
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
try {
|
|
1009
|
+
const phoneNumber = parsePhoneNumber(this.phoneNumber, this.selectedCountry);
|
|
1010
|
+
return phoneNumber.format('RFC3966') || '';
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
return '';
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Devuelve el tipo de número (MOBILE, FIXED_LINE, etc.)
|
|
1018
|
+
*/
|
|
1019
|
+
getNumberType() {
|
|
1020
|
+
if (!this.phoneNumber) {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
try {
|
|
1025
|
+
const phoneNumber = parsePhoneNumber(this.phoneNumber, this.selectedCountry);
|
|
1026
|
+
return phoneNumber.getType() || null;
|
|
1027
|
+
} catch (e) {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Devuelve información completa del número en un objeto
|
|
1034
|
+
*/
|
|
1035
|
+
getInfo() {
|
|
1036
|
+
const countryData = this.countries.find(c => c.iso2 === this.selectedCountry);
|
|
1037
|
+
|
|
1038
|
+
return {
|
|
1039
|
+
country: this.selectedCountry,
|
|
1040
|
+
dialCode: this.getDialCode(),
|
|
1041
|
+
raw: this.getRaw(),
|
|
1042
|
+
e164: this.getE164(),
|
|
1043
|
+
international: this.formatInternational(),
|
|
1044
|
+
national: this.formatNational(),
|
|
1045
|
+
rfc3966: this.formatRFC3966(),
|
|
1046
|
+
isValid: this.isValid,
|
|
1047
|
+
type: this.getNumberType(),
|
|
1048
|
+
countryName: countryData?.name || this.selectedCountry
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Devuelve metadata del país seleccionado
|
|
1054
|
+
*/
|
|
1055
|
+
getCountryMetadata() {
|
|
1056
|
+
const countryData = this.countries.find(c => c.iso2 === this.selectedCountry);
|
|
1057
|
+
if (!countryData) {
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
return {
|
|
1062
|
+
iso2: countryData.iso2,
|
|
1063
|
+
name: countryData.name,
|
|
1064
|
+
dialCode: `+${countryData.dialCode}`,
|
|
1065
|
+
flag: countryData.flag
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// ========== MÉTODOS DE CONTROL PROGRAMÁTICO ==========
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Establece el país programáticamente
|
|
1073
|
+
*/
|
|
1074
|
+
setCountry(iso2) {
|
|
1075
|
+
const countryData = this.countries.find(c => c.iso2 === iso2);
|
|
1076
|
+
if (!countryData) {
|
|
1077
|
+
console.warn(`País ${iso2} no encontrado`);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
this.selectCountry(iso2, countryData.dialCode);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Establece el número telefónico programáticamente
|
|
1086
|
+
*/
|
|
1087
|
+
setPhoneNumber(number) {
|
|
1088
|
+
if (this.isDisabled || this.isReadonly) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
this.phoneNumber = number;
|
|
1093
|
+
if (this.phoneInput) {
|
|
1094
|
+
this.phoneInput.value = number;
|
|
1095
|
+
}
|
|
1096
|
+
this.updatePhoneNumber();
|
|
1097
|
+
|
|
1098
|
+
// Ejecutar callbacks
|
|
1099
|
+
const isValid = this.isValid;
|
|
1100
|
+
this.executeCallback('onPhoneChange', this.phoneNumber, isValid, this.selectedCountry);
|
|
1101
|
+
this.emitEvent('phoneChange', {
|
|
1102
|
+
phoneNumber: this.phoneNumber,
|
|
1103
|
+
isValid,
|
|
1104
|
+
country: this.selectedCountry
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Establece país y número juntos
|
|
1110
|
+
*/
|
|
1111
|
+
setValue(country, number) {
|
|
1112
|
+
this.setCountry(country);
|
|
1113
|
+
this.setPhoneNumber(number);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Habilita el componente
|
|
1118
|
+
*/
|
|
1119
|
+
enable() {
|
|
1120
|
+
this.isDisabled = false;
|
|
1121
|
+
|
|
1122
|
+
if (this.phoneInput) {
|
|
1123
|
+
this.phoneInput.disabled = false;
|
|
1124
|
+
}
|
|
1125
|
+
if (this.dropdownButton) {
|
|
1126
|
+
this.dropdownButton.disabled = false;
|
|
1127
|
+
}
|
|
1128
|
+
if (this.container) {
|
|
1129
|
+
this.container.classList.remove('phone-lib-disabled');
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Deshabilita el componente
|
|
1135
|
+
*/
|
|
1136
|
+
disable() {
|
|
1137
|
+
this.isDisabled = true;
|
|
1138
|
+
|
|
1139
|
+
if (this.phoneInput) {
|
|
1140
|
+
this.phoneInput.disabled = true;
|
|
1141
|
+
}
|
|
1142
|
+
if (this.dropdownButton) {
|
|
1143
|
+
this.dropdownButton.disabled = true;
|
|
1144
|
+
}
|
|
1145
|
+
if (this.container) {
|
|
1146
|
+
this.container.classList.add('phone-lib-disabled');
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
this.closeDropdown();
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Establece modo solo lectura
|
|
1154
|
+
*/
|
|
1155
|
+
setReadonly(readonly) {
|
|
1156
|
+
this.isReadonly = readonly;
|
|
1157
|
+
|
|
1158
|
+
if (this.phoneInput) {
|
|
1159
|
+
this.phoneInput.readOnly = readonly;
|
|
1160
|
+
}
|
|
1161
|
+
if (this.container) {
|
|
1162
|
+
this.container.classList.toggle('phone-lib-readonly', readonly);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Resetea a valores iniciales
|
|
1168
|
+
*/
|
|
1169
|
+
reset() {
|
|
1170
|
+
this.selectedCountry = this.options.initialCountry;
|
|
1171
|
+
this.phoneNumber = '';
|
|
1172
|
+
this.isValid = false;
|
|
1173
|
+
|
|
1174
|
+
if (this.phoneInput) {
|
|
1175
|
+
this.phoneInput.value = '';
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Re-renderizar para actualizar visualmente
|
|
1179
|
+
this.render();
|
|
1180
|
+
this.attachEventListeners();
|
|
1181
|
+
|
|
1182
|
+
// Ejecutar callbacks
|
|
1183
|
+
this.executeCallback('onCountryChange', this.selectedCountry, this.getDialCode());
|
|
1184
|
+
this.executeCallback('onPhoneChange', '', false, this.selectedCountry);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Destruye la instancia y limpia recursos
|
|
1189
|
+
*/
|
|
1190
|
+
destroy() {
|
|
1191
|
+
// Remover event listeners
|
|
1192
|
+
if (this.phoneInput) {
|
|
1193
|
+
const newInput = this.phoneInput.cloneNode(true);
|
|
1194
|
+
this.phoneInput.parentNode.replaceChild(newInput, this.phoneInput);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Limpiar contenedor
|
|
1198
|
+
if (this.container) {
|
|
1199
|
+
this.container.innerHTML = '';
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Limpiar referencias
|
|
1203
|
+
this.dropdownButton = null;
|
|
1204
|
+
this.dropdownMenu = null;
|
|
1205
|
+
this.phoneInput = null;
|
|
1206
|
+
this.hintElement = null;
|
|
1207
|
+
this.dialCodeInput = null;
|
|
1208
|
+
this.countriesList = null;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Actualiza opciones dinámicamente
|
|
1213
|
+
*/
|
|
1214
|
+
updateOptions(newOptions) {
|
|
1215
|
+
// Actualizar opciones
|
|
1216
|
+
this.options = { ...this.options, ...newOptions };
|
|
1217
|
+
|
|
1218
|
+
// Re-aplicar estados si cambiaron
|
|
1219
|
+
if (newOptions.disabled !== undefined) {
|
|
1220
|
+
this.isDisabled = newOptions.disabled;
|
|
1221
|
+
if (this.isDisabled) {
|
|
1222
|
+
this.disable();
|
|
1223
|
+
} else {
|
|
1224
|
+
this.enable();
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
if (newOptions.readonly !== undefined) {
|
|
1229
|
+
this.setReadonly(newOptions.readonly);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Si cambió el país inicial, actualizar
|
|
1233
|
+
if (newOptions.initialCountry && newOptions.initialCountry !== this.selectedCountry) {
|
|
1234
|
+
this.setCountry(newOptions.initialCountry);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Re-renderizar si cambió layout u opciones visuales importantes
|
|
1238
|
+
if (newOptions.layout || newOptions.showDialCode !== undefined || newOptions.customClasses || newOptions.customStyles) {
|
|
1239
|
+
this.render();
|
|
1240
|
+
this.attachEventListeners();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Exportar para uso en módulos ES6
|
|
1246
|
+
export default PhoneLib;
|
|
1247
|
+
|
|
1248
|
+
// También exportar para uso global si se carga directamente
|
|
1249
|
+
if (typeof window !== 'undefined') {
|
|
1250
|
+
window.PhoneLib = PhoneLib;
|
|
1251
|
+
}
|