@jacksonavila/phone-lib 2.0.5 → 2.0.6

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.6/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.6/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.6/`
93
+ - **unpkg:** `https://unpkg.com/@jacksonavila/phone-lib@2.0.6/`
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.6/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.6/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.6",
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.6/phone-lib.css">
8
+ * <script src="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.6/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.6';
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
 
@@ -801,7 +893,12 @@ class PhoneLib {
801
893
  * Actualiza el número de teléfono y formatea
802
894
  */
803
895
  updatePhoneNumber() {
896
+ if (!this.phoneInput) return;
897
+
804
898
  if (!this.phoneNumber) {
899
+ if (this.phoneInput.value !== '') {
900
+ this.phoneInput.value = '';
901
+ }
805
902
  if (this.hintElement) {
806
903
  this.hintElement.textContent = '';
807
904
  }
@@ -812,10 +909,27 @@ class PhoneLib {
812
909
  const formatter = new AsYouType(this.selectedCountry);
813
910
  const formatted = formatter.input(this.phoneNumber);
814
911
 
912
+ // Solo actualizar si el valor formateado es diferente
815
913
  if (formatted && formatted !== this.phoneInput.value) {
914
+ // Guardar la posición del cursor antes de actualizar
915
+ const cursorPosition = this.phoneInput.selectionStart;
916
+ const inputLength = this.phoneInput.value.length;
917
+ const wasAtEnd = cursorPosition === inputLength;
918
+
919
+ // Actualizar el valor
816
920
  this.phoneInput.value = formatted;
817
- // Actualizar phoneNumber con el valor formateado para mantener sincronización
818
- this.phoneNumber = formatted;
921
+
922
+ // Restaurar la posición del cursor de manera inteligente
923
+ if (wasAtEnd) {
924
+ // Si estaba al final, mantenerlo al final
925
+ this.phoneInput.setSelectionRange(formatted.length, formatted.length);
926
+ } else {
927
+ // Intentar mantener la posición relativa
928
+ // Calcular la nueva posición basada en la diferencia de longitud
929
+ const lengthDiff = formatted.length - inputLength;
930
+ const newPosition = Math.max(0, Math.min(cursorPosition + lengthDiff, formatted.length));
931
+ this.phoneInput.setSelectionRange(newPosition, newPosition);
932
+ }
819
933
  }
820
934
 
821
935
  // Actualizar hint
@@ -913,12 +1027,24 @@ class PhoneLib {
913
1027
  * Selecciona un país
914
1028
  */
915
1029
  selectCountry(iso2, dialCode, silent = false) {
1030
+ // Validar que el país existe
1031
+ if (!iso2 || !dialCode) {
1032
+ console.warn('PhoneLib: selectCountry requiere iso2 y dialCode válidos');
1033
+ return;
1034
+ }
1035
+
916
1036
  const previousCountry = this.selectedCountry;
917
1037
  this.selectedCountry = iso2;
918
1038
 
919
1039
  // Actualizar botón según el layout
920
1040
  const countryData = this.countries.find(c => c.iso2 === iso2);
921
1041
 
1042
+ // Si el país no se encuentra, usar valores por defecto
1043
+ if (!countryData) {
1044
+ console.warn(`PhoneLib: País ${iso2} no encontrado en la lista`);
1045
+ return;
1046
+ }
1047
+
922
1048
  if (this.options.layout === 'separated') {
923
1049
  // Layout separado: actualizar flag, nombre del país y código de marcación (si está visible)
924
1050
  const flagElement = this.dropdownButton.querySelector('.phone-lib-flag');
@@ -955,9 +1081,11 @@ class PhoneLib {
955
1081
  // Cerrar dropdown
956
1082
  this.closeDropdown();
957
1083
 
958
- // Actualizar placeholder y formato
959
- this.phoneInput.placeholder = this.getPlaceholder();
960
- this.updatePhoneNumber();
1084
+ // Actualizar placeholder y formato (solo si el input existe)
1085
+ if (this.phoneInput) {
1086
+ this.phoneInput.placeholder = this.getPlaceholder();
1087
+ this.updatePhoneNumber();
1088
+ }
961
1089
 
962
1090
  // Ejecutar callbacks y eventos solo si no es silencioso
963
1091
  if (!silent) {
@@ -1249,11 +1377,8 @@ class PhoneLib {
1249
1377
  * Destruye la instancia y limpia recursos
1250
1378
  */
1251
1379
  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
- }
1380
+ // Remover todos los event listeners
1381
+ this.removeEventListeners();
1257
1382
 
1258
1383
  // Limpiar contenedor
1259
1384
  if (this.container) {
@@ -1267,6 +1392,9 @@ class PhoneLib {
1267
1392
  this.hintElement = null;
1268
1393
  this.dialCodeInput = null;
1269
1394
  this.countriesList = null;
1395
+ this._boundHandlers = null;
1396
+ this._countryItemHandlers = null;
1397
+ this._keyboardHandlers = null;
1270
1398
  }
1271
1399
 
1272
1400
  /**