@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 +6 -6
- package/package.json +1 -1
- package/phone-lib.cdn.js +3 -3
- package/phone-lib.js +189 -61
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.
|
|
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.
|
|
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.
|
|
93
|
-
- **unpkg:** `https://unpkg.com/@jacksonavila/phone-lib@2.0.
|
|
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.
|
|
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.
|
|
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
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.
|
|
8
|
-
* <script src="https://cdn.jsdelivr.net/npm/@jacksonavila/phone-lib@2.0.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
662
|
-
|
|
663
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
689
|
-
|
|
690
|
-
|
|
709
|
+
if (this.phoneInput) {
|
|
710
|
+
this._boundHandlers.phoneInput = (e) => {
|
|
711
|
+
if (this.isDisabled || this.isReadonly) return;
|
|
691
712
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
this.validatePhone();
|
|
695
|
-
}
|
|
696
|
-
});
|
|
713
|
+
// Obtener el valor actual del input
|
|
714
|
+
const inputValue = e.target.value;
|
|
697
715
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
this.emitEvent('focus');
|
|
701
|
-
});
|
|
716
|
+
// Procesar el input
|
|
717
|
+
this.handlePhoneInput(inputValue);
|
|
702
718
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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]
|
|
738
|
-
|
|
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]
|
|
743
|
-
|
|
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
|
-
|
|
818
|
-
|
|
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
|
|
960
|
-
|
|
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
|
-
|
|
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
|
/**
|