@dile/ui 3.7.3 → 3.8.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/components/input/__screenshots__/input-mask.spec.js/dile-input-mask-allows-selecting-and-replacing-characters-1.png +0 -0
- package/components/input/__screenshots__/input-mask.spec.js/dile-input-mask-does-not-show-error-while-typing-incomplete-input-1.png +0 -0
- package/components/input/__screenshots__/input-mask.spec.js/dile-input-mask-handles-mixed-case-transformation-1.png +0 -0
- package/components/input/__screenshots__/input-mask.spec.js/dile-input-mask-masks-input-with-letters-and-digits-1.png +0 -0
- package/components/input/__screenshots__/input-mask.spec.js/dile-input-mask-transforms-to-lowercase-when-pattern-uses-a-1.png +0 -0
- package/components/input/__screenshots__/input-mask.spec.js/dile-input-mask-transforms-to-uppercase-when-pattern-uses-A-1.png +0 -0
- package/components/input/index.js +3 -1
- package/components/input/input-mask.js +5 -0
- package/components/input/input-mask.spec.js +186 -0
- package/components/input/input-number-mask.spec.js +17 -1
- package/components/input/src/DileInputMask.js +173 -0
- package/components/input/src/DileInputNumberMask.js +76 -33
- package/components/input/src/MaskPattern.js +194 -0
- package/package.json +2 -2
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { DileInput } from './src/DileInput.js';
|
|
2
2
|
export { DileInputIcon } from './src/DileInputIcon.js';
|
|
3
3
|
export { DileInputInteger } from './src/DileInputInteger.js';
|
|
4
|
+
export { DileInputMask } from './src/DileInputMask.js';
|
|
4
5
|
export { DileInputMessage } from './src/DileInputMessage.js';
|
|
5
6
|
export { DileInputMoney } from './src/DileInputMoney.js';
|
|
6
7
|
export { DileInputNumber } from './src/DileInputNumber.js';
|
|
@@ -9,4 +10,5 @@ export { DileInputPercentage } from './src/DileInputPercentage.js';
|
|
|
9
10
|
export { DileInputSearch } from './src/DileInputSearch.js';
|
|
10
11
|
export { labelStyles } from './src/label-styles.js';
|
|
11
12
|
export { messageStyles } from './src/message-styles.js';
|
|
12
|
-
export { Mask } from './src/Mask.js';
|
|
13
|
+
export { Mask } from './src/Mask.js';
|
|
14
|
+
export { MaskPattern } from './src/MaskPattern.js';
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import './input-mask.js';
|
|
3
|
+
|
|
4
|
+
describe('dile-input-mask', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
async function renderInputMask(html) {
|
|
10
|
+
document.body.innerHTML = html;
|
|
11
|
+
const el = document.body.querySelector('dile-input-mask');
|
|
12
|
+
await el.updateComplete;
|
|
13
|
+
return el;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
it('renders a native text input', async () => {
|
|
17
|
+
const el = await renderInputMask('<dile-input-mask name="iban" mask="AA00 0000 0000 0000 0000"></dile-input-mask>');
|
|
18
|
+
expect(el.shadowRoot.querySelector('input')).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('masks input with letters and digits', async () => {
|
|
22
|
+
const el = await renderInputMask('<dile-input-mask name="iban" mask="AA00 0000 0000 0000 0000"></dile-input-mask>');
|
|
23
|
+
const input = el.shadowRoot.querySelector('input');
|
|
24
|
+
|
|
25
|
+
const chars = ['E', 'S', '9', '1', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'];
|
|
26
|
+
chars.forEach((char) => {
|
|
27
|
+
const keydownEvent = new KeyboardEvent('keydown', {
|
|
28
|
+
key: char,
|
|
29
|
+
bubbles: true,
|
|
30
|
+
cancelable: true,
|
|
31
|
+
});
|
|
32
|
+
input.dispatchEvent(keydownEvent);
|
|
33
|
+
input.value += char;
|
|
34
|
+
const inputEvent = new Event('input', { bubbles: true });
|
|
35
|
+
input.dispatchEvent(inputEvent);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await el.updateComplete;
|
|
39
|
+
|
|
40
|
+
expect(el.maskedValue).toBe('ES91 0000 0000 0000 0000');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('validates invalid format on blur', async () => {
|
|
44
|
+
const el = await renderInputMask('<dile-input-mask name="iban" mask="AA00 0000 0000 0000 0000"></dile-input-mask>');
|
|
45
|
+
const input = el.shadowRoot.querySelector('input');
|
|
46
|
+
|
|
47
|
+
// Try to start with a digit instead of a letter
|
|
48
|
+
input.value = '91ES0000000000000000';
|
|
49
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
50
|
+
await el.updateComplete;
|
|
51
|
+
input.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
52
|
+
await el.updateComplete;
|
|
53
|
+
|
|
54
|
+
expect(el.errored).toBe(true);
|
|
55
|
+
expect(el.message).toContain('expected a letter');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('supports multiple languages for error messages', async () => {
|
|
59
|
+
// English (default)
|
|
60
|
+
const elEn = await renderInputMask('<dile-input-mask name="iban" mask="AA00" language="en"></dile-input-mask>');
|
|
61
|
+
const inputEn = elEn.shadowRoot.querySelector('input');
|
|
62
|
+
inputEn.value = '9100';
|
|
63
|
+
inputEn.dispatchEvent(new Event('input', { bubbles: true }));
|
|
64
|
+
await elEn.updateComplete;
|
|
65
|
+
inputEn.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
66
|
+
await elEn.updateComplete;
|
|
67
|
+
|
|
68
|
+
expect(elEn.message).toContain('expected a letter');
|
|
69
|
+
|
|
70
|
+
// Spanish
|
|
71
|
+
const elEs = await renderInputMask('<dile-input-mask name="iban" mask="AA00" language="es"></dile-input-mask>');
|
|
72
|
+
const inputEs = elEs.shadowRoot.querySelector('input');
|
|
73
|
+
inputEs.value = '9100';
|
|
74
|
+
inputEs.dispatchEvent(new Event('input', { bubbles: true }));
|
|
75
|
+
await elEs.updateComplete;
|
|
76
|
+
inputEs.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
77
|
+
await elEs.updateComplete;
|
|
78
|
+
|
|
79
|
+
expect(elEs.message).toContain('se esperaba una letra');
|
|
80
|
+
|
|
81
|
+
// French
|
|
82
|
+
const elFr = await renderInputMask('<dile-input-mask name="iban" mask="AA00" language="fr"></dile-input-mask>');
|
|
83
|
+
const inputFr = elFr.shadowRoot.querySelector('input');
|
|
84
|
+
inputFr.value = '9100';
|
|
85
|
+
inputFr.dispatchEvent(new Event('input', { bubbles: true }));
|
|
86
|
+
await elFr.updateComplete;
|
|
87
|
+
inputFr.dispatchEvent(new Event('blur', { bubbles: true }));
|
|
88
|
+
await elFr.updateComplete;
|
|
89
|
+
|
|
90
|
+
expect(elFr.message).toContain('lettre attendue');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('allows selecting and replacing characters', async () => {
|
|
94
|
+
const el = await renderInputMask('<dile-input-mask name="iban" mask="AA00 0000"></dile-input-mask>');
|
|
95
|
+
const input = el.shadowRoot.querySelector('input');
|
|
96
|
+
|
|
97
|
+
// First, enter some initial value
|
|
98
|
+
const initialChars = ['E', 'S', '9', '1', '0', '0', '0', '0'];
|
|
99
|
+
initialChars.forEach((char) => {
|
|
100
|
+
input.value += char;
|
|
101
|
+
const inputEvent = new Event('input', { bubbles: true });
|
|
102
|
+
input.dispatchEvent(inputEvent);
|
|
103
|
+
});
|
|
104
|
+
await el.updateComplete;
|
|
105
|
+
|
|
106
|
+
expect(el.maskedValue).toBe('ES91 0000');
|
|
107
|
+
|
|
108
|
+
// Now simulate selecting characters at positions 3-5 (the "91") and replacing with "AB"
|
|
109
|
+
// In the masked value "ES91 0000", positions 3-5 contain "91"
|
|
110
|
+
input.value = 'ESAB 0000';
|
|
111
|
+
input.setSelectionRange(5, 5); // Cursor after the replacement
|
|
112
|
+
const inputEvent = new Event('input', { bubbles: true });
|
|
113
|
+
input.dispatchEvent(inputEvent);
|
|
114
|
+
await el.updateComplete;
|
|
115
|
+
|
|
116
|
+
// Should have replaced with AB and maintained format
|
|
117
|
+
expect(el.value).toContain('ESAB');
|
|
118
|
+
expect(el.maskedValue).toBe('ESAB 0000');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('does not show error while typing incomplete input', async () => {
|
|
122
|
+
const el = await renderInputMask('<dile-input-mask name="iban" mask="AA00 0000 0000 0000 0000"></dile-input-mask>');
|
|
123
|
+
const input = el.shadowRoot.querySelector('input');
|
|
124
|
+
|
|
125
|
+
// User starts typing correctly
|
|
126
|
+
input.value = 'ES91';
|
|
127
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
128
|
+
await el.updateComplete;
|
|
129
|
+
|
|
130
|
+
expect(el.errored).toBeFalsy();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('transforms to uppercase when pattern uses A', async () => {
|
|
134
|
+
const el = await renderInputMask('<dile-input-mask name="test" mask="AA00 AA"></dile-input-mask>');
|
|
135
|
+
const input = el.shadowRoot.querySelector('input');
|
|
136
|
+
|
|
137
|
+
// User types lowercase, should transform to uppercase
|
|
138
|
+
const chars = ['e', 's', '9', '1', 'a', 'b'];
|
|
139
|
+
chars.forEach((char) => {
|
|
140
|
+
input.value += char;
|
|
141
|
+
const inputEvent = new Event('input', { bubbles: true });
|
|
142
|
+
input.dispatchEvent(inputEvent);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await el.updateComplete;
|
|
146
|
+
|
|
147
|
+
expect(el.maskedValue).toBe('ES91 AB');
|
|
148
|
+
expect(el.value).toBe('ES91AB');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('transforms to lowercase when pattern uses a', async () => {
|
|
152
|
+
const el = await renderInputMask('<dile-input-mask name="test" mask="aa00 aa"></dile-input-mask>');
|
|
153
|
+
const input = el.shadowRoot.querySelector('input');
|
|
154
|
+
|
|
155
|
+
// User types uppercase, should transform to lowercase
|
|
156
|
+
const chars = ['E', 'S', '9', '1', 'A', 'B'];
|
|
157
|
+
chars.forEach((char) => {
|
|
158
|
+
input.value += char;
|
|
159
|
+
const inputEvent = new Event('input', { bubbles: true });
|
|
160
|
+
input.dispatchEvent(inputEvent);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await el.updateComplete;
|
|
164
|
+
|
|
165
|
+
expect(el.maskedValue).toBe('es91 ab');
|
|
166
|
+
expect(el.value).toBe('es91ab');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('handles mixed case transformation', async () => {
|
|
170
|
+
const el = await renderInputMask('<dile-input-mask name="test" mask="AA-aa-00"></dile-input-mask>');
|
|
171
|
+
const input = el.shadowRoot.querySelector('input');
|
|
172
|
+
|
|
173
|
+
// User types: first two should be uppercase, next two lowercase, then digits
|
|
174
|
+
const chars = ['a', 'b', 'c', 'd', '1', '2'];
|
|
175
|
+
chars.forEach((char) => {
|
|
176
|
+
input.value += char;
|
|
177
|
+
const inputEvent = new Event('input', { bubbles: true });
|
|
178
|
+
input.dispatchEvent(inputEvent);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await el.updateComplete;
|
|
182
|
+
|
|
183
|
+
expect(el.maskedValue).toBe('AB-cd-12');
|
|
184
|
+
expect(el.value).toBe('ABcd12');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -22,9 +22,25 @@ describe('dile-input-number-mask', () => {
|
|
|
22
22
|
const el = await renderInputNumberMask('<dile-input-number-mask name="code" mask="00-00"></dile-input-number-mask>');
|
|
23
23
|
const input = el.shadowRoot.querySelector('input');
|
|
24
24
|
|
|
25
|
+
// Simular el usuario escribiendo números: keydown + input event
|
|
25
26
|
['1', '2', '3'].forEach((key) => {
|
|
26
|
-
|
|
27
|
+
// El navegador primero dispara keydown
|
|
28
|
+
const keydownEvent = new KeyboardEvent('keydown', {
|
|
29
|
+
key,
|
|
30
|
+
bubbles: true,
|
|
31
|
+
cancelable: true
|
|
32
|
+
});
|
|
33
|
+
input.dispatchEvent(keydownEvent);
|
|
34
|
+
|
|
35
|
+
// Simular que el navegador actualiza el value del input
|
|
36
|
+
input.value += key;
|
|
37
|
+
|
|
38
|
+
// Luego dispara input event
|
|
39
|
+
const inputEvent = new Event('input', { bubbles: true });
|
|
40
|
+
input.dispatchEvent(inputEvent);
|
|
27
41
|
});
|
|
42
|
+
|
|
43
|
+
// Esperar a que Lit procese los cambios
|
|
28
44
|
await el.updateComplete;
|
|
29
45
|
|
|
30
46
|
expect(el.maskedValue).toBe('12-3');
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { DileInput } from '../../input/index.js';
|
|
2
|
+
import { MaskPattern } from './MaskPattern.js';
|
|
3
|
+
|
|
4
|
+
export class DileInputMask extends DileInput {
|
|
5
|
+
|
|
6
|
+
static get properties() {
|
|
7
|
+
return {
|
|
8
|
+
...super.properties,
|
|
9
|
+
mask: { type: String },
|
|
10
|
+
maskedValue: { type: String },
|
|
11
|
+
language: { type: String },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.mask = '';
|
|
18
|
+
this.maskedValue = '';
|
|
19
|
+
this.content = '';
|
|
20
|
+
this.language = 'en';
|
|
21
|
+
this._keyDownHandler = this.handleKeyDown.bind(this);
|
|
22
|
+
this._blurHandler = this.handleBlur.bind(this);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
firstUpdated() {
|
|
26
|
+
super.firstUpdated();
|
|
27
|
+
this.createMaskController(this.mask);
|
|
28
|
+
if (this.el) {
|
|
29
|
+
this.el.addEventListener('keydown', this._keyDownHandler);
|
|
30
|
+
this.el.addEventListener('blur', this._blurHandler);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
createMaskController(mask) {
|
|
35
|
+
this.maskController = new MaskPattern(mask, this.language);
|
|
36
|
+
this.maxChars = this.maskController.getTotalCharactersExpected();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
updated(changedProperties) {
|
|
40
|
+
super.updated(changedProperties);
|
|
41
|
+
if (changedProperties.has('mask')) {
|
|
42
|
+
this.maskController.setPattern(this.mask);
|
|
43
|
+
this.maxChars = this.maskController.getTotalCharactersExpected();
|
|
44
|
+
this.updateMaskedValue();
|
|
45
|
+
}
|
|
46
|
+
if (changedProperties.has('language')) {
|
|
47
|
+
this.maskController.setLanguage(this.language);
|
|
48
|
+
}
|
|
49
|
+
if (changedProperties.has('value')) {
|
|
50
|
+
const transformedValue = this.maskController.transformValue(this.value.slice(0, this.maxChars));
|
|
51
|
+
this.content = transformedValue;
|
|
52
|
+
this.updateMaskedValue();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
disconnectedCallback() {
|
|
57
|
+
if (this.el) {
|
|
58
|
+
this.el.removeEventListener('keydown', this._keyDownHandler);
|
|
59
|
+
this.el.removeEventListener('blur', this._blurHandler);
|
|
60
|
+
}
|
|
61
|
+
super.disconnectedCallback();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
updateMaskedValue() {
|
|
65
|
+
this.maskedValue = this.maskController.maskIt(this.content);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
computeValue(value) {
|
|
69
|
+
return this.maskedValue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_input(e) {
|
|
73
|
+
const inputElement = e.target;
|
|
74
|
+
|
|
75
|
+
const inputValue = inputElement.value;
|
|
76
|
+
const oldCursorPos = inputElement.selectionStart;
|
|
77
|
+
|
|
78
|
+
const cleanValue = this.maskController.getCleanValue(inputValue).slice(0, this.maxChars);
|
|
79
|
+
const transformedValue = this.maskController.transformValue(cleanValue);
|
|
80
|
+
|
|
81
|
+
this.content = transformedValue;
|
|
82
|
+
this.updateMaskedValue();
|
|
83
|
+
this.value = transformedValue;
|
|
84
|
+
|
|
85
|
+
inputElement.value = this.maskedValue;
|
|
86
|
+
|
|
87
|
+
this.restoreCursorPosition(inputElement, inputValue, oldCursorPos);
|
|
88
|
+
|
|
89
|
+
if (this.hideErrorOnInput && this.errored) {
|
|
90
|
+
this.clearError();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Restores cursor position after formatting the value.
|
|
96
|
+
* Positions it based on the count of alphanumeric characters entered.
|
|
97
|
+
*/
|
|
98
|
+
restoreCursorPosition(inputElement, oldValue, oldCursorPos) {
|
|
99
|
+
const beforeCursor = oldValue.substring(0, oldCursorPos);
|
|
100
|
+
const cleanCharsBeforeCursor = beforeCursor.replace(/[^a-zA-Z0-9]/g, '').length;
|
|
101
|
+
|
|
102
|
+
let charsFound = 0;
|
|
103
|
+
let newCursorPos = 0;
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < this.maskedValue.length; i++) {
|
|
106
|
+
if (/[a-zA-Z0-9]/.test(this.maskedValue[i])) {
|
|
107
|
+
if (charsFound === cleanCharsBeforeCursor) {
|
|
108
|
+
newCursorPos = i;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
charsFound++;
|
|
112
|
+
}
|
|
113
|
+
newCursorPos = i + 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
inputElement.setSelectionRange(newCursorPos, newCursorPos);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
handleKeyDown(e) {
|
|
120
|
+
const inputElement = this.el;
|
|
121
|
+
if (e.target !== inputElement) return;
|
|
122
|
+
|
|
123
|
+
const key = e.key;
|
|
124
|
+
|
|
125
|
+
if (this.isAllowedCharacter(key, inputElement)) return;
|
|
126
|
+
|
|
127
|
+
const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Tab', 'Enter'];
|
|
128
|
+
if (allowedKeys.includes(key)) return;
|
|
129
|
+
|
|
130
|
+
if (e.ctrlKey || e.metaKey) return;
|
|
131
|
+
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
handleBlur(e) {
|
|
136
|
+
if (!this.maskController) return;
|
|
137
|
+
|
|
138
|
+
const validation = this.maskController.validate(this.content);
|
|
139
|
+
|
|
140
|
+
if (!validation.valid) {
|
|
141
|
+
this.errored = true;
|
|
142
|
+
this.message = validation.error || this.getInvalidFormatMessage();
|
|
143
|
+
} else if (this.maskController.isComplete(this.content)) {
|
|
144
|
+
this.clearError();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getInvalidFormatMessage() {
|
|
149
|
+
const messages = {
|
|
150
|
+
en: `Invalid format. Expected: ${this.maskController.getFormatDescription()}`,
|
|
151
|
+
es: `Formato inválido. Se espera: ${this.maskController.getFormatDescription()}`,
|
|
152
|
+
fr: `Format invalide. Attendu: ${this.maskController.getFormatDescription()}`,
|
|
153
|
+
de: `Ungültiges Format. Erwartet: ${this.maskController.getFormatDescription()}`,
|
|
154
|
+
cat: `Format invàlid. S'espera: ${this.maskController.getFormatDescription()}`,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
return messages[this.language] || messages['en'];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
isAllowedCharacter(char, inputElement) {
|
|
161
|
+
if (/[a-zA-Z0-9]/.test(char)) {
|
|
162
|
+
// If there's a selection, user is replacing text, so allow it
|
|
163
|
+
if (inputElement.selectionStart !== inputElement.selectionEnd) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
// Otherwise only allow if we haven't reached max chars
|
|
167
|
+
if (this.content.length < this.maxChars) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { LitElement, html } from 'lit';
|
|
2
1
|
import { DileInput } from '../../input/index.js';
|
|
3
2
|
import { Mask } from './Mask.js';
|
|
4
3
|
|
|
@@ -6,6 +5,7 @@ export class DileInputNumberMask extends DileInput {
|
|
|
6
5
|
|
|
7
6
|
static get properties() {
|
|
8
7
|
return {
|
|
8
|
+
...super.properties,
|
|
9
9
|
mask: { type: String },
|
|
10
10
|
maskedValue: { type: String },
|
|
11
11
|
};
|
|
@@ -14,12 +14,17 @@ export class DileInputNumberMask extends DileInput {
|
|
|
14
14
|
constructor() {
|
|
15
15
|
super();
|
|
16
16
|
this.mask = '';
|
|
17
|
+
this.maskedValue = '';
|
|
18
|
+
this.content = '';
|
|
19
|
+
this._keyDownHandler = this.handleKeyDown.bind(this);
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
firstUpdated() {
|
|
20
|
-
|
|
21
|
-
this.maskedValue = '';
|
|
23
|
+
super.firstUpdated();
|
|
22
24
|
this.createMaskController(this.mask);
|
|
25
|
+
if (this.el) {
|
|
26
|
+
this.el.addEventListener('keydown', this._keyDownHandler);
|
|
27
|
+
}
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
createMaskController(mask) {
|
|
@@ -31,50 +36,88 @@ export class DileInputNumberMask extends DileInput {
|
|
|
31
36
|
super.updated(changedProperties);
|
|
32
37
|
if (changedProperties.has('mask')) {
|
|
33
38
|
this.maskController.setPattern(this.mask);
|
|
34
|
-
this.
|
|
39
|
+
this.updateMaskedValue();
|
|
35
40
|
}
|
|
36
41
|
if (changedProperties.has('value')) {
|
|
37
42
|
this.content = this.value.slice(0, this.maxChars);
|
|
38
|
-
this.
|
|
43
|
+
this.updateMaskedValue();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
disconnectedCallback() {
|
|
48
|
+
if (this.el) {
|
|
49
|
+
this.el.removeEventListener('keydown', this._keyDownHandler);
|
|
39
50
|
}
|
|
51
|
+
super.disconnectedCallback();
|
|
40
52
|
}
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
updateMaskedValue() {
|
|
43
55
|
this.maskedValue = this.maskController.maskIt(this.content);
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
return
|
|
48
|
-
<div>
|
|
49
|
-
${this.label
|
|
50
|
-
? html`<label for="textField">${this.label}</label>`
|
|
51
|
-
: ''
|
|
52
|
-
}
|
|
53
|
-
<input
|
|
54
|
-
type="text"
|
|
55
|
-
id="textField"
|
|
56
|
-
name="${this.name}"
|
|
57
|
-
placeholder="${this.placeholder}"
|
|
58
|
-
?disabled="${this.disabled}"
|
|
59
|
-
@keypress="${this._lookForEnter}"
|
|
60
|
-
@keydown="${this.doKeyDown}"
|
|
61
|
-
.value="${this.maskedValue}"
|
|
62
|
-
class="${ this.errored ? 'errored' : '' }">
|
|
63
|
-
</div>
|
|
64
|
-
`;
|
|
58
|
+
computeValue(value) {
|
|
59
|
+
return this.maskedValue;
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
|
|
68
|
-
e.
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
_input(e) {
|
|
63
|
+
const inputElement = e.target;
|
|
64
|
+
const cursorPos = inputElement.selectionStart;
|
|
65
|
+
|
|
66
|
+
const inputValue = inputElement.value;
|
|
67
|
+
const numericOnly = inputValue.replace(/\D/g, '').slice(0, this.maxChars);
|
|
68
|
+
|
|
69
|
+
this.content = numericOnly;
|
|
70
|
+
this.updateMaskedValue();
|
|
71
|
+
this.value = numericOnly;
|
|
72
|
+
|
|
73
|
+
inputElement.value = this.maskedValue;
|
|
74
|
+
|
|
75
|
+
this.restoreCursorPosition(inputElement, inputValue, cursorPos);
|
|
76
|
+
|
|
77
|
+
if (this.hideErrorOnInput && this.errored) {
|
|
78
|
+
this.clearError();
|
|
71
79
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Restores cursor position after formatting the value.
|
|
84
|
+
* Counts how many numeric digits were before the cursor and positions it at the same count in the masked value.
|
|
85
|
+
*/
|
|
86
|
+
restoreCursorPosition(inputElement, oldValue, oldCursorPos) {
|
|
87
|
+
const beforeCursor = oldValue.substring(0, oldCursorPos);
|
|
88
|
+
const numericBeforeCursor = beforeCursor.replace(/\D/g, '').length;
|
|
89
|
+
|
|
90
|
+
let numbersFound = 0;
|
|
91
|
+
let newCursorPos = 0;
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < this.maskedValue.length; i++) {
|
|
94
|
+
if (this.isNumeric(this.maskedValue[i])) {
|
|
95
|
+
if (numbersFound === numericBeforeCursor) {
|
|
96
|
+
newCursorPos = i;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
numbersFound++;
|
|
75
100
|
}
|
|
101
|
+
newCursorPos = i + 1;
|
|
76
102
|
}
|
|
77
|
-
|
|
103
|
+
|
|
104
|
+
inputElement.setSelectionRange(newCursorPos, newCursorPos);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
handleKeyDown(e) {
|
|
108
|
+
const inputElement = this.el;
|
|
109
|
+
if (e.target !== inputElement) return;
|
|
110
|
+
|
|
111
|
+
const key = e.key;
|
|
112
|
+
|
|
113
|
+
if (this.isNumeric(key)) return;
|
|
114
|
+
|
|
115
|
+
const allowedKeys = ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Tab', 'Enter'];
|
|
116
|
+
if (allowedKeys.includes(key)) return;
|
|
117
|
+
|
|
118
|
+
if (e.ctrlKey || e.metaKey) return;
|
|
119
|
+
|
|
120
|
+
e.preventDefault();
|
|
78
121
|
}
|
|
79
122
|
|
|
80
123
|
isNumeric(char) {
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic mask pattern that supports letters (A/a), numbers (0), and literal characters.
|
|
3
|
+
* Pattern definition:
|
|
4
|
+
* 'A' = any letter (a-z, A-Z) - will be transformed to UPPERCASE
|
|
5
|
+
* 'a' = any letter (a-z, A-Z) - will be transformed to lowercase
|
|
6
|
+
* '0' = any digit (0-9)
|
|
7
|
+
* other chars = fixed literal characters in the mask
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* 'aa00 0000 0000 0000 0000' = IBAN format (2 lowercase letters, check digit, then numbers with spaces)
|
|
11
|
+
* '(000) 000-0000' = US phone format
|
|
12
|
+
* 'AA-00-AA' = Format with uppercase letters and digits
|
|
13
|
+
*/
|
|
14
|
+
export class MaskPattern {
|
|
15
|
+
constructor(pattern, language = 'en') {
|
|
16
|
+
this.pattern = pattern;
|
|
17
|
+
this.language = language;
|
|
18
|
+
this.parsePattern();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
parsePattern() {
|
|
22
|
+
this.patternMap = [];
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < this.pattern.length; i++) {
|
|
25
|
+
const char = this.pattern[i];
|
|
26
|
+
if (char === 'A') {
|
|
27
|
+
this.patternMap.push({ type: 'letterUpper', index: i });
|
|
28
|
+
} else if (char === 'a') {
|
|
29
|
+
this.patternMap.push({ type: 'letterLower', index: i });
|
|
30
|
+
} else if (char === '0') {
|
|
31
|
+
this.patternMap.push({ type: 'digit', index: i });
|
|
32
|
+
} else {
|
|
33
|
+
this.patternMap.push({ type: 'literal', char, index: i });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setPattern(newPattern) {
|
|
39
|
+
this.pattern = newPattern;
|
|
40
|
+
this.parsePattern();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setLanguage(language) {
|
|
44
|
+
this.language = language;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getTotalCharactersExpected() {
|
|
48
|
+
return this.patternMap.filter(p => p.type !== 'literal').length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
maskIt(value) {
|
|
52
|
+
let valueIndex = 0;
|
|
53
|
+
let masked = '';
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < this.patternMap.length; i++) {
|
|
56
|
+
const patternInfo = this.patternMap[i];
|
|
57
|
+
|
|
58
|
+
if (patternInfo.type === 'literal') {
|
|
59
|
+
masked += patternInfo.char;
|
|
60
|
+
} else {
|
|
61
|
+
if (valueIndex < value.length) {
|
|
62
|
+
masked += value[valueIndex];
|
|
63
|
+
valueIndex++;
|
|
64
|
+
} else {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return masked;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validates if the given value matches the pattern requirements
|
|
75
|
+
* Only validates positions that have been filled
|
|
76
|
+
* Note: Validation only checks if a character is a letter or digit,
|
|
77
|
+
* actual case transformation is done by transformValue()
|
|
78
|
+
*/
|
|
79
|
+
validate(value) {
|
|
80
|
+
let valueIndex = 0;
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < this.patternMap.length && valueIndex < value.length; i++) {
|
|
83
|
+
const patternInfo = this.patternMap[i];
|
|
84
|
+
|
|
85
|
+
if (patternInfo.type === 'letterUpper' || patternInfo.type === 'letterLower') {
|
|
86
|
+
const char = value[valueIndex];
|
|
87
|
+
if (!/[a-zA-Z]/.test(char)) {
|
|
88
|
+
return {
|
|
89
|
+
valid: false,
|
|
90
|
+
error: this.getErrorMessage('invalidLetter', valueIndex + 1, char),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
valueIndex++;
|
|
94
|
+
} else if (patternInfo.type === 'digit') {
|
|
95
|
+
const char = value[valueIndex];
|
|
96
|
+
if (!/[0-9]/.test(char)) {
|
|
97
|
+
return {
|
|
98
|
+
valid: false,
|
|
99
|
+
error: this.getErrorMessage('invalidDigit', valueIndex + 1, char),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
valueIndex++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { valid: true, error: null };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
isComplete(value) {
|
|
110
|
+
return value.length === this.getTotalCharactersExpected();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getFormatDescription() {
|
|
114
|
+
const lettersUpper = this.patternMap.filter(p => p.type === 'letterUpper').length;
|
|
115
|
+
const lettersLower = this.patternMap.filter(p => p.type === 'letterLower').length;
|
|
116
|
+
const digits = this.patternMap.filter(p => p.type === 'digit').length;
|
|
117
|
+
|
|
118
|
+
const parts = [];
|
|
119
|
+
if (lettersUpper > 0) parts.push(`${lettersUpper} uppercase letter${lettersUpper !== 1 ? 's' : ''}`);
|
|
120
|
+
if (lettersLower > 0) parts.push(`${lettersLower} lowercase letter${lettersLower !== 1 ? 's' : ''}`);
|
|
121
|
+
if (digits > 0) parts.push(`${digits} digit${digits !== 1 ? 's' : ''}`);
|
|
122
|
+
|
|
123
|
+
return parts.join(' + ');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getCleanValue(maskedValue) {
|
|
127
|
+
return maskedValue.replace(/[^a-zA-Z0-9]/g, '');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Transforms a clean value according to the pattern's case requirements
|
|
132
|
+
* 'A' positions will be uppercase, 'a' positions will be lowercase
|
|
133
|
+
* Returns only the transformed value (letters and digits), without literals
|
|
134
|
+
* @param {string} cleanValue - The clean value (letters and digits only)
|
|
135
|
+
* @returns {string} The transformed value with case applied (no literals)
|
|
136
|
+
*/
|
|
137
|
+
transformValue(cleanValue) {
|
|
138
|
+
let result = '';
|
|
139
|
+
let valueIndex = 0;
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < this.patternMap.length; i++) {
|
|
142
|
+
const patternInfo = this.patternMap[i];
|
|
143
|
+
|
|
144
|
+
if (patternInfo.type === 'letterUpper') {
|
|
145
|
+
if (valueIndex < cleanValue.length) {
|
|
146
|
+
result += cleanValue[valueIndex].toUpperCase();
|
|
147
|
+
valueIndex++;
|
|
148
|
+
}
|
|
149
|
+
} else if (patternInfo.type === 'letterLower') {
|
|
150
|
+
if (valueIndex < cleanValue.length) {
|
|
151
|
+
result += cleanValue[valueIndex].toLowerCase();
|
|
152
|
+
valueIndex++;
|
|
153
|
+
}
|
|
154
|
+
} else if (patternInfo.type === 'digit') {
|
|
155
|
+
if (valueIndex < cleanValue.length) {
|
|
156
|
+
result += cleanValue[valueIndex];
|
|
157
|
+
valueIndex++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Note: We skip literals here, they will be added by maskIt()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getErrorMessage(type, position, char) {
|
|
167
|
+
const messages = {
|
|
168
|
+
en: {
|
|
169
|
+
invalidLetter: `Position ${position}: expected a letter but got "${char}"`,
|
|
170
|
+
invalidDigit: `Position ${position}: expected a digit but got "${char}"`,
|
|
171
|
+
},
|
|
172
|
+
es: {
|
|
173
|
+
invalidLetter: `Posición ${position}: se esperaba una letra pero recibió "${char}"`,
|
|
174
|
+
invalidDigit: `Posición ${position}: se esperaba un dígito pero recibió "${char}"`,
|
|
175
|
+
},
|
|
176
|
+
fr: {
|
|
177
|
+
invalidLetter: `Position ${position} : lettre attendue mais reçu "${char}"`,
|
|
178
|
+
invalidDigit: `Position ${position} : chiffre attendu mais reçu "${char}"`,
|
|
179
|
+
},
|
|
180
|
+
de: {
|
|
181
|
+
invalidLetter: `Position ${position}: Buchstabe erwartet, aber "${char}" erhalten`,
|
|
182
|
+
invalidDigit: `Position ${position}: Ziffer erwartet, aber "${char}" erhalten`,
|
|
183
|
+
},
|
|
184
|
+
cat: {
|
|
185
|
+
invalidLetter: `Posició ${position}: s'esperava una lletra però va rebre "${char}"`,
|
|
186
|
+
invalidDigit: `Posició ${position}: s'esperava un dígit però va rebre "${char}"`,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const lang = messages[this.language] || messages['en'];
|
|
191
|
+
return lang[type] || '';
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/ui",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"description": "UI Core components from dile-components.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,5 +28,5 @@
|
|
|
28
28
|
"publishConfig": {
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "dd0e035ddb5e6e051ce36dbd2a365fec53371cef"
|
|
32
32
|
}
|