@jacksonavila/phone-lib 2.0.5 → 2.0.7

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 CHANGED
@@ -61,7 +61,7 @@ You can use PhoneLib directly from CDN without npm / Puedes usar PhoneLib direct
61
61
  <!DOCTYPE html>
62
62
  <html>
63
63
  <head>
64
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.5/phone-lib.css">
64
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.7/phone-lib.css">
65
65
  </head>
66
66
  <body>
67
67
  <div id="phone-container"></div>
@@ -69,7 +69,7 @@ You can use PhoneLib directly from CDN without npm / Puedes usar PhoneLib direct
69
69
  <script type="importmap">
70
70
  {
71
71
  "imports": {
72
- "@jacksonavila/phone-lib": "https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.5/phone-lib.js",
72
+ "@jacksonavila/phone-lib": "https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.7/phone-lib.js",
73
73
  "libphonenumber-js": "https://esm.sh/libphonenumber-js@1.11.0"
74
74
  }
75
75
  }
@@ -89,8 +89,8 @@ You can use PhoneLib directly from CDN without npm / Puedes usar PhoneLib direct
89
89
  ```
90
90
 
91
91
  **CDN URLs / URLs de CDN:**
92
- - **jsDelivr:** `https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.5/`
93
- - **unpkg:** `https://unpkg.com/@jacksonavila/phone-lib@2.0.5/`
92
+ - **jsDelivr:** `https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.7/`
93
+ - **unpkg:** `https://unpkg.com/@jacksonavila/phone-lib@2.0.7/`
94
94
 
95
95
  ### Method 2: Script Tag (All Browsers) / Método 2: Script Tag (Todos los Navegadores)
96
96
 
@@ -98,12 +98,12 @@ You can use PhoneLib directly from CDN without npm / Puedes usar PhoneLib direct
98
98
  <!DOCTYPE html>
99
99
  <html>
100
100
  <head>
101
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.5/phone-lib.css">
101
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.7/phone-lib.css">
102
102
  </head>
103
103
  <body>
104
104
  <div id="phone-container"></div>
105
105
 
106
- <script src="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.5/phone-lib.cdn.js"></script>
106
+ <script src="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.7/phone-lib.cdn.js"></script>
107
107
 
108
108
  <script>
109
109
  let phoneLib = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jacksonavila/phone-lib",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Librería JavaScript para input de teléfono con selector de país y banderas - Compatible con Vanilla JS y React",
5
5
  "main": "phone-lib.js",
6
6
  "module": "phone-lib.js",
package/phone-lib.cdn.js CHANGED
@@ -4,8 +4,8 @@
4
4
  * Carga libphonenumber-js dinámicamente y expone PhoneLib globalmente
5
5
  *
6
6
  * Uso:
7
- * <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.5/phone-lib.css">
8
- * <script src="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.5/phone-lib.cdn.js"></script>
7
+ * <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.7/phone-lib.css">
8
+ * <script src="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.7/phone-lib.cdn.js"></script>
9
9
  * <script>
10
10
  * document.addEventListener('phoneLibReady', () => {
11
11
  * const phoneLib = new PhoneLib('#container', {...});
@@ -17,7 +17,7 @@
17
17
  'use strict';
18
18
 
19
19
  // Versión del paquete (actualizar cuando se publique nueva versión)
20
- const PACKAGE_VERSION = '2.0.5';
20
+ const PACKAGE_VERSION = '2.0.7';
21
21
  const PACKAGE_NAME = '@jacksonavila/phone-lib';
22
22
 
23
23
  // URLs de CDN
package/phone-lib.js CHANGED
@@ -406,14 +406,17 @@ class PhoneLib {
406
406
  * Obtiene el HTML del icono de flecha
407
407
  */
408
408
  getArrowIcon() {
409
- // Si hay un icono personalizado, usarlo
410
- if (this.options.arrowIcon) {
411
- return this.options.arrowIcon;
409
+ // Si hay un icono personalizado, usarlo (validar que sea string)
410
+ if (this.options.arrowIcon && typeof this.options.arrowIcon === 'string') {
411
+ // Sanitización básica: remover scripts potencialmente peligrosos
412
+ // Nota: En producción, considerar usar una librería de sanitización
413
+ const sanitized = this.options.arrowIcon.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
414
+ return sanitized;
412
415
  }
413
416
 
414
417
  // Chevron SVG por defecto (simple, apuntando hacia abajo - solo líneas, sin relleno)
415
418
  // Forma de chevron: dos líneas que forman una V apuntando hacia abajo
416
- // Usa dos paths separados para asegurar que se vea como chevron, no triángulo
419
+ // Usa dos líneas separadas para asegurar que se vea como chevron, no triángulo
417
420
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
418
421
  <line x1="3" y1="5" x2="6" y2="8"/>
419
422
  <line x1="6" y1="8" x2="9" y2="5"/>
@@ -657,94 +660,183 @@ class PhoneLib {
657
660
  * Adjunta los event listeners
658
661
  */
659
662
  attachEventListeners() {
663
+ // Remover listeners anteriores si existen (para evitar duplicados)
664
+ if (this._boundHandlers) {
665
+ this.removeEventListeners();
666
+ }
667
+
668
+ // Guardar referencias a handlers para poder removerlos después
669
+ this._boundHandlers = {
670
+ dropdownClick: (e) => {
671
+ if (this.isDisabled) return;
672
+ e.stopPropagation();
673
+ this.toggleDropdown();
674
+ },
675
+ documentClick: (e) => {
676
+ if (!this.container.contains(e.target)) {
677
+ this.closeDropdown();
678
+ }
679
+ }
680
+ };
681
+
660
682
  // Toggle dropdown
661
- this.dropdownButton.addEventListener('click', (e) => {
662
- if (this.isDisabled) return;
663
- e.stopPropagation();
664
- this.toggleDropdown();
665
- });
683
+ if (this.dropdownButton) {
684
+ this.dropdownButton.addEventListener('click', this._boundHandlers.dropdownClick);
685
+ }
666
686
 
667
687
  // Cerrar dropdown al hacer click fuera
668
- document.addEventListener('click', (e) => {
669
- if (!this.container.contains(e.target)) {
670
- this.closeDropdown();
671
- }
672
- });
688
+ document.addEventListener('click', this._boundHandlers.documentClick);
673
689
 
674
690
  // Seleccionar país
675
691
  const countryItems = this.container.querySelectorAll('.phone-lib-country-item');
692
+ this._countryItemHandlers = [];
676
693
  countryItems.forEach(item => {
677
- item.addEventListener('click', () => {
694
+ const handler = () => {
678
695
  if (item.classList.contains('disabled')) {
679
696
  return; // No permitir seleccionar países deshabilitados
680
697
  }
681
698
  const iso2 = item.dataset.iso2;
682
699
  const dialCode = item.dataset.dialCode;
683
- this.selectCountry(iso2, dialCode);
684
- });
700
+ if (iso2 && dialCode) {
701
+ this.selectCountry(iso2, dialCode);
702
+ }
703
+ };
704
+ item.addEventListener('click', handler);
705
+ this._countryItemHandlers.push({ item, handler });
685
706
  });
686
707
 
687
708
  // Input de teléfono
688
- this.phoneInput.addEventListener('input', (e) => {
689
- if (this.isDisabled || this.isReadonly) return;
690
- this.handlePhoneInput(e.target.value);
709
+ if (this.phoneInput) {
710
+ this._boundHandlers.phoneInput = (e) => {
711
+ if (this.isDisabled || this.isReadonly) return;
691
712
 
692
- // Validación en tiempo real si está habilitada
693
- if (this.options.validateOnInput) {
694
- this.validatePhone();
695
- }
696
- });
713
+ // Obtener el valor actual del input
714
+ const inputValue = e.target.value;
697
715
 
698
- this.phoneInput.addEventListener('focus', () => {
699
- this.executeCallback('onFocus');
700
- this.emitEvent('focus');
701
- });
716
+ // Procesar el input
717
+ this.handlePhoneInput(inputValue);
702
718
 
703
- this.phoneInput.addEventListener('blur', () => {
704
- const isValid = this.validatePhone();
705
- this.executeCallback('onBlur', this.phoneNumber, isValid);
706
- this.emitEvent('blur', { phoneNumber: this.phoneNumber, isValid });
707
- });
719
+ // Validación en tiempo real si está habilitada
720
+ if (this.options.validateOnInput) {
721
+ this.validatePhone();
722
+ }
723
+ };
724
+
725
+ this._boundHandlers.phoneFocus = () => {
726
+ this.executeCallback('onFocus');
727
+ this.emitEvent('focus');
728
+ };
729
+
730
+ this._boundHandlers.phoneBlur = () => {
731
+ const isValid = this.validatePhone();
732
+ this.executeCallback('onBlur', this.phoneNumber, isValid);
733
+ this.emitEvent('blur', { phoneNumber: this.phoneNumber, isValid });
734
+ };
735
+
736
+ this.phoneInput.addEventListener('input', this._boundHandlers.phoneInput);
737
+ this.phoneInput.addEventListener('focus', this._boundHandlers.phoneFocus);
738
+ this.phoneInput.addEventListener('blur', this._boundHandlers.phoneBlur);
739
+ }
708
740
 
709
741
  // Navegación por teclado en dropdown
710
742
  this.setupKeyboardNavigation();
711
743
  }
712
744
 
745
+ /**
746
+ * Remueve los event listeners
747
+ */
748
+ removeEventListeners() {
749
+ // Remover listeners del botón
750
+ if (this.dropdownButton && this._boundHandlers?.dropdownClick) {
751
+ this.dropdownButton.removeEventListener('click', this._boundHandlers.dropdownClick);
752
+ }
753
+
754
+ // Remover listener del document
755
+ if (this._boundHandlers?.documentClick) {
756
+ document.removeEventListener('click', this._boundHandlers.documentClick);
757
+ }
758
+
759
+ // Remover listeners de items de países
760
+ if (this._countryItemHandlers) {
761
+ this._countryItemHandlers.forEach(({ item, handler }) => {
762
+ item.removeEventListener('click', handler);
763
+ });
764
+ this._countryItemHandlers = [];
765
+ }
766
+
767
+ // Remover listeners del input
768
+ if (this.phoneInput && this._boundHandlers) {
769
+ if (this._boundHandlers.phoneInput) {
770
+ this.phoneInput.removeEventListener('input', this._boundHandlers.phoneInput);
771
+ }
772
+ if (this._boundHandlers.phoneFocus) {
773
+ this.phoneInput.removeEventListener('focus', this._boundHandlers.phoneFocus);
774
+ }
775
+ if (this._boundHandlers.phoneBlur) {
776
+ this.phoneInput.removeEventListener('blur', this._boundHandlers.phoneBlur);
777
+ }
778
+ }
779
+
780
+ // Remover listeners de teclado
781
+ if (this._keyboardHandlers) {
782
+ if (this.dropdownButton && this._keyboardHandlers.buttonKeydown) {
783
+ this.dropdownButton.removeEventListener('keydown', this._keyboardHandlers.buttonKeydown);
784
+ }
785
+ if (this.dropdownMenu && this._keyboardHandlers.menuKeydown) {
786
+ this.dropdownMenu.removeEventListener('keydown', this._keyboardHandlers.menuKeydown);
787
+ }
788
+ }
789
+ }
790
+
713
791
  /**
714
792
  * Configura navegación por teclado
715
793
  */
716
794
  setupKeyboardNavigation() {
717
795
  if (!this.dropdownButton) return;
718
796
 
719
- this.dropdownButton.addEventListener('keydown', (e) => {
720
- if (e.key === 'Enter' || e.key === ' ') {
721
- e.preventDefault();
722
- this.toggleDropdown();
723
- } else if (e.key === 'Escape') {
724
- this.closeDropdown();
725
- }
726
- });
727
-
728
- // Navegación con flechas en el dropdown
729
- if (this.dropdownMenu) {
730
- this.dropdownMenu.addEventListener('keydown', (e) => {
797
+ // Guardar handlers para poder removerlos después
798
+ this._keyboardHandlers = {
799
+ buttonKeydown: (e) => {
800
+ if (e.key === 'Enter' || e.key === ' ') {
801
+ e.preventDefault();
802
+ this.toggleDropdown();
803
+ } else if (e.key === 'Escape') {
804
+ this.closeDropdown();
805
+ }
806
+ },
807
+ menuKeydown: (e) => {
731
808
  const items = Array.from(this.container.querySelectorAll('.phone-lib-country-item:not([style*="display: none"])'));
809
+ if (items.length === 0) return;
810
+
732
811
  const currentIndex = items.findIndex(item => item.classList.contains('selected'));
733
812
 
734
813
  if (e.key === 'ArrowDown') {
735
814
  e.preventDefault();
736
815
  const nextIndex = (currentIndex + 1) % items.length;
737
- items[nextIndex]?.click();
738
- items[nextIndex]?.scrollIntoView({ block: 'nearest' });
816
+ const nextItem = items[nextIndex];
817
+ if (nextItem && !nextItem.classList.contains('disabled')) {
818
+ nextItem.click();
819
+ nextItem.scrollIntoView({ block: 'nearest' });
820
+ }
739
821
  } else if (e.key === 'ArrowUp') {
740
822
  e.preventDefault();
741
823
  const prevIndex = currentIndex <= 0 ? items.length - 1 : currentIndex - 1;
742
- items[prevIndex]?.click();
743
- items[prevIndex]?.scrollIntoView({ block: 'nearest' });
824
+ const prevItem = items[prevIndex];
825
+ if (prevItem && !prevItem.classList.contains('disabled')) {
826
+ prevItem.click();
827
+ prevItem.scrollIntoView({ block: 'nearest' });
828
+ }
744
829
  } else if (e.key === 'Escape') {
745
830
  this.closeDropdown();
746
831
  }
747
- });
832
+ }
833
+ };
834
+
835
+ this.dropdownButton.addEventListener('keydown', this._keyboardHandlers.buttonKeydown);
836
+
837
+ // Navegación con flechas en el dropdown
838
+ if (this.dropdownMenu) {
839
+ this.dropdownMenu.addEventListener('keydown', this._keyboardHandlers.menuKeydown);
748
840
  }
749
841
  }
750
842
 
@@ -753,17 +845,34 @@ class PhoneLib {
753
845
  */
754
846
  handlePhoneInput(value) {
755
847
  const previousNumber = this.phoneNumber;
848
+ const previousCountry = this.selectedCountry;
756
849
  this.phoneNumber = value;
757
850
 
758
851
  // Detección automática de país si está habilitada
852
+ // IMPORTANTE: Hacer esto ANTES de formatear para que el formateo use el país correcto
759
853
  if (this.options.autoDetectCountry && value) {
760
854
  this.autoDetectCountry(value);
761
855
  }
762
856
 
857
+ // Actualizar el número formateado (usará el país correcto si se detectó)
763
858
  this.updatePhoneNumber();
764
859
 
765
860
  // Ejecutar callbacks y eventos (validar sin actualizar visuals para obtener estado actual)
766
861
  const isValid = this.phoneNumber ? this.validatePhone(false) : false;
862
+
863
+ // Emitir evento de cambio de país si cambió
864
+ if (previousCountry !== this.selectedCountry) {
865
+ const dialCode = this.getDialCode();
866
+ const countryData = this.countries.find(c => c.iso2 === this.selectedCountry);
867
+ this.executeCallback('onCountryChange', this.selectedCountry, dialCode, countryData?.name);
868
+ this.emitEvent('countryChange', {
869
+ country: this.selectedCountry,
870
+ dialCode,
871
+ countryName: countryData?.name,
872
+ previousCountry
873
+ });
874
+ }
875
+
767
876
  this.executeCallback('onPhoneChange', this.phoneNumber, isValid, this.selectedCountry);
768
877
  this.emitEvent('phoneChange', {
769
878
  phoneNumber: this.phoneNumber,
@@ -801,7 +910,12 @@ class PhoneLib {
801
910
  * Actualiza el número de teléfono y formatea
802
911
  */
803
912
  updatePhoneNumber() {
913
+ if (!this.phoneInput) return;
914
+
804
915
  if (!this.phoneNumber) {
916
+ if (this.phoneInput.value !== '') {
917
+ this.phoneInput.value = '';
918
+ }
805
919
  if (this.hintElement) {
806
920
  this.hintElement.textContent = '';
807
921
  }
@@ -809,13 +923,48 @@ class PhoneLib {
809
923
  }
810
924
 
811
925
  try {
812
- const formatter = new AsYouType(this.selectedCountry);
926
+ // Si el número empieza con +, intentar detectar el país para formatear correctamente
927
+ let countryForFormatting = this.selectedCountry;
928
+ if (this.phoneNumber && this.phoneNumber.startsWith('+')) {
929
+ try {
930
+ const parsed = parsePhoneNumber(this.phoneNumber);
931
+ if (parsed && parsed.country) {
932
+ // Usar el país detectado para formatear, si está disponible
933
+ const detectedCountry = parsed.country;
934
+ const countryAvailable = this.countries.find(c => c.iso2 === detectedCountry && !c.disabled);
935
+ if (countryAvailable) {
936
+ countryForFormatting = detectedCountry;
937
+ }
938
+ }
939
+ } catch (e) {
940
+ // Si no se puede detectar, usar el país seleccionado actual
941
+ }
942
+ }
943
+
944
+ const formatter = new AsYouType(countryForFormatting);
813
945
  const formatted = formatter.input(this.phoneNumber);
814
946
 
947
+ // Solo actualizar si el valor formateado es diferente
815
948
  if (formatted && formatted !== this.phoneInput.value) {
949
+ // Guardar la posición del cursor antes de actualizar
950
+ const cursorPosition = this.phoneInput.selectionStart;
951
+ const inputLength = this.phoneInput.value.length;
952
+ const wasAtEnd = cursorPosition === inputLength;
953
+
954
+ // Actualizar el valor
816
955
  this.phoneInput.value = formatted;
817
- // Actualizar phoneNumber con el valor formateado para mantener sincronización
818
- this.phoneNumber = formatted;
956
+
957
+ // Restaurar la posición del cursor de manera inteligente
958
+ if (wasAtEnd) {
959
+ // Si estaba al final, mantenerlo al final
960
+ this.phoneInput.setSelectionRange(formatted.length, formatted.length);
961
+ } else {
962
+ // Intentar mantener la posición relativa
963
+ // Calcular la nueva posición basada en la diferencia de longitud
964
+ const lengthDiff = formatted.length - inputLength;
965
+ const newPosition = Math.max(0, Math.min(cursorPosition + lengthDiff, formatted.length));
966
+ this.phoneInput.setSelectionRange(newPosition, newPosition);
967
+ }
819
968
  }
820
969
 
821
970
  // Actualizar hint
@@ -913,12 +1062,24 @@ class PhoneLib {
913
1062
  * Selecciona un país
914
1063
  */
915
1064
  selectCountry(iso2, dialCode, silent = false) {
1065
+ // Validar que el país existe
1066
+ if (!iso2 || !dialCode) {
1067
+ console.warn('PhoneLib: selectCountry requiere iso2 y dialCode válidos');
1068
+ return;
1069
+ }
1070
+
916
1071
  const previousCountry = this.selectedCountry;
917
1072
  this.selectedCountry = iso2;
918
1073
 
919
1074
  // Actualizar botón según el layout
920
1075
  const countryData = this.countries.find(c => c.iso2 === iso2);
921
1076
 
1077
+ // Si el país no se encuentra, usar valores por defecto
1078
+ if (!countryData) {
1079
+ console.warn(`PhoneLib: País ${iso2} no encontrado en la lista`);
1080
+ return;
1081
+ }
1082
+
922
1083
  if (this.options.layout === 'separated') {
923
1084
  // Layout separado: actualizar flag, nombre del país y código de marcación (si está visible)
924
1085
  const flagElement = this.dropdownButton.querySelector('.phone-lib-flag');
@@ -955,9 +1116,11 @@ class PhoneLib {
955
1116
  // Cerrar dropdown
956
1117
  this.closeDropdown();
957
1118
 
958
- // Actualizar placeholder y formato
959
- this.phoneInput.placeholder = this.getPlaceholder();
960
- this.updatePhoneNumber();
1119
+ // Actualizar placeholder y formato (solo si el input existe)
1120
+ if (this.phoneInput) {
1121
+ this.phoneInput.placeholder = this.getPlaceholder();
1122
+ this.updatePhoneNumber();
1123
+ }
961
1124
 
962
1125
  // Ejecutar callbacks y eventos solo si no es silencioso
963
1126
  if (!silent) {
@@ -1249,11 +1412,8 @@ class PhoneLib {
1249
1412
  * Destruye la instancia y limpia recursos
1250
1413
  */
1251
1414
  destroy() {
1252
- // Remover event listeners
1253
- if (this.phoneInput) {
1254
- const newInput = this.phoneInput.cloneNode(true);
1255
- this.phoneInput.parentNode.replaceChild(newInput, this.phoneInput);
1256
- }
1415
+ // Remover todos los event listeners
1416
+ this.removeEventListeners();
1257
1417
 
1258
1418
  // Limpiar contenedor
1259
1419
  if (this.container) {
@@ -1267,6 +1427,9 @@ class PhoneLib {
1267
1427
  this.hintElement = null;
1268
1428
  this.dialCodeInput = null;
1269
1429
  this.countriesList = null;
1430
+ this._boundHandlers = null;
1431
+ this._countryItemHandlers = null;
1432
+ this._keyboardHandlers = null;
1270
1433
  }
1271
1434
 
1272
1435
  /**