fn-input 0.0.1
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 +127 -0
- package/fesm2022/fn-input.mjs +1106 -0
- package/fesm2022/fn-input.mjs.map +1 -0
- package/package.json +25 -0
- package/types/fn-input.d.ts +290 -0
- package/types/fn-input.d.ts.map +1 -0
|
@@ -0,0 +1,1106 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { EventEmitter, inject, signal, ViewChildren, ViewChild, Output, Input, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import * as i2 from '@angular/common';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
import { HttpClient } from '@angular/common/http';
|
|
6
|
+
import * as i1 from '@angular/forms';
|
|
7
|
+
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
8
|
+
import * as i3 from '@ngx-translate/core';
|
|
9
|
+
import { TranslateService, TranslateModule } from '@ngx-translate/core';
|
|
10
|
+
import { Subscription, merge } from 'rxjs';
|
|
11
|
+
import { startWith } from 'rxjs/operators';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CURRENCY_META = [
|
|
14
|
+
{ code: 'USD', locale: 'en-US', symbol: '$', digit: '1.2-2' },
|
|
15
|
+
{ code: 'MYR', locale: 'en-MY', symbol: 'RM', digit: '1.2-2' },
|
|
16
|
+
{ code: 'EUR', locale: 'en-US', symbol: '€', digit: '1.2-2' },
|
|
17
|
+
{ code: 'GBP', locale: 'en-US', symbol: '£', digit: '1.2-2' },
|
|
18
|
+
{ code: 'JPY', locale: 'ja-JP', symbol: '¥', digit: '1.0-0' },
|
|
19
|
+
{ code: 'CNY', locale: 'zh-CN', symbol: '¥', digit: '1.2-2' },
|
|
20
|
+
{ code: 'INR', locale: 'en-IN', symbol: '₹', digit: '1.2-2' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
class FNInput {
|
|
24
|
+
cdr;
|
|
25
|
+
field = {};
|
|
26
|
+
helperHandle;
|
|
27
|
+
toastService;
|
|
28
|
+
currencyMeta = DEFAULT_CURRENCY_META;
|
|
29
|
+
defaultLocale = 'en-US';
|
|
30
|
+
form;
|
|
31
|
+
valueChange = new EventEmitter();
|
|
32
|
+
fieldBlur = new EventEmitter();
|
|
33
|
+
translate = inject(TranslateService);
|
|
34
|
+
hasFocus = signal(false, ...(ngDevMode ? [{ debugName: "hasFocus" }] : /* istanbul ignore next */ []));
|
|
35
|
+
textareaElement;
|
|
36
|
+
iconContainers;
|
|
37
|
+
http = inject(HttpClient);
|
|
38
|
+
subs = new Subscription();
|
|
39
|
+
sizeMap = {
|
|
40
|
+
extrasmall: 10,
|
|
41
|
+
small: 16,
|
|
42
|
+
medium: 20,
|
|
43
|
+
large: 24,
|
|
44
|
+
'x-large': 32,
|
|
45
|
+
xxlarge: 48,
|
|
46
|
+
};
|
|
47
|
+
getIconSizeName(size) {
|
|
48
|
+
switch (size) {
|
|
49
|
+
case 16:
|
|
50
|
+
return 'small';
|
|
51
|
+
case 20:
|
|
52
|
+
return 'medium';
|
|
53
|
+
case 24:
|
|
54
|
+
return 'large';
|
|
55
|
+
case 32:
|
|
56
|
+
return 'x-large';
|
|
57
|
+
default:
|
|
58
|
+
return 'medium';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
get isAlphanumeric() {
|
|
62
|
+
return this.field.isAlphanumeric !== false;
|
|
63
|
+
}
|
|
64
|
+
get isEmailField() {
|
|
65
|
+
return this.field.type === 'email' || this.field.name?.toLowerCase().includes('email');
|
|
66
|
+
}
|
|
67
|
+
get alphanumericPattern() {
|
|
68
|
+
if (this.field.isAddressLine) {
|
|
69
|
+
return /[^A-Za-z0-9 \-_&(),/]/g;
|
|
70
|
+
}
|
|
71
|
+
return /[^A-Za-z0-9 \-_&()]/g;
|
|
72
|
+
}
|
|
73
|
+
isDisabled = false;
|
|
74
|
+
control;
|
|
75
|
+
isVisible = signal(true, ...(ngDevMode ? [{ debugName: "isVisible" }] : /* istanbul ignore next */ [])); // Track visibility state
|
|
76
|
+
helperText = '';
|
|
77
|
+
// Use a counter-based approach for unique IDs (safer than Math.random())
|
|
78
|
+
static idCounter = 0;
|
|
79
|
+
uniqueId = `fn-input-${++FNInput.idCounter}`;
|
|
80
|
+
constructor(cdr) {
|
|
81
|
+
this.cdr = cdr;
|
|
82
|
+
}
|
|
83
|
+
ngOnInit() {
|
|
84
|
+
if (this.field.hidden)
|
|
85
|
+
return;
|
|
86
|
+
this.helperText = this.field.helperText ?? '';
|
|
87
|
+
this.initFormControl();
|
|
88
|
+
if (this.field.value)
|
|
89
|
+
this.control.setValue(this.field.value);
|
|
90
|
+
this.setupVisibilityCondition();
|
|
91
|
+
this.setupFieldMessageListener();
|
|
92
|
+
}
|
|
93
|
+
ngOnDestroy() {
|
|
94
|
+
this.subs.unsubscribe();
|
|
95
|
+
}
|
|
96
|
+
ngOnChanges(changes) {
|
|
97
|
+
if (changes['field'] || changes['form']) {
|
|
98
|
+
this.updateAllIcons();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
setupFieldMessageListener() {
|
|
102
|
+
if (!this.control)
|
|
103
|
+
return;
|
|
104
|
+
const val$ = this.control.valueChanges.pipe(startWith(this.control.value));
|
|
105
|
+
const status$ = this.control.statusChanges.pipe(startWith(this.control.status));
|
|
106
|
+
this.subs.add(merge(val$, status$).subscribe(() => {
|
|
107
|
+
try {
|
|
108
|
+
this.cdr.detectChanges();
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
console.warn('CDR detectChanges failed during field message update:', e);
|
|
112
|
+
this.cdr.markForCheck();
|
|
113
|
+
}
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
ngAfterViewInit() {
|
|
117
|
+
// Access the textarea element by ViewChild or native element reference
|
|
118
|
+
this.autoResizeInitial();
|
|
119
|
+
if (this.field.type === 'textarea' && this.control?.value) {
|
|
120
|
+
setTimeout(() => this.autoResizeInitial(), 30);
|
|
121
|
+
}
|
|
122
|
+
// Apply initial formatting after view is initialized
|
|
123
|
+
if (this.field.type === 'number' && this.field.isCurrency && this.control?.value) {
|
|
124
|
+
this.formatInitialCurrencyValue();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
enforceLowercase(event) {
|
|
128
|
+
const input = event.target;
|
|
129
|
+
const start = input.selectionStart;
|
|
130
|
+
const end = input.selectionEnd;
|
|
131
|
+
input.value = input.value.toLowerCase();
|
|
132
|
+
if (this.control) {
|
|
133
|
+
this.control.setValue(input.value, { emitEvent: false });
|
|
134
|
+
}
|
|
135
|
+
this.valueChange.emit({ name: this.field.name, value: input.value });
|
|
136
|
+
// restore cursor position so typing is smooth
|
|
137
|
+
input.setSelectionRange(start, end);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Handle email input - allows only valid email characters and enforces lowercase.
|
|
141
|
+
* Allowed: a-z, 0-9, . _ + - @
|
|
142
|
+
* Rules:
|
|
143
|
+
* - Only one @ allowed
|
|
144
|
+
* - No consecutive dots (..)
|
|
145
|
+
* - Automatic lowercase
|
|
146
|
+
*/
|
|
147
|
+
handleEmailInput(event) {
|
|
148
|
+
const input = event.target;
|
|
149
|
+
const cursorPosition = input.selectionStart || 0;
|
|
150
|
+
const originalValue = input.value;
|
|
151
|
+
// 1. Convert to lowercase
|
|
152
|
+
const lowerValue = originalValue.toLowerCase();
|
|
153
|
+
// 2. Split at the first @ to handle local and domain parts separately
|
|
154
|
+
const parts = lowerValue.split('@');
|
|
155
|
+
let localPart = parts[0];
|
|
156
|
+
let domainPart = parts.length > 1 ? parts.slice(1).join('') : null;
|
|
157
|
+
// 3. Filter local part: a-z, 0-9, ., _, +, -
|
|
158
|
+
// Rule: Cannot start with a dot (.)
|
|
159
|
+
localPart = localPart.replaceAll(/[^a-z0-9._+-]/g, '');
|
|
160
|
+
while (localPart.startsWith('.')) {
|
|
161
|
+
localPart = localPart.substring(1);
|
|
162
|
+
}
|
|
163
|
+
while (localPart.includes('..')) {
|
|
164
|
+
localPart = localPart.replaceAll('..', '.');
|
|
165
|
+
}
|
|
166
|
+
// 4. Filter domain part: a-z, 0-9, ., -
|
|
167
|
+
// Rule: No underscores (_), no plus signs (+), cannot start with a hyphen (-)
|
|
168
|
+
let filteredValue = localPart;
|
|
169
|
+
if (domainPart !== null) {
|
|
170
|
+
domainPart = domainPart.replaceAll(/[^a-z0-9.-]/g, '');
|
|
171
|
+
while (domainPart.startsWith('-')) {
|
|
172
|
+
domainPart = domainPart.substring(1);
|
|
173
|
+
}
|
|
174
|
+
while (domainPart.includes('..')) {
|
|
175
|
+
domainPart = domainPart.replaceAll('..', '.');
|
|
176
|
+
}
|
|
177
|
+
filteredValue += '@' + domainPart;
|
|
178
|
+
}
|
|
179
|
+
if (originalValue !== filteredValue) {
|
|
180
|
+
const charsRemovedBeforeCursor = this.countRemovedCharsBeforeCursor(originalValue.toLowerCase(), filteredValue, cursorPosition);
|
|
181
|
+
input.value = filteredValue;
|
|
182
|
+
this.control.setValue(filteredValue, { emitEvent: false });
|
|
183
|
+
const newCursorPos = Math.max(0, cursorPosition - charsRemovedBeforeCursor);
|
|
184
|
+
input.setSelectionRange(newCursorPos, newCursorPos);
|
|
185
|
+
}
|
|
186
|
+
else if (originalValue !== lowerValue) {
|
|
187
|
+
// If only case changed (e.g., typed Uppercase)
|
|
188
|
+
input.value = filteredValue;
|
|
189
|
+
this.control.setValue(filteredValue, { emitEvent: false });
|
|
190
|
+
input.setSelectionRange(cursorPosition, cursorPosition);
|
|
191
|
+
}
|
|
192
|
+
this.valueChange.emit({ name: this.field.name, value: filteredValue });
|
|
193
|
+
this.control.markAsTouched();
|
|
194
|
+
}
|
|
195
|
+
onFocus() {
|
|
196
|
+
this.hasFocus.set(true);
|
|
197
|
+
}
|
|
198
|
+
formatInitialCurrencyValue() {
|
|
199
|
+
const numericValue = Number(this.control.value);
|
|
200
|
+
if (!Number.isNaN(numericValue)) {
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
const inputElement = document.getElementById(this.uniqueId);
|
|
203
|
+
if (inputElement) {
|
|
204
|
+
inputElement.value = this.formatCurrencyWithCommas(numericValue);
|
|
205
|
+
}
|
|
206
|
+
}, 50);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
initFormControl() {
|
|
210
|
+
const controller = this.form.controls?.[this.field.name];
|
|
211
|
+
this.control = controller;
|
|
212
|
+
if (this.field.value)
|
|
213
|
+
this.control.setValue(this.field.value);
|
|
214
|
+
if (this.control) {
|
|
215
|
+
// Handle disabled state
|
|
216
|
+
if (this.field.disabled ?? false) {
|
|
217
|
+
this.control?.disable();
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
this.control?.enable();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
autoResizeInitial() {
|
|
225
|
+
const textarea = this.textareaElement?.nativeElement;
|
|
226
|
+
if (textarea) {
|
|
227
|
+
// Get the number of rows (default to 2 if not specified)
|
|
228
|
+
const rows = this.field.rows || 1;
|
|
229
|
+
// Calculate line height from computed styles
|
|
230
|
+
const computedStyle = globalThis.getComputedStyle(textarea);
|
|
231
|
+
const lineHeight = Number.parseFloat(computedStyle.lineHeight) || 32; // Default to 24px if not set
|
|
232
|
+
// Calculate minimum height based on rows
|
|
233
|
+
const minHeight = rows * lineHeight;
|
|
234
|
+
// Reset height to auto to get accurate scrollHeight
|
|
235
|
+
textarea.style.height = 'auto';
|
|
236
|
+
// Use the larger of scrollHeight or minHeight
|
|
237
|
+
const newHeight = Math.max(textarea.scrollHeight, minHeight);
|
|
238
|
+
textarea.style.height = newHeight + 'px';
|
|
239
|
+
textarea.style.overflow = 'hidden';
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
handleTextArea(event) {
|
|
243
|
+
const element = event.target;
|
|
244
|
+
const cursorPosition = element.selectionStart;
|
|
245
|
+
const originalValue = element.value;
|
|
246
|
+
// Apply alphanumeric filtering if enabled
|
|
247
|
+
if (this.field.type === 'textarea' && this.isAlphanumeric) {
|
|
248
|
+
// Only allow characters based on the selected pattern
|
|
249
|
+
const alphanumericPattern = this.alphanumericPattern;
|
|
250
|
+
let filteredValue = originalValue.replaceAll(alphanumericPattern, '');
|
|
251
|
+
// Prevent leading spaces
|
|
252
|
+
if (filteredValue.startsWith(' ')) {
|
|
253
|
+
filteredValue = filteredValue.trimStart();
|
|
254
|
+
}
|
|
255
|
+
// Replace multiple consecutive spaces with single space
|
|
256
|
+
filteredValue = filteredValue.replaceAll(/\s{2,}/g, ' ');
|
|
257
|
+
if (originalValue !== filteredValue) {
|
|
258
|
+
// Calculate the number of characters removed before the cursor position
|
|
259
|
+
const charsRemovedBeforeCursor = originalValue.length - filteredValue.length;
|
|
260
|
+
element.value = filteredValue;
|
|
261
|
+
this.control.setValue(filteredValue, { emitEvent: false });
|
|
262
|
+
// Restore cursor position - account for removed characters
|
|
263
|
+
const newCursorPos = Math.max(0, (cursorPosition || 0) - charsRemovedBeforeCursor);
|
|
264
|
+
element.setSelectionRange(newCursorPos, newCursorPos);
|
|
265
|
+
}
|
|
266
|
+
// increase height
|
|
267
|
+
this.autoResizeInitial();
|
|
268
|
+
this.valueChange.emit({ name: this.field.name, value: filteredValue });
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
let filteredValue = originalValue;
|
|
272
|
+
// Strict filtering if !isAlphanumeric
|
|
273
|
+
if (!this.isAlphanumeric) {
|
|
274
|
+
console.log('isAlphanumeric', this.isAlphanumeric);
|
|
275
|
+
const allowedPattern = /[^a-zA-Z0-9\s(){}[\];,."='+\-*/<>!&|%_@$?:]/g;
|
|
276
|
+
filteredValue = filteredValue.replaceAll(allowedPattern, '');
|
|
277
|
+
}
|
|
278
|
+
// Prevent leading spaces
|
|
279
|
+
if (filteredValue.startsWith(' ')) {
|
|
280
|
+
filteredValue = filteredValue.trimStart();
|
|
281
|
+
}
|
|
282
|
+
// Replace multiple consecutive spaces with single space
|
|
283
|
+
filteredValue = filteredValue.replaceAll(/\s{2,}/g, ' ');
|
|
284
|
+
if (originalValue !== filteredValue) {
|
|
285
|
+
// Calculate the number of characters removed before the cursor position
|
|
286
|
+
const charsRemovedBeforeCursor = originalValue.length - filteredValue.length;
|
|
287
|
+
element.value = filteredValue;
|
|
288
|
+
this.control.setValue(filteredValue, { emitEvent: false });
|
|
289
|
+
// Restore cursor position - account for removed characters
|
|
290
|
+
const newCursorPos = Math.max(0, (cursorPosition || 0) - charsRemovedBeforeCursor);
|
|
291
|
+
element.setSelectionRange(newCursorPos, newCursorPos);
|
|
292
|
+
}
|
|
293
|
+
// increase height
|
|
294
|
+
this.autoResizeInitial();
|
|
295
|
+
this.valueChange.emit({ name: this.field.name, value: filteredValue });
|
|
296
|
+
}
|
|
297
|
+
this.control.markAsTouched();
|
|
298
|
+
}
|
|
299
|
+
handleInput(event) {
|
|
300
|
+
const target = event.target;
|
|
301
|
+
const cursorPosition = target.selectionStart;
|
|
302
|
+
const originalValue = target.value;
|
|
303
|
+
let filteredValue = originalValue;
|
|
304
|
+
// IF NOT alphanumeric, user wants strict filtering:
|
|
305
|
+
// "only accept tihs.) {} [] ; , . " ' = + - * / < > ! & | % &_( )-/.;@!, $% : dont allow emoji"
|
|
306
|
+
// Interpretation: Allow alphanumeric + specified special chars. Ban emojis.
|
|
307
|
+
// Actually, the user said: "if isAlphanumeric false only accept..."
|
|
308
|
+
// So if isAlphanumeric is TRUE, we use the existing handleAlphanumericInput logic (or similar).
|
|
309
|
+
// The current logic in handleInput is generic.
|
|
310
|
+
// The existing code has a separate `handleAlphanumericInput` method but it's not called here?
|
|
311
|
+
// Wait, the template likely calls `handleAlphanumericInput` if `isAlphanumeric` is true, or `handleInput` otherwise?
|
|
312
|
+
// Let's check the template.
|
|
313
|
+
// BUT assuming `handleInput` is the generic handler:
|
|
314
|
+
if (!this.isAlphanumeric) {
|
|
315
|
+
// Regex to allow: a-z, A-Z, 0-9, and: ) {} [] ; , . " ' = + - * / < > ! & | % _ ( - @ $ ? :
|
|
316
|
+
// Note: User list: ) {} [] ; , . " ' = + - * / < > ! & | % &_( )-/.;@!, $%
|
|
317
|
+
// Combined unique special chars: ) { } [ ] ; , . " ' = + - * / < > ! & | % _ ( @ $ ? :
|
|
318
|
+
// Also space is implied? usually yes.
|
|
319
|
+
// And NO EMOJIS.
|
|
320
|
+
// easier to just replace anything NOT in the allowed set.
|
|
321
|
+
// Allowed chars regex pattern:
|
|
322
|
+
// A-Za-z0-9
|
|
323
|
+
// Space: \s
|
|
324
|
+
// Special: (){}[\];,."='+\-*/<>!&|%_@$?:
|
|
325
|
+
// Note: - inside [] needs to be escaped or at end. ] needs escape. \ needs escape? (not in list).
|
|
326
|
+
// User list has: `.` `"` `'` `?` (wait, ? not in list?)
|
|
327
|
+
// User list: `) {} [] ; , . " ' = + - * / < > ! & | % &_( )-/.;@!, $%`
|
|
328
|
+
// Uniques: ) { } [ ] ; , . " ' = + - * / < > ! & | % _ ( @ $
|
|
329
|
+
// (The `&` is repeated, `.` repeated, `,` repeated etc.)
|
|
330
|
+
// I will create a regex that matches anything NOT in this list and replace with empty string.
|
|
331
|
+
// [^a-zA-Z0-9\s(){}[\];,."='+\-*/<>!&|%_@$]
|
|
332
|
+
// Note: `*`, `+`, `?` etc need escaping in regex if outside [], but inside [] they are literals mostly.
|
|
333
|
+
// `-` needs to be last or escaped. `]` needs escape. `^` needs escape if first? No, invalid here.
|
|
334
|
+
// Let's verify valid chars:
|
|
335
|
+
// a-z A-Z 0-9
|
|
336
|
+
// space
|
|
337
|
+
// ) { } [ ] ; , . " ' = + - * / < > ! & | % _ ( @ $ :
|
|
338
|
+
const allowedPattern = /[^a-zA-Z0-9\s(){}[\];,."='+\-*/<>!&|%_@$?:]/g;
|
|
339
|
+
filteredValue = filteredValue.replaceAll(allowedPattern, '');
|
|
340
|
+
}
|
|
341
|
+
// Prevent leading spaces
|
|
342
|
+
if (filteredValue.startsWith(' ')) {
|
|
343
|
+
filteredValue = filteredValue.trimStart();
|
|
344
|
+
}
|
|
345
|
+
// replaceAll multiple consecutive spaces with single space
|
|
346
|
+
filteredValue = filteredValue.replaceAll(/\s{2,}/g, ' ');
|
|
347
|
+
if (originalValue !== filteredValue) {
|
|
348
|
+
// Calculate the number of characters removed before the cursor position
|
|
349
|
+
const charsRemovedBeforeCursor = originalValue.length - filteredValue.length;
|
|
350
|
+
target.value = filteredValue;
|
|
351
|
+
this.control.setValue(filteredValue, { emitEvent: false });
|
|
352
|
+
// Restore cursor position - account for removed characters
|
|
353
|
+
const newCursorPos = Math.max(0, (cursorPosition || 0) - charsRemovedBeforeCursor);
|
|
354
|
+
target.setSelectionRange(newCursorPos, newCursorPos);
|
|
355
|
+
}
|
|
356
|
+
// Trailing spaces will be trimmed on blur, not during input
|
|
357
|
+
this.valueChange.emit({ name: this.field.name, value: filteredValue });
|
|
358
|
+
this.control.markAsTouched();
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Handle alphanumeric input - allows only letters, numbers, and specific special characters
|
|
362
|
+
* Allowed: A-Z, a-z, 0-9, space, hyphen, underscore, ampersand, and parentheses
|
|
363
|
+
* Also prevents leading spaces and multiple consecutive spaces
|
|
364
|
+
*/
|
|
365
|
+
handleAlphanumericInput(event) {
|
|
366
|
+
const target = event.target;
|
|
367
|
+
const cursorPosition = target.selectionStart;
|
|
368
|
+
const originalValue = target.value;
|
|
369
|
+
// Only allow characters based on the selected pattern
|
|
370
|
+
const alphanumericPattern = this.alphanumericPattern;
|
|
371
|
+
let filteredValue = originalValue.replaceAll(alphanumericPattern, '');
|
|
372
|
+
// Prevent leading spaces
|
|
373
|
+
if (filteredValue.startsWith(' ')) {
|
|
374
|
+
filteredValue = filteredValue.trimStart();
|
|
375
|
+
}
|
|
376
|
+
// Replace multiple consecutive spaces with single space
|
|
377
|
+
filteredValue = filteredValue.replaceAll(/\s{2,}/g, ' ');
|
|
378
|
+
if (originalValue !== filteredValue) {
|
|
379
|
+
// Calculate the number of characters removed before the cursor position
|
|
380
|
+
const charsRemovedBeforeCursor = originalValue.length - filteredValue.length;
|
|
381
|
+
target.value = filteredValue;
|
|
382
|
+
this.control.setValue(filteredValue, { emitEvent: false });
|
|
383
|
+
// Restore cursor position - account for removed characters
|
|
384
|
+
const newCursorPos = Math.max(0, (cursorPosition || 0) - charsRemovedBeforeCursor);
|
|
385
|
+
target.setSelectionRange(newCursorPos, newCursorPos);
|
|
386
|
+
}
|
|
387
|
+
this.valueChange.emit({ name: this.field.name, value: filteredValue });
|
|
388
|
+
this.control.markAsTouched();
|
|
389
|
+
}
|
|
390
|
+
handleBlur(e, maxDecimals = 2) {
|
|
391
|
+
this.hasFocus.set(false);
|
|
392
|
+
const target = e.target;
|
|
393
|
+
const cleanValueString = target.value;
|
|
394
|
+
if (!cleanValueString) {
|
|
395
|
+
this.fieldBlur.emit({ name: this.field.name, value: '' });
|
|
396
|
+
this.cdr.detectChanges();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
// Delegate to type-specific handlers
|
|
400
|
+
if (this.field.type === 'text' || this.field.type === 'textarea') {
|
|
401
|
+
this.handleTextFieldBlur(target, cleanValueString);
|
|
402
|
+
}
|
|
403
|
+
else if (this.field.type === 'number') {
|
|
404
|
+
this.handleNumberFieldBlur(target, cleanValueString, maxDecimals);
|
|
405
|
+
}
|
|
406
|
+
// Emit final value
|
|
407
|
+
this.fieldBlur.emit({
|
|
408
|
+
name: this.field.name,
|
|
409
|
+
value: target.value,
|
|
410
|
+
});
|
|
411
|
+
this.cdr.detectChanges();
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Handle blur for text and textarea fields - trim trailing spaces
|
|
415
|
+
*/
|
|
416
|
+
handleTextFieldBlur(target, cleanValueString) {
|
|
417
|
+
const trimmed = cleanValueString.trim();
|
|
418
|
+
if (cleanValueString !== trimmed) {
|
|
419
|
+
target.value = trimmed;
|
|
420
|
+
this.control.setValue(trimmed, { emitEvent: false });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Handle blur for number fields - validate and format
|
|
425
|
+
*/
|
|
426
|
+
handleNumberFieldBlur(target, cleanValueString, maxDecimals) {
|
|
427
|
+
// Remove commas for parsing/validation
|
|
428
|
+
const numString = cleanValueString.replaceAll(',', '');
|
|
429
|
+
// Check if the string is a valid number format
|
|
430
|
+
const isValidNumberFormat = /^-?\d+(\.\d+)?$/.test(numString);
|
|
431
|
+
// If not a valid number format, clear the field
|
|
432
|
+
if (!isValidNumberFormat) {
|
|
433
|
+
target.value = '';
|
|
434
|
+
this.control.setValue('');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
// Format currency fields or set raw value for non-currency
|
|
438
|
+
if (this.field.type === 'number' && this.field.isCurrency) {
|
|
439
|
+
this.processCurrencyValue(target, numString, maxDecimals);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// Non-currency: preserve the raw string value
|
|
443
|
+
target.value = numString;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Process currency value - handle formatting and decimal places
|
|
448
|
+
*/
|
|
449
|
+
processCurrencyValue(target, numString, maxDecimals) {
|
|
450
|
+
// Handle negative values - convert to positive
|
|
451
|
+
let processedValue = numString.startsWith('-') ? numString.substring(1) : numString;
|
|
452
|
+
// Handle edge case of "-0" or just "-"
|
|
453
|
+
if (processedValue === '' || processedValue === '0') {
|
|
454
|
+
processedValue = '0';
|
|
455
|
+
}
|
|
456
|
+
// Format the value with proper decimal places
|
|
457
|
+
const cleanValueString = this.formatDecimalPlaces(processedValue, maxDecimals);
|
|
458
|
+
const formattedValue = this.formatCurrencyWithCommas(cleanValueString, maxDecimals);
|
|
459
|
+
// Update the control value with the numeric string (not formatted)
|
|
460
|
+
this.control.setValue(cleanValueString, { emitEvent: false });
|
|
461
|
+
// Trigger validators and mark as touched
|
|
462
|
+
this.control.updateValueAndValidity();
|
|
463
|
+
this.control.markAsTouched();
|
|
464
|
+
// Set display value with commas after Angular's form control update
|
|
465
|
+
setTimeout(() => {
|
|
466
|
+
target.value = formattedValue;
|
|
467
|
+
}, 0);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Format decimal places - pad or truncate to maxDecimals
|
|
471
|
+
*/
|
|
472
|
+
formatDecimalPlaces(value, maxDecimals) {
|
|
473
|
+
let [intPart, decPart] = value.includes('.') ? value.split('.') : [value, ''];
|
|
474
|
+
// Pad or truncate decimal part to maxDecimals
|
|
475
|
+
if (decPart.length < maxDecimals) {
|
|
476
|
+
decPart = decPart.padEnd(maxDecimals, '0');
|
|
477
|
+
}
|
|
478
|
+
else if (decPart.length > maxDecimals) {
|
|
479
|
+
decPart = decPart.slice(0, maxDecimals);
|
|
480
|
+
}
|
|
481
|
+
// Build the clean value string (without commas)
|
|
482
|
+
return maxDecimals > 0 ? `${intPart}.${decPart}` : intPart;
|
|
483
|
+
}
|
|
484
|
+
handleNumberInput(event, maxDecimals = 2) {
|
|
485
|
+
const target = event.target;
|
|
486
|
+
const originalValue = target.value;
|
|
487
|
+
const cursorPosition = target.selectionStart ?? 0;
|
|
488
|
+
// Step 1: Filter and format the input value
|
|
489
|
+
let value = this.filterNumericInput(target.value, maxDecimals);
|
|
490
|
+
// Step 2: Update cursor position if value changed
|
|
491
|
+
this.updateCursorPositionAfterFilter(target, originalValue, value, cursorPosition);
|
|
492
|
+
// Step 3: Update form control and validate
|
|
493
|
+
this.updateFormControlValue(value, maxDecimals);
|
|
494
|
+
// Step 4: Emit changes
|
|
495
|
+
this.valueChange.emit({ name: this.field.name, value: target.value });
|
|
496
|
+
this.control.markAsTouched();
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Filter numeric input to only allow digits and decimal point
|
|
500
|
+
* Ensures only one decimal point and enforces max decimal places
|
|
501
|
+
*/
|
|
502
|
+
filterNumericInput(value, maxDecimals) {
|
|
503
|
+
// Remove ALL non-numeric characters except decimal point
|
|
504
|
+
let filtered = value.replaceAll(/[^0-9.]/g, '');
|
|
505
|
+
// Ensure only one decimal point
|
|
506
|
+
filtered = this.filterDecimalPoints(filtered);
|
|
507
|
+
// Restrict to maxDecimals decimal places if decimal exists
|
|
508
|
+
filtered = this.enforceDecimalLimit(filtered, maxDecimals);
|
|
509
|
+
return filtered;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Ensure only one decimal point exists in the value
|
|
513
|
+
*/
|
|
514
|
+
filterDecimalPoints(value) {
|
|
515
|
+
const parts = value.split('.');
|
|
516
|
+
if (parts.length > 2) {
|
|
517
|
+
return parts[0] + '.' + parts.slice(1).join('');
|
|
518
|
+
}
|
|
519
|
+
return value;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Enforce maximum decimal places limit
|
|
523
|
+
*/
|
|
524
|
+
enforceDecimalLimit(value, maxDecimals) {
|
|
525
|
+
if (!value.includes('.')) {
|
|
526
|
+
return value;
|
|
527
|
+
}
|
|
528
|
+
const [integerPart, decimalPart] = value.split('.');
|
|
529
|
+
if (decimalPart && decimalPart.length > maxDecimals) {
|
|
530
|
+
return integerPart + '.' + decimalPart.slice(0, maxDecimals);
|
|
531
|
+
}
|
|
532
|
+
return value;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Update cursor position after filtering input
|
|
536
|
+
*/
|
|
537
|
+
updateCursorPositionAfterFilter(target, originalValue, newValue, cursorPosition) {
|
|
538
|
+
if (target.value === newValue) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const charsRemovedBeforeCursor = this.countRemovedCharsBeforeCursor(originalValue, newValue, cursorPosition);
|
|
542
|
+
target.value = newValue;
|
|
543
|
+
const newPosition = Math.max(0, cursorPosition - charsRemovedBeforeCursor);
|
|
544
|
+
target.setSelectionRange(newPosition, newPosition);
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Update form control value and perform validation
|
|
548
|
+
*/
|
|
549
|
+
updateFormControlValue(value, maxDecimals) {
|
|
550
|
+
this.control.setValue(value, { emitEvent: false });
|
|
551
|
+
this.validateIntegerDigits(value, maxDecimals);
|
|
552
|
+
this.clearNumericError();
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Count how many characters were removed before the cursor position
|
|
556
|
+
* This helps maintain correct cursor position after filtering input
|
|
557
|
+
*/
|
|
558
|
+
countRemovedCharsBeforeCursor(original, filtered, cursorPos) {
|
|
559
|
+
let removedCount = 0;
|
|
560
|
+
let filteredIndex = 0;
|
|
561
|
+
for (let i = 0; i < cursorPos && i < original.length; i++) {
|
|
562
|
+
if (filteredIndex < filtered.length && original[i] === filtered[filteredIndex]) {
|
|
563
|
+
filteredIndex++;
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
removedCount++;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return removedCount;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Handle paste event for number inputs
|
|
573
|
+
* Prevents pasting of non-numeric characters and enforces digit limits
|
|
574
|
+
*/
|
|
575
|
+
handleNumberPaste(event, maxDecimals = 2) {
|
|
576
|
+
event.preventDefault();
|
|
577
|
+
const target = event.target;
|
|
578
|
+
const pastedText = event.clipboardData?.getData('text') || '';
|
|
579
|
+
const isCurrency = this.field.type === 'number' && !!this.field.isCurrency;
|
|
580
|
+
const filteredText = isCurrency
|
|
581
|
+
? this.getFilteredCurrencyPastedText(pastedText)
|
|
582
|
+
: this.getFilteredIntegerPastedText(pastedText);
|
|
583
|
+
const finalValue = this.buildFinalPasteValue(target, filteredText, isCurrency, maxDecimals);
|
|
584
|
+
// Update the input
|
|
585
|
+
target.value = finalValue;
|
|
586
|
+
this.control.setValue(finalValue, { emitEvent: false });
|
|
587
|
+
this.validateIntegerDigits(finalValue, maxDecimals);
|
|
588
|
+
this.clearNumericError();
|
|
589
|
+
this.valueChange.emit({ name: this.field.name, value: finalValue });
|
|
590
|
+
this.control.markAsTouched();
|
|
591
|
+
this.setCursorAfterPaste(target, filteredText, finalValue);
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Filter pasted text for currency input (allows digits and decimal point)
|
|
595
|
+
*/
|
|
596
|
+
getFilteredCurrencyPastedText(pastedText) {
|
|
597
|
+
return pastedText.replaceAll(/[^0-9.]/g, '');
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Filter pasted text for integer-only input (allows digits only)
|
|
601
|
+
*/
|
|
602
|
+
getFilteredIntegerPastedText(pastedText) {
|
|
603
|
+
return pastedText.replaceAll(/\D/g, '');
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Build final value after paste operation
|
|
607
|
+
*/
|
|
608
|
+
buildFinalPasteValue(target, filteredText, isCurrency, maxDecimals) {
|
|
609
|
+
const currentValue = target.value;
|
|
610
|
+
const start = target.selectionStart;
|
|
611
|
+
const end = target.selectionEnd;
|
|
612
|
+
let finalValue;
|
|
613
|
+
// If selection API is not supported or no selection, replace entire content
|
|
614
|
+
if (start === null || end === null || (start === 0 && end === 0)) {
|
|
615
|
+
finalValue = filteredText;
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
finalValue = currentValue.substring(0, start) + filteredText + currentValue.substring(end);
|
|
619
|
+
}
|
|
620
|
+
return isCurrency ? this.applyDecimalRestrictions(finalValue, maxDecimals) : finalValue;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Apply decimal restrictions for currency fields
|
|
624
|
+
*/
|
|
625
|
+
applyDecimalRestrictions(value, maxDecimals) {
|
|
626
|
+
let result = value;
|
|
627
|
+
// Ensure only one decimal point
|
|
628
|
+
const parts = result.split('.');
|
|
629
|
+
if (parts.length > 2) {
|
|
630
|
+
result = parts[0] + '.' + parts.slice(1).join('');
|
|
631
|
+
}
|
|
632
|
+
// Restrict to maxDecimals decimal places
|
|
633
|
+
if (result.includes('.')) {
|
|
634
|
+
const [intPart, decPart] = result.split('.');
|
|
635
|
+
if (decPart.length > maxDecimals) {
|
|
636
|
+
result = intPart + '.' + decPart.slice(0, maxDecimals);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Validate integer digit limit and set/clear errors
|
|
643
|
+
*/
|
|
644
|
+
validateIntegerDigits(value, maxDecimals) {
|
|
645
|
+
if (this.field.type !== 'number' || !this.field.maxIntegerDigits) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const maxIntDigits = this.field.maxIntegerDigits ?? 15;
|
|
649
|
+
const [intPart] = value.includes('.') ? value.split('.') : [value];
|
|
650
|
+
if (intPart.length > maxIntDigits) {
|
|
651
|
+
this.setMaxIntegerDigitsError(intPart.length, maxIntDigits, maxDecimals);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
this.clearMaxIntegerDigitsError();
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Set max integer digits error
|
|
659
|
+
*/
|
|
660
|
+
setMaxIntegerDigitsError(actual, max, maxDecimals) {
|
|
661
|
+
const currentErrors = this.control.errors || {};
|
|
662
|
+
this.control.setErrors({
|
|
663
|
+
...currentErrors,
|
|
664
|
+
maxIntegerDigits: {
|
|
665
|
+
actual,
|
|
666
|
+
max,
|
|
667
|
+
maxDecimals,
|
|
668
|
+
message: `Please shorten your input to ${max} whole digits and ${maxDecimals} decimal or fewer.`,
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Clear max integer digits error if present
|
|
674
|
+
*/
|
|
675
|
+
clearMaxIntegerDigitsError() {
|
|
676
|
+
if (!this.control.hasError('maxIntegerDigits')) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const errors = { ...this.control.errors };
|
|
680
|
+
delete errors['maxIntegerDigits'];
|
|
681
|
+
this.control.setErrors(Object.keys(errors).length ? errors : null);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Clear numeric error if present
|
|
685
|
+
*/
|
|
686
|
+
clearNumericError() {
|
|
687
|
+
if (!this.control.hasError('numeric')) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const errors = { ...this.control.errors };
|
|
691
|
+
delete errors['numeric'];
|
|
692
|
+
this.control.setErrors(Object.keys(errors).length ? errors : null);
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Set cursor position after paste operation
|
|
696
|
+
*/
|
|
697
|
+
setCursorAfterPaste(target, filteredText, finalValue) {
|
|
698
|
+
// Input type="number" does not support selection APIs
|
|
699
|
+
if (target.type === 'number') {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const start = target.selectionStart || 0;
|
|
703
|
+
const newCursorPos = start + filteredText.length;
|
|
704
|
+
const actualPos = Math.min(newCursorPos, finalValue.length);
|
|
705
|
+
target.setSelectionRange(actualPos, actualPos);
|
|
706
|
+
}
|
|
707
|
+
// Format number with comma separators
|
|
708
|
+
// Uses string-based formatting for large numbers to preserve precision
|
|
709
|
+
formatCurrencyWithCommas(value, maxDecimals = 2) {
|
|
710
|
+
if (this.field.type !== 'number' || !this.field.isCurrency)
|
|
711
|
+
return value.toString();
|
|
712
|
+
const strValue = value.toString();
|
|
713
|
+
// Return original value for empty or invalid inputs
|
|
714
|
+
if (!strValue || strValue === 'NaN')
|
|
715
|
+
return '';
|
|
716
|
+
// Use string-based formatting to preserve precision for large numbers
|
|
717
|
+
const [integerPart, decimalPart] = strValue.includes('.')
|
|
718
|
+
? strValue.split('.')
|
|
719
|
+
: [strValue, ''];
|
|
720
|
+
// Format integer part with comma separators (every 3 digits from right)
|
|
721
|
+
// Using iterative approach instead of regex to avoid ReDoS concerns
|
|
722
|
+
let formattedInteger = '';
|
|
723
|
+
const len = integerPart.length;
|
|
724
|
+
for (let i = 0; i < len; i++) {
|
|
725
|
+
if (i > 0 && (len - i) % 3 === 0) {
|
|
726
|
+
formattedInteger += ',';
|
|
727
|
+
}
|
|
728
|
+
formattedInteger += integerPart[i];
|
|
729
|
+
}
|
|
730
|
+
// Format decimal part (pad or truncate to maxDecimals)
|
|
731
|
+
let formattedDecimal = decimalPart || '';
|
|
732
|
+
if (formattedDecimal.length < maxDecimals) {
|
|
733
|
+
formattedDecimal = formattedDecimal.padEnd(maxDecimals, '0');
|
|
734
|
+
}
|
|
735
|
+
else if (formattedDecimal.length > maxDecimals) {
|
|
736
|
+
formattedDecimal = formattedDecimal.slice(0, maxDecimals);
|
|
737
|
+
}
|
|
738
|
+
return maxDecimals > 0 ? `${formattedInteger}.${formattedDecimal}` : formattedInteger;
|
|
739
|
+
}
|
|
740
|
+
handleNumberKeydown(event, isCurrency) {
|
|
741
|
+
if (isCurrency)
|
|
742
|
+
return;
|
|
743
|
+
// Allow: Backspace, Delete, Tab, Arrow keys
|
|
744
|
+
if (['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
// Allow clipboard operations (Ctrl+V, Ctrl+C, Ctrl+X, Cmd+V, Cmd+C, Cmd+X)
|
|
748
|
+
// and selection (Ctrl+A, Cmd+A)
|
|
749
|
+
if (event.ctrlKey || event.metaKey) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
// Block everything that is not 0-9
|
|
753
|
+
if (!/^\d$/.test(event.key)) {
|
|
754
|
+
event.preventDefault();
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// Get locale from currency code using currencyMeta
|
|
758
|
+
getLocaleFromCurrency(currencyCode) {
|
|
759
|
+
const currencyObj = this.currencyMeta.find((c) => c.code === currencyCode);
|
|
760
|
+
return currencyObj?.locale || this.defaultLocale;
|
|
761
|
+
}
|
|
762
|
+
showPassword = false;
|
|
763
|
+
passwordStrengthLabel = '';
|
|
764
|
+
strengthClass = '';
|
|
765
|
+
passwordStrengthPercent = 0;
|
|
766
|
+
isPasswordFocused = false;
|
|
767
|
+
onPasswordFocus() {
|
|
768
|
+
this.hasFocus.set(true);
|
|
769
|
+
this.isPasswordFocused = true;
|
|
770
|
+
this.cdr.detectChanges();
|
|
771
|
+
}
|
|
772
|
+
onPasswordBlur() {
|
|
773
|
+
this.isPasswordFocused = false;
|
|
774
|
+
this.cdr.detectChanges();
|
|
775
|
+
}
|
|
776
|
+
handlePasswordBlur(e) {
|
|
777
|
+
this.handleBlur(e); // Original blur functionality (sets hasFocus to false)
|
|
778
|
+
this.onPasswordBlur(); // Hide password feedback
|
|
779
|
+
}
|
|
780
|
+
checkPasswordStrength(value) {
|
|
781
|
+
if (!value || this.field.type !== 'password') {
|
|
782
|
+
this.passwordStrengthLabel = '';
|
|
783
|
+
this.strengthClass = '';
|
|
784
|
+
this.passwordStrengthPercent = 0;
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
let score = 0;
|
|
788
|
+
if (value.length >= 8)
|
|
789
|
+
score++;
|
|
790
|
+
if (/[A-Z]/.test(value))
|
|
791
|
+
score++;
|
|
792
|
+
if (/\d/.test(value))
|
|
793
|
+
score++;
|
|
794
|
+
if (/[\W_]/.test(value))
|
|
795
|
+
score++;
|
|
796
|
+
if (score <= 1) {
|
|
797
|
+
this.passwordStrengthLabel = this.field.weakLabel || 'Weak';
|
|
798
|
+
this.strengthClass = 'text-[var(--RHB-Red-100)]';
|
|
799
|
+
this.passwordStrengthPercent = 25;
|
|
800
|
+
}
|
|
801
|
+
else if (score === 2 || score === 3) {
|
|
802
|
+
this.passwordStrengthLabel = this.field.mediumLabel || 'Medium';
|
|
803
|
+
this.strengthClass = 'text-[var(--Orange-100)]';
|
|
804
|
+
this.passwordStrengthPercent = score === 2 ? 50 : 75;
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
this.passwordStrengthLabel = this.field.strongLabel || 'Strong';
|
|
808
|
+
this.strengthClass = 'text-[var(--Green-100)]';
|
|
809
|
+
this.passwordStrengthPercent = 100;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
increment() {
|
|
813
|
+
if (this.field.type !== 'number')
|
|
814
|
+
return;
|
|
815
|
+
const step = this.field.step || 1;
|
|
816
|
+
const current = Number(this.control.value) || 0;
|
|
817
|
+
let nextValue = current + step;
|
|
818
|
+
if (typeof this.field.max === 'number') {
|
|
819
|
+
nextValue = Math.min(nextValue, this.field.max);
|
|
820
|
+
}
|
|
821
|
+
this.control.setValue(nextValue);
|
|
822
|
+
this.control.markAsTouched();
|
|
823
|
+
this.valueChange.emit({ name: this.field.name, value: nextValue });
|
|
824
|
+
// Update display value for currency inputs
|
|
825
|
+
if (this.field.isCurrency) {
|
|
826
|
+
setTimeout(() => {
|
|
827
|
+
const inputElement = document.getElementById(this.uniqueId);
|
|
828
|
+
if (inputElement) {
|
|
829
|
+
inputElement.value = this.formatCurrencyWithCommas(nextValue);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
decrement() {
|
|
835
|
+
if (this.field.type !== 'number')
|
|
836
|
+
return;
|
|
837
|
+
const step = this.field.step || 1;
|
|
838
|
+
const current = Number(this.control.value) || 0;
|
|
839
|
+
let nextValue = current - step;
|
|
840
|
+
// Ensure minimum value is 0 (no negative values)
|
|
841
|
+
const minValue = Math.max(this.field.min || 0, 0);
|
|
842
|
+
nextValue = Math.max(nextValue, minValue);
|
|
843
|
+
this.control.setValue(nextValue);
|
|
844
|
+
this.control.markAsTouched();
|
|
845
|
+
this.valueChange.emit({ name: this.field.name, value: nextValue });
|
|
846
|
+
// Update display value for currency inputs
|
|
847
|
+
if (this.field.isCurrency) {
|
|
848
|
+
setTimeout(() => {
|
|
849
|
+
const inputElement = document.getElementById(this.uniqueId);
|
|
850
|
+
if (inputElement) {
|
|
851
|
+
inputElement.value = this.formatCurrencyWithCommas(nextValue);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
copyText(e) {
|
|
857
|
+
e.stopPropagation();
|
|
858
|
+
if (!this.control.value)
|
|
859
|
+
return;
|
|
860
|
+
navigator.clipboard.writeText(this.control.value).then(() => {
|
|
861
|
+
if (this.toastService) {
|
|
862
|
+
this.toastService.success('Copied to clipboard', 'Success', 1000);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Set up visibility condition listener
|
|
868
|
+
* Watches the dependent field and shows/hides this field based on conditions
|
|
869
|
+
*/
|
|
870
|
+
setupVisibilityCondition() {
|
|
871
|
+
if (!this.field.visibilityCondition || !this.form)
|
|
872
|
+
return;
|
|
873
|
+
const { dependsOn } = this.field.visibilityCondition;
|
|
874
|
+
const dependentControl = this.form.get(dependsOn);
|
|
875
|
+
if (!dependentControl) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
// Initial visibility check
|
|
879
|
+
this.updateVisibility(dependentControl.value);
|
|
880
|
+
// Subscribe to value changes
|
|
881
|
+
dependentControl.valueChanges.subscribe((value) => {
|
|
882
|
+
this.updateVisibility(value);
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Update field visibility based on dependent field value
|
|
887
|
+
*/
|
|
888
|
+
updateVisibility(dependentValue) {
|
|
889
|
+
if (!this.field.visibilityCondition)
|
|
890
|
+
return;
|
|
891
|
+
const { showWhen, hideWhen } = this.field.visibilityCondition;
|
|
892
|
+
let shouldBeVisible = false;
|
|
893
|
+
// Check showWhen condition
|
|
894
|
+
if (showWhen !== undefined) {
|
|
895
|
+
if (Array.isArray(showWhen)) {
|
|
896
|
+
shouldBeVisible = showWhen.includes(dependentValue);
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
shouldBeVisible = dependentValue === showWhen;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
// Check hideWhen condition (overrides showWhen)
|
|
903
|
+
if (hideWhen !== undefined) {
|
|
904
|
+
const shouldHide = Array.isArray(hideWhen)
|
|
905
|
+
? hideWhen.includes(dependentValue)
|
|
906
|
+
: dependentValue === hideWhen;
|
|
907
|
+
if (shouldHide) {
|
|
908
|
+
shouldBeVisible = false;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
// Update visibility
|
|
912
|
+
this.isVisible.set(shouldBeVisible);
|
|
913
|
+
// Only clear value when hiding if there's no initial value
|
|
914
|
+
// This preserves data when editing existing records
|
|
915
|
+
if (!shouldBeVisible && this.control && !this.control.value) {
|
|
916
|
+
this.control.setValue(null);
|
|
917
|
+
this.control.markAsUntouched();
|
|
918
|
+
this.control.updateValueAndValidity();
|
|
919
|
+
}
|
|
920
|
+
this.cdr.detectChanges();
|
|
921
|
+
}
|
|
922
|
+
showFormFieldMessage(control, helperText) {
|
|
923
|
+
if (this.helperHandle) {
|
|
924
|
+
return this.helperHandle.showFormFieldMessage(control, helperText);
|
|
925
|
+
}
|
|
926
|
+
const isError = control?.touched && !!control?.errors;
|
|
927
|
+
const isHelper = !isError && Boolean(helperText ?? '');
|
|
928
|
+
return isError || isHelper;
|
|
929
|
+
}
|
|
930
|
+
getTranslatedLabel(label) {
|
|
931
|
+
if (!label)
|
|
932
|
+
return '';
|
|
933
|
+
try {
|
|
934
|
+
const labelStr = label.toString();
|
|
935
|
+
return (labelStr
|
|
936
|
+
.split('::')
|
|
937
|
+
.map((part) => part.trim())
|
|
938
|
+
.filter((part) => !!part)
|
|
939
|
+
.map((part) => this.translate.instant(part))
|
|
940
|
+
.join(' ') + this.getEndSymbol(labelStr));
|
|
941
|
+
}
|
|
942
|
+
catch (e) {
|
|
943
|
+
console.warn('Translation failed in getTranslatedLabel:', label, e);
|
|
944
|
+
return label;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
getEndSymbol(label) {
|
|
948
|
+
return label.toString().includes('required') && label.toString().includes('FNFieldMessage')
|
|
949
|
+
? '.'
|
|
950
|
+
: '';
|
|
951
|
+
}
|
|
952
|
+
// --- ICON LOGIC ---
|
|
953
|
+
updateAllIcons() {
|
|
954
|
+
// We'll call this after ViewInit or when inputs change
|
|
955
|
+
setTimeout(() => {
|
|
956
|
+
this.iconContainers?.forEach((container) => {
|
|
957
|
+
const name = container.nativeElement.getAttribute('data-icon-name');
|
|
958
|
+
const variant = container.nativeElement.getAttribute('data-icon-variant') || 'Line';
|
|
959
|
+
const size = (container.nativeElement.getAttribute('data-icon-size') ||
|
|
960
|
+
'medium');
|
|
961
|
+
const color = container.nativeElement.getAttribute('data-icon-color');
|
|
962
|
+
const disabled = container.nativeElement.getAttribute('data-icon-disabled') === 'true';
|
|
963
|
+
if (name) {
|
|
964
|
+
this.loadIconToContainer(container.nativeElement, name, variant, size, color, disabled);
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
}, 0);
|
|
968
|
+
}
|
|
969
|
+
loadIconToContainer(container, name, variant, sizeName, color, disabled) {
|
|
970
|
+
const size = this.sizeMap[sizeName];
|
|
971
|
+
const iconPath = this.getIconPath(name, variant, size);
|
|
972
|
+
if (!iconPath) {
|
|
973
|
+
container.innerHTML = '';
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
this.http.get(iconPath, { responseType: 'text' }).subscribe({
|
|
977
|
+
next: (raw) => {
|
|
978
|
+
try {
|
|
979
|
+
const parser = new DOMParser();
|
|
980
|
+
const doc = parser.parseFromString(raw, 'image/svg+xml');
|
|
981
|
+
const svg = doc.querySelector('svg');
|
|
982
|
+
if (!svg) {
|
|
983
|
+
container.innerHTML = '';
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
// SVG Normalization logic
|
|
987
|
+
for (const s of svg.querySelectorAll('style'))
|
|
988
|
+
s.remove();
|
|
989
|
+
if (color) {
|
|
990
|
+
for (const el of svg.querySelectorAll('*')) {
|
|
991
|
+
const style = el.getAttribute('style');
|
|
992
|
+
if (style) {
|
|
993
|
+
const cleaned = style
|
|
994
|
+
.replaceAll(/fill\s*:\s*[^;]+;?/gi, '')
|
|
995
|
+
.replaceAll(/stroke\s*:\s*[^;]+;?/gi, '');
|
|
996
|
+
if (cleaned)
|
|
997
|
+
el.setAttribute('style', cleaned);
|
|
998
|
+
else
|
|
999
|
+
el.removeAttribute('style');
|
|
1000
|
+
}
|
|
1001
|
+
const fill = el.getAttribute('fill');
|
|
1002
|
+
if (fill && fill !== 'none' && !fill.startsWith('url(')) {
|
|
1003
|
+
el.setAttribute('fill', 'currentColor');
|
|
1004
|
+
}
|
|
1005
|
+
const stroke = el.getAttribute('stroke');
|
|
1006
|
+
if (stroke && stroke !== 'none' && !stroke.startsWith('url(')) {
|
|
1007
|
+
el.setAttribute('stroke', 'currentColor');
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
svg.removeAttribute('width');
|
|
1012
|
+
svg.removeAttribute('height');
|
|
1013
|
+
if (!svg.getAttribute('viewBox')) {
|
|
1014
|
+
svg.setAttribute('viewBox', `0 0 ${size} ${size}`);
|
|
1015
|
+
}
|
|
1016
|
+
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
|
1017
|
+
svg.setAttribute('focusable', 'false');
|
|
1018
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
1019
|
+
svg.setAttribute('width', '100%');
|
|
1020
|
+
svg.setAttribute('height', '100%');
|
|
1021
|
+
const style = `;display:block;max-width:100%;max-height:100%;${disabled ? 'opacity:0.5;cursor:not-allowed;' : ''}`;
|
|
1022
|
+
svg.setAttribute('style', (svg.getAttribute('style') || '') + style);
|
|
1023
|
+
container.innerHTML = '';
|
|
1024
|
+
container.appendChild(svg);
|
|
1025
|
+
this.cdr.detectChanges();
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
console.error(`[fn-input] Error parsing SVG for icon "${name}":`, error);
|
|
1029
|
+
container.innerHTML = '';
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
error: () => {
|
|
1033
|
+
container.innerHTML = '';
|
|
1034
|
+
this.cdr.detectChanges();
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
getIconPath(icon, variant, size) {
|
|
1039
|
+
if (!icon)
|
|
1040
|
+
return null;
|
|
1041
|
+
if (size === 16 || size === 10) {
|
|
1042
|
+
return `assets/icons/${variant}/${size}px/${icon}.svg`;
|
|
1043
|
+
}
|
|
1044
|
+
return `assets/icons/${variant}/${size}px/${icon}--${size}.svg`;
|
|
1045
|
+
}
|
|
1046
|
+
// --- MESSAGE LOGIC ---
|
|
1047
|
+
getFieldMessage() {
|
|
1048
|
+
if (!this.control)
|
|
1049
|
+
return this.field?.helperText || '';
|
|
1050
|
+
if (this.control.touched && this.control.errors) {
|
|
1051
|
+
for (const key of Object.keys(this.control.errors)) {
|
|
1052
|
+
const errorValue = this.control.errors[key];
|
|
1053
|
+
if (errorValue && typeof errorValue === 'object' && errorValue.message) {
|
|
1054
|
+
return errorValue.message;
|
|
1055
|
+
}
|
|
1056
|
+
if (this.field?.errors?.[key]) {
|
|
1057
|
+
return this.field.errors[key];
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return (this.field?.errors?.['default'] ||
|
|
1061
|
+
'Please enter ' + this.translate.instant(this.field?.label));
|
|
1062
|
+
}
|
|
1063
|
+
return this.field?.helperText || '';
|
|
1064
|
+
}
|
|
1065
|
+
get isError() {
|
|
1066
|
+
return !!(this.control?.touched && this.control?.errors);
|
|
1067
|
+
}
|
|
1068
|
+
get isSuccess() {
|
|
1069
|
+
return !!(this.control?.valid && this.control?.touched);
|
|
1070
|
+
}
|
|
1071
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: FNInput, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
1072
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.2", type: FNInput, isStandalone: true, selector: "fn-input", inputs: { field: "field", helperHandle: "helperHandle", toastService: "toastService", currencyMeta: "currencyMeta", defaultLocale: "defaultLocale", form: "form" }, outputs: { valueChange: "valueChange", fieldBlur: "fieldBlur" }, viewQueries: [{ propertyName: "textareaElement", first: true, predicate: ["fnTextarea"], descendants: true }, { propertyName: "iconContainers", predicate: ["iconContainer"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (field.name && isVisible() && !field.hidden) {\r\n <div class=\"flex flex-col gap-2\">\r\n @if (field.type! !== 'hidden') {\r\n <label\r\n [for]=\"uniqueId\"\r\n class=\"fn-label !whitespace-normal\"\r\n [ngClass]=\"\r\n (field.statusLabel ? field.statusLabel : field.labelVariant || 'p4') +\r\n ' ' +\r\n (field.className || '')\r\n \"\r\n [style.color]=\"field.color || null\"\r\n >\r\n {{ getTranslatedLabel(field.label) }}\r\n @if (!field.required && !field.hideOptional) {\r\n <span> (Optional)</span>\r\n }\r\n </label>\r\n }\r\n\r\n <ng-container>\r\n @switch (field.type) {\r\n <!-- Textarea Field -->\r\n @case ('textarea') {\r\n <textarea\r\n #fnTextarea\r\n style=\"resize: none\"\r\n [name]=\"field.name\"\r\n [id]=\"uniqueId\"\r\n [required]=\"field.required || false\"\r\n [placeholder]=\"field.placeholder || '' | translate\"\r\n [disabled]=\"field.disabled || false\"\r\n [rows]=\"field.rows || 1\"\r\n [readonly]=\"field.readOnly || false\"\r\n [formControl]=\"control\"\r\n (input)=\"handleTextArea($event)\"\r\n (blur)=\"handleBlur($event)\"\r\n class=\"peer w-full leading-6 rounded-none border-0 border-b border-[var(--Base-30)] pb-1.75 bg-transparent text-base text-[var(--Base-100)] focus:outline-none focus:ring-0 placeholder:!text-[var(--Base-50)] placeholder:!text-base focus:[caret-color:var(--RHB-Blue-100)]\"\r\n [ngClass]=\"{\r\n 'opacity-100': control.disabled || field.readOnly,\r\n '!text-[var(--Base-30)]': control.disabled || field.readOnly,\r\n '!border-[var(--RHB-Blue-100)]': hasFocus(),\r\n '!border-[var(--RHB-Red-100)]': control.touched && control.errors,\r\n '!border-[var(--Green-100)]':\r\n field['hasSuccessBorder'] && control.valid && !control.errors,\r\n }\"\r\n (focus)=\"onFocus()\"\r\n ></textarea>\r\n }\r\n\r\n <!-- Password Field -->\r\n\r\n @case ('password') {\r\n <div class=\"relative w-full\">\r\n <input\r\n [type]=\"showPassword ? 'text' : 'password'\"\r\n [id]=\"uniqueId\"\r\n [required]=\"field.required || false\"\r\n [disabled]=\"field.disabled || false\"\r\n [formControl]=\"control\"\r\n [placeholder]=\"field.placeholder || '' | translate\"\r\n [readonly]=\"field.readOnly || false\"\r\n (input)=\"isAlphanumeric ? handleAlphanumericInput($event) : handleInput($event)\"\r\n (focus)=\"onPasswordFocus()\"\r\n (blur)=\"handlePasswordBlur($event)\"\r\n class=\"peer h-8 w-full leading-6 rounded-none border-0 border-b border-[var(--Base-30)] pb-1.75 bg-transparent text-base text-[var(--Base-100)] focus:outline-none focus:ring-0 placeholder:!text-[var(--Base-50)] placeholder:!text-base focus:[caret-color:var(--RHB-Blue-100)] overflow-hidden text-ellipsis\"\r\n [ngClass]=\"{\r\n 'opacity-100': control.disabled || field.readOnly,\r\n '!text-[var(--Base-30)]': control.disabled || field.readOnly,\r\n '!border-[var(--RHB-Blue-100)]': hasFocus() && !control.disabled,\r\n '!border-[var(--RHB-Red-100)]': control.touched && control.errors,\r\n '!border-[var(--Green-100)]': field['hasSuccessBorder'] && control.valid,\r\n 'pr-12': field.toggleMask && !field.isCopyText,\r\n 'pr-16': field.isCopyText && !field.toggleMask,\r\n 'pr-24': field.toggleMask && field.isCopyText && !field.hasGenerateKey,\r\n 'pr-32': field.toggleMask && field.isCopyText && field.hasGenerateKey,\r\n }\"\r\n />\r\n @if (field.toggleMask && !field.isCopyText) {\r\n <!-- Only Eye Icon -->\r\n <span\r\n class=\"absolute right-0 top-0 cursor-pointer text-[var(--Base-10)] hover:text-[var(--RHB-Blue-100)]\"\r\n (click)=\"showPassword = !showPassword\"\r\n (keydown)=\"showPassword = !showPassword\"\r\n >\r\n @if (!showPassword) {\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"eye-close\"\r\n data-icon-variant=\"Line\"\r\n data-icon-size=\"large\"\r\n data-icon-color=\"var(--Base-100)\"\r\n ></div>\r\n } @else {\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"eye-open\"\r\n data-icon-variant=\"Line\"\r\n data-icon-size=\"large\"\r\n data-icon-color=\"var(--Base-100)\"\r\n ></div>\r\n }\r\n </span>\r\n } @else if (field.isCopyText && !field.toggleMask) {\r\n <!-- Only Copy Icon -->\r\n <span\r\n class=\"absolute right-12 top-0 cursor-pointer text-[var(--Base-10)] hover:text-[var(--RHB-Blue-100)]\"\r\n (click)=\"copyText($event)\"\r\n (keydown)=\"copyText($event)\"\r\n >\r\n <div #iconContainer class=\"icon-container\" data-icon-name=\"two-square\"></div>\r\n </span>\r\n @if (field.hasGenerateKey) {\r\n <span\r\n class=\"absolute right-4 top-0 cursor-pointer text-[var(--Base-10)] hover:text-[var(--RHB-Blue-100)]\"\r\n (click)=\"field.onGenerateKey && field.onGenerateKey()\"\r\n (keydown)=\"field.onGenerateKey && field.onGenerateKey()\"\r\n >\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"two-square\"\r\n data-icon-variant=\"Line\"\r\n data-icon-size=\"large\"\r\n ></div>\r\n </span>\r\n }\r\n } @else if (field.toggleMask && field.isCopyText) {\r\n <!-- Both Eye + Copy Icons -->\r\n <span\r\n class=\"absolute right-20 top-0 cursor-pointer text-[var(--Base-10)] hover:text-[var(--RHB-Blue-100)]\"\r\n (click)=\"showPassword = !showPassword\"\r\n (keydown)=\"showPassword = !showPassword\"\r\n >\r\n @if (!showPassword) {\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"eye-close\"\r\n data-icon-variant=\"Line\"\r\n data-icon-size=\"large\"\r\n data-icon-color=\"var(--Base-100)\"\r\n ></div>\r\n } @else {\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"eye-open\"\r\n data-icon-variant=\"Line\"\r\n data-icon-size=\"large\"\r\n data-icon-color=\"var(--Base-100)\"\r\n ></div>\r\n }\r\n </span>\r\n\r\n <span\r\n class=\"absolute right-12 top-0 cursor-pointer text-[var(--Base-10)] hover:text-[var(--RHB-Blue-100)]\"\r\n (click)=\"copyText($event)\"\r\n (keydown)=\"copyText($event)\"\r\n >\r\n <div #iconContainer class=\"icon-container\" data-icon-name=\"two-square\"></div>\r\n </span>\r\n\r\n @if (field.hasGenerateKey) {\r\n <span\r\n class=\"absolute right-4 top-0 text-[var(--Base-10)]\"\r\n [ngClass]=\"{\r\n 'cursor-pointer hover:text-[var(--RHB-Blue-100)]':\r\n control.value && control.value.length > 0,\r\n 'cursor-not-allowed opacity-50': !control.value || control.value.length === 0,\r\n }\"\r\n (click)=\"\r\n control.value &&\r\n control.value.length > 0 &&\r\n field.onGenerateKey &&\r\n field.onGenerateKey()\r\n \"\r\n (keydown)=\"\r\n control.value &&\r\n control.value.length > 0 &&\r\n field.onGenerateKey &&\r\n field.onGenerateKey()\r\n \"\r\n >\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"round-arrow-top-left\"\r\n ></div>\r\n </span>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Number Field -->\r\n @case ('number') {\r\n <input\r\n [type]=\"field.type === 'number' && field.isCurrency ? 'text' : 'number'\"\r\n [name]=\"field.name\"\r\n [id]=\"uniqueId\"\r\n [required]=\"field.required || false\"\r\n [placeholder]=\"field.placeholder || '' | translate\"\r\n [disabled]=\"isDisabled || false\"\r\n [readOnly]=\"field.readOnly || false\"\r\n [formControl]=\"control\"\r\n [min]=\"field.type === 'number' && !field.isCurrency ? 0 : null\"\r\n [max]=\"field.type === 'number' && !field.isCurrency ? field.max : null\"\r\n [step]=\"field.type === 'number' && !field.isCurrency ? field.step || 1 : null\"\r\n (input)=\"handleNumberInput($event, field?.minFractionDigits || 2)\"\r\n (paste)=\"handleNumberPaste($event, field?.minFractionDigits || 2)\"\r\n (blur)=\"handleBlur($event, field?.minFractionDigits || 2)\"\r\n (keydown)=\"handleNumberKeydown($event, field?.isCurrency || false)\"\r\n class=\"peer h-8 w-full leading-6 rounded-none border-0 border-b border-[var(--Base-30)] pb-1.75 bg-transparent text-base text-[var(--Base-100)] focus:outline-none focus:ring-0 placeholder:!text-[var(--Base-50)] placeholder:!text-base focus:[caret-color:var(--RHB-Blue-100)]\"\r\n [ngClass]=\"{\r\n 'opacity-100': control.disabled || field.readOnly,\r\n '!text-[var(--Base-30)]': control.disabled || field.readOnly,\r\n '!border-[var(--RHB-Blue-100)]': hasFocus(),\r\n '!border-[var(--RHB-Red-100)]': control.touched && control.errors,\r\n '!border-[var(--Green-100)]':\r\n field['hasSuccessBorder'] && control.valid && !control.errors,\r\n 'text-[24px] font-bold leading-[32px]': field.isCurrency,\r\n }\"\r\n (focus)=\"onFocus()\"\r\n />\r\n }\r\n\r\n <!-- Default Input Field (text, email, etc.) -->\r\n @default {\r\n <div class=\"relative z-0\">\r\n <input\r\n [type]=\"field.type\"\r\n [name]=\"field.name\"\r\n [id]=\"uniqueId\"\r\n [required]=\"field.required || false\"\r\n [placeholder]=\"field.placeholder || '' | translate\"\r\n [disabled]=\"field.disabled || false\"\r\n [readOnly]=\"field.readOnly || false\"\r\n [formControl]=\"control\"\r\n (input)=\"\r\n isEmailField\r\n ? handleEmailInput($event)\r\n : field.type === 'text' && isAlphanumeric\r\n ? handleAlphanumericInput($event)\r\n : handleInput($event)\r\n \"\r\n (blur)=\"handleBlur($event)\"\r\n class=\"peer h-8 w-full leading-6 rounded-none border-0 border-b border-[var(--Base-30)] pb-1.75 bg-transparent text-base text-[var(--Base-100)] focus:outline-none focus:ring-0 placeholder:!text-[var(--Base-50)] placeholder:!text-base focus:[caret-color:var(--RHB-Blue-100)]\"\r\n [ngClass]=\"{\r\n 'opacity-100': control.disabled || field.readOnly,\r\n '!text-[var(--Base-30)]': control.disabled || field.readOnly,\r\n '!border-[var(--RHB-Blue-100)]': hasFocus(),\r\n '!border-[var(--RHB-Red-100)]': control.touched && control.errors,\r\n '!border-[var(--Green-100)]':\r\n field['hasSuccessBorder'] && control.valid && !control.errors,\r\n 'pr-8': field.icon,\r\n }\"\r\n (focus)=\"onFocus()\"\r\n />\r\n @if (field.icon) {\r\n <span class=\"absolute bottom-2 right-0.5 flex items-center\">\r\n <div\r\n #iconContainer\r\n class=\"bg-white\"\r\n [attr.data-icon-name]=\"field.icon.name\"\r\n [attr.data-icon-variant]=\"field.icon.variant\"\r\n [attr.data-icon-size]=\"getIconSizeName(field.icon.size)\"\r\n [attr.data-icon-color]=\"control.disabled ? 'var(--Base-30)' : ''\"\r\n ></div>\r\n </span>\r\n }\r\n </div>\r\n }\r\n }\r\n </ng-container>\r\n\r\n @if (showFormFieldMessage(control, helperText)) {\r\n <div class=\"fn-field-message-container\">\r\n <span\r\n class=\"fn-field-message-text\"\r\n [ngClass]=\"{\r\n error: isError,\r\n success: isSuccess,\r\n }\"\r\n >\r\n {{ getFieldMessage() | translate }}\r\n </span>\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: [".fn-label-container{display:flex;flex-direction:column;gap:4px;width:100%}.fn-label-text{color:var(--fn-sys-color-on-surface-variant);font-family:var(--fn-sys-font-family-base);font-size:14px;font-weight:500;line-height:20px}.fn-label-text.disabled{color:var(--fn-sys-color-outline)}.fn-label-text.error{color:var(--fn-sys-color-error)}.fn-field-message-container{display:flex;align-items:center;gap:4px;margin-top:4px;min-height:20px}.fn-field-message-text{font-family:var(--fn-sys-font-family-base);font-size:12px;font-weight:400;line-height:16px;color:var(--fn-sys-color-on-surface-variant)}.fn-field-message-text.error{color:var(--fn-sys-color-error)}.fn-field-message-text.success{color:var(--fn-sys-color-success, #2e7d32)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1073
|
+
}
|
|
1074
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.2", ngImport: i0, type: FNInput, decorators: [{
|
|
1075
|
+
type: Component,
|
|
1076
|
+
args: [{ selector: 'fn-input', standalone: true, imports: [FormsModule, CommonModule, ReactiveFormsModule, TranslateModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (field.name && isVisible() && !field.hidden) {\r\n <div class=\"flex flex-col gap-2\">\r\n @if (field.type! !== 'hidden') {\r\n <label\r\n [for]=\"uniqueId\"\r\n class=\"fn-label !whitespace-normal\"\r\n [ngClass]=\"\r\n (field.statusLabel ? field.statusLabel : field.labelVariant || 'p4') +\r\n ' ' +\r\n (field.className || '')\r\n \"\r\n [style.color]=\"field.color || null\"\r\n >\r\n {{ getTranslatedLabel(field.label) }}\r\n @if (!field.required && !field.hideOptional) {\r\n <span> (Optional)</span>\r\n }\r\n </label>\r\n }\r\n\r\n <ng-container>\r\n @switch (field.type) {\r\n <!-- Textarea Field -->\r\n @case ('textarea') {\r\n <textarea\r\n #fnTextarea\r\n style=\"resize: none\"\r\n [name]=\"field.name\"\r\n [id]=\"uniqueId\"\r\n [required]=\"field.required || false\"\r\n [placeholder]=\"field.placeholder || '' | translate\"\r\n [disabled]=\"field.disabled || false\"\r\n [rows]=\"field.rows || 1\"\r\n [readonly]=\"field.readOnly || false\"\r\n [formControl]=\"control\"\r\n (input)=\"handleTextArea($event)\"\r\n (blur)=\"handleBlur($event)\"\r\n class=\"peer w-full leading-6 rounded-none border-0 border-b border-[var(--Base-30)] pb-1.75 bg-transparent text-base text-[var(--Base-100)] focus:outline-none focus:ring-0 placeholder:!text-[var(--Base-50)] placeholder:!text-base focus:[caret-color:var(--RHB-Blue-100)]\"\r\n [ngClass]=\"{\r\n 'opacity-100': control.disabled || field.readOnly,\r\n '!text-[var(--Base-30)]': control.disabled || field.readOnly,\r\n '!border-[var(--RHB-Blue-100)]': hasFocus(),\r\n '!border-[var(--RHB-Red-100)]': control.touched && control.errors,\r\n '!border-[var(--Green-100)]':\r\n field['hasSuccessBorder'] && control.valid && !control.errors,\r\n }\"\r\n (focus)=\"onFocus()\"\r\n ></textarea>\r\n }\r\n\r\n <!-- Password Field -->\r\n\r\n @case ('password') {\r\n <div class=\"relative w-full\">\r\n <input\r\n [type]=\"showPassword ? 'text' : 'password'\"\r\n [id]=\"uniqueId\"\r\n [required]=\"field.required || false\"\r\n [disabled]=\"field.disabled || false\"\r\n [formControl]=\"control\"\r\n [placeholder]=\"field.placeholder || '' | translate\"\r\n [readonly]=\"field.readOnly || false\"\r\n (input)=\"isAlphanumeric ? handleAlphanumericInput($event) : handleInput($event)\"\r\n (focus)=\"onPasswordFocus()\"\r\n (blur)=\"handlePasswordBlur($event)\"\r\n class=\"peer h-8 w-full leading-6 rounded-none border-0 border-b border-[var(--Base-30)] pb-1.75 bg-transparent text-base text-[var(--Base-100)] focus:outline-none focus:ring-0 placeholder:!text-[var(--Base-50)] placeholder:!text-base focus:[caret-color:var(--RHB-Blue-100)] overflow-hidden text-ellipsis\"\r\n [ngClass]=\"{\r\n 'opacity-100': control.disabled || field.readOnly,\r\n '!text-[var(--Base-30)]': control.disabled || field.readOnly,\r\n '!border-[var(--RHB-Blue-100)]': hasFocus() && !control.disabled,\r\n '!border-[var(--RHB-Red-100)]': control.touched && control.errors,\r\n '!border-[var(--Green-100)]': field['hasSuccessBorder'] && control.valid,\r\n 'pr-12': field.toggleMask && !field.isCopyText,\r\n 'pr-16': field.isCopyText && !field.toggleMask,\r\n 'pr-24': field.toggleMask && field.isCopyText && !field.hasGenerateKey,\r\n 'pr-32': field.toggleMask && field.isCopyText && field.hasGenerateKey,\r\n }\"\r\n />\r\n @if (field.toggleMask && !field.isCopyText) {\r\n <!-- Only Eye Icon -->\r\n <span\r\n class=\"absolute right-0 top-0 cursor-pointer text-[var(--Base-10)] hover:text-[var(--RHB-Blue-100)]\"\r\n (click)=\"showPassword = !showPassword\"\r\n (keydown)=\"showPassword = !showPassword\"\r\n >\r\n @if (!showPassword) {\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"eye-close\"\r\n data-icon-variant=\"Line\"\r\n data-icon-size=\"large\"\r\n data-icon-color=\"var(--Base-100)\"\r\n ></div>\r\n } @else {\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"eye-open\"\r\n data-icon-variant=\"Line\"\r\n data-icon-size=\"large\"\r\n data-icon-color=\"var(--Base-100)\"\r\n ></div>\r\n }\r\n </span>\r\n } @else if (field.isCopyText && !field.toggleMask) {\r\n <!-- Only Copy Icon -->\r\n <span\r\n class=\"absolute right-12 top-0 cursor-pointer text-[var(--Base-10)] hover:text-[var(--RHB-Blue-100)]\"\r\n (click)=\"copyText($event)\"\r\n (keydown)=\"copyText($event)\"\r\n >\r\n <div #iconContainer class=\"icon-container\" data-icon-name=\"two-square\"></div>\r\n </span>\r\n @if (field.hasGenerateKey) {\r\n <span\r\n class=\"absolute right-4 top-0 cursor-pointer text-[var(--Base-10)] hover:text-[var(--RHB-Blue-100)]\"\r\n (click)=\"field.onGenerateKey && field.onGenerateKey()\"\r\n (keydown)=\"field.onGenerateKey && field.onGenerateKey()\"\r\n >\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"two-square\"\r\n data-icon-variant=\"Line\"\r\n data-icon-size=\"large\"\r\n ></div>\r\n </span>\r\n }\r\n } @else if (field.toggleMask && field.isCopyText) {\r\n <!-- Both Eye + Copy Icons -->\r\n <span\r\n class=\"absolute right-20 top-0 cursor-pointer text-[var(--Base-10)] hover:text-[var(--RHB-Blue-100)]\"\r\n (click)=\"showPassword = !showPassword\"\r\n (keydown)=\"showPassword = !showPassword\"\r\n >\r\n @if (!showPassword) {\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"eye-close\"\r\n data-icon-variant=\"Line\"\r\n data-icon-size=\"large\"\r\n data-icon-color=\"var(--Base-100)\"\r\n ></div>\r\n } @else {\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"eye-open\"\r\n data-icon-variant=\"Line\"\r\n data-icon-size=\"large\"\r\n data-icon-color=\"var(--Base-100)\"\r\n ></div>\r\n }\r\n </span>\r\n\r\n <span\r\n class=\"absolute right-12 top-0 cursor-pointer text-[var(--Base-10)] hover:text-[var(--RHB-Blue-100)]\"\r\n (click)=\"copyText($event)\"\r\n (keydown)=\"copyText($event)\"\r\n >\r\n <div #iconContainer class=\"icon-container\" data-icon-name=\"two-square\"></div>\r\n </span>\r\n\r\n @if (field.hasGenerateKey) {\r\n <span\r\n class=\"absolute right-4 top-0 text-[var(--Base-10)]\"\r\n [ngClass]=\"{\r\n 'cursor-pointer hover:text-[var(--RHB-Blue-100)]':\r\n control.value && control.value.length > 0,\r\n 'cursor-not-allowed opacity-50': !control.value || control.value.length === 0,\r\n }\"\r\n (click)=\"\r\n control.value &&\r\n control.value.length > 0 &&\r\n field.onGenerateKey &&\r\n field.onGenerateKey()\r\n \"\r\n (keydown)=\"\r\n control.value &&\r\n control.value.length > 0 &&\r\n field.onGenerateKey &&\r\n field.onGenerateKey()\r\n \"\r\n >\r\n <div\r\n #iconContainer\r\n class=\"icon-container\"\r\n data-icon-name=\"round-arrow-top-left\"\r\n ></div>\r\n </span>\r\n }\r\n }\r\n </div>\r\n }\r\n\r\n <!-- Number Field -->\r\n @case ('number') {\r\n <input\r\n [type]=\"field.type === 'number' && field.isCurrency ? 'text' : 'number'\"\r\n [name]=\"field.name\"\r\n [id]=\"uniqueId\"\r\n [required]=\"field.required || false\"\r\n [placeholder]=\"field.placeholder || '' | translate\"\r\n [disabled]=\"isDisabled || false\"\r\n [readOnly]=\"field.readOnly || false\"\r\n [formControl]=\"control\"\r\n [min]=\"field.type === 'number' && !field.isCurrency ? 0 : null\"\r\n [max]=\"field.type === 'number' && !field.isCurrency ? field.max : null\"\r\n [step]=\"field.type === 'number' && !field.isCurrency ? field.step || 1 : null\"\r\n (input)=\"handleNumberInput($event, field?.minFractionDigits || 2)\"\r\n (paste)=\"handleNumberPaste($event, field?.minFractionDigits || 2)\"\r\n (blur)=\"handleBlur($event, field?.minFractionDigits || 2)\"\r\n (keydown)=\"handleNumberKeydown($event, field?.isCurrency || false)\"\r\n class=\"peer h-8 w-full leading-6 rounded-none border-0 border-b border-[var(--Base-30)] pb-1.75 bg-transparent text-base text-[var(--Base-100)] focus:outline-none focus:ring-0 placeholder:!text-[var(--Base-50)] placeholder:!text-base focus:[caret-color:var(--RHB-Blue-100)]\"\r\n [ngClass]=\"{\r\n 'opacity-100': control.disabled || field.readOnly,\r\n '!text-[var(--Base-30)]': control.disabled || field.readOnly,\r\n '!border-[var(--RHB-Blue-100)]': hasFocus(),\r\n '!border-[var(--RHB-Red-100)]': control.touched && control.errors,\r\n '!border-[var(--Green-100)]':\r\n field['hasSuccessBorder'] && control.valid && !control.errors,\r\n 'text-[24px] font-bold leading-[32px]': field.isCurrency,\r\n }\"\r\n (focus)=\"onFocus()\"\r\n />\r\n }\r\n\r\n <!-- Default Input Field (text, email, etc.) -->\r\n @default {\r\n <div class=\"relative z-0\">\r\n <input\r\n [type]=\"field.type\"\r\n [name]=\"field.name\"\r\n [id]=\"uniqueId\"\r\n [required]=\"field.required || false\"\r\n [placeholder]=\"field.placeholder || '' | translate\"\r\n [disabled]=\"field.disabled || false\"\r\n [readOnly]=\"field.readOnly || false\"\r\n [formControl]=\"control\"\r\n (input)=\"\r\n isEmailField\r\n ? handleEmailInput($event)\r\n : field.type === 'text' && isAlphanumeric\r\n ? handleAlphanumericInput($event)\r\n : handleInput($event)\r\n \"\r\n (blur)=\"handleBlur($event)\"\r\n class=\"peer h-8 w-full leading-6 rounded-none border-0 border-b border-[var(--Base-30)] pb-1.75 bg-transparent text-base text-[var(--Base-100)] focus:outline-none focus:ring-0 placeholder:!text-[var(--Base-50)] placeholder:!text-base focus:[caret-color:var(--RHB-Blue-100)]\"\r\n [ngClass]=\"{\r\n 'opacity-100': control.disabled || field.readOnly,\r\n '!text-[var(--Base-30)]': control.disabled || field.readOnly,\r\n '!border-[var(--RHB-Blue-100)]': hasFocus(),\r\n '!border-[var(--RHB-Red-100)]': control.touched && control.errors,\r\n '!border-[var(--Green-100)]':\r\n field['hasSuccessBorder'] && control.valid && !control.errors,\r\n 'pr-8': field.icon,\r\n }\"\r\n (focus)=\"onFocus()\"\r\n />\r\n @if (field.icon) {\r\n <span class=\"absolute bottom-2 right-0.5 flex items-center\">\r\n <div\r\n #iconContainer\r\n class=\"bg-white\"\r\n [attr.data-icon-name]=\"field.icon.name\"\r\n [attr.data-icon-variant]=\"field.icon.variant\"\r\n [attr.data-icon-size]=\"getIconSizeName(field.icon.size)\"\r\n [attr.data-icon-color]=\"control.disabled ? 'var(--Base-30)' : ''\"\r\n ></div>\r\n </span>\r\n }\r\n </div>\r\n }\r\n }\r\n </ng-container>\r\n\r\n @if (showFormFieldMessage(control, helperText)) {\r\n <div class=\"fn-field-message-container\">\r\n <span\r\n class=\"fn-field-message-text\"\r\n [ngClass]=\"{\r\n error: isError,\r\n success: isSuccess,\r\n }\"\r\n >\r\n {{ getFieldMessage() | translate }}\r\n </span>\r\n </div>\r\n }\r\n </div>\r\n}\r\n", styles: [".fn-label-container{display:flex;flex-direction:column;gap:4px;width:100%}.fn-label-text{color:var(--fn-sys-color-on-surface-variant);font-family:var(--fn-sys-font-family-base);font-size:14px;font-weight:500;line-height:20px}.fn-label-text.disabled{color:var(--fn-sys-color-outline)}.fn-label-text.error{color:var(--fn-sys-color-error)}.fn-field-message-container{display:flex;align-items:center;gap:4px;margin-top:4px;min-height:20px}.fn-field-message-text{font-family:var(--fn-sys-font-family-base);font-size:12px;font-weight:400;line-height:16px;color:var(--fn-sys-color-on-surface-variant)}.fn-field-message-text.error{color:var(--fn-sys-color-error)}.fn-field-message-text.success{color:var(--fn-sys-color-success, #2e7d32)}\n"] }]
|
|
1077
|
+
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { field: [{
|
|
1078
|
+
type: Input
|
|
1079
|
+
}], helperHandle: [{
|
|
1080
|
+
type: Input
|
|
1081
|
+
}], toastService: [{
|
|
1082
|
+
type: Input
|
|
1083
|
+
}], currencyMeta: [{
|
|
1084
|
+
type: Input
|
|
1085
|
+
}], defaultLocale: [{
|
|
1086
|
+
type: Input
|
|
1087
|
+
}], form: [{
|
|
1088
|
+
type: Input
|
|
1089
|
+
}], valueChange: [{
|
|
1090
|
+
type: Output
|
|
1091
|
+
}], fieldBlur: [{
|
|
1092
|
+
type: Output
|
|
1093
|
+
}], textareaElement: [{
|
|
1094
|
+
type: ViewChild,
|
|
1095
|
+
args: ['fnTextarea']
|
|
1096
|
+
}], iconContainers: [{
|
|
1097
|
+
type: ViewChildren,
|
|
1098
|
+
args: ['iconContainer']
|
|
1099
|
+
}] } });
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Generated bundle index. Do not edit.
|
|
1103
|
+
*/
|
|
1104
|
+
|
|
1105
|
+
export { DEFAULT_CURRENCY_META, FNInput };
|
|
1106
|
+
//# sourceMappingURL=fn-input.mjs.map
|