@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/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
+ }