@up_si_vale/sivale-componentes-angular 1.0.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/README.md +81 -0
- package/ng-package.json +9 -0
- package/package.json +40 -0
- package/src/lib/atoms/button/button.component.html +21 -0
- package/src/lib/atoms/button/button.component.scss +242 -0
- package/src/lib/atoms/button/button.component.ts +43 -0
- package/src/lib/atoms/checkbox/checkbox.component.scss +131 -0
- package/src/lib/atoms/checkbox/checkbox.component.ts +130 -0
- package/src/lib/atoms/date-picker/date-picker.component.html +295 -0
- package/src/lib/atoms/date-picker/date-picker.component.scss +1002 -0
- package/src/lib/atoms/date-picker/date-picker.component.ts +942 -0
- package/src/lib/atoms/input/input.component.ts +239 -0
- package/src/lib/atoms/loader/loader.component.scss +144 -0
- package/src/lib/atoms/loader/loader.component.ts +44 -0
- package/src/lib/atoms/radio/radio.component.scss +115 -0
- package/src/lib/atoms/radio/radio.component.ts +103 -0
- package/src/lib/atoms/textarea/textarea.component.ts +155 -0
- package/src/lib/directives/asset-source.directive.ts +64 -0
- package/src/lib/directives/icon-source.directive.ts +74 -0
- package/src/lib/directives/no-leading-space.directive.ts +18 -0
- package/src/lib/directives/only-letters.directive.ts +21 -0
- package/src/lib/directives/only-number.directive.ts +15 -0
- package/src/lib/directives/rfc.directive.ts +60 -0
- package/src/lib/directives/tooltip.directive.ts +211 -0
- package/src/lib/models/snackbar.models.ts +16 -0
- package/src/lib/molecules/copy-input/copy-input.component.html +29 -0
- package/src/lib/molecules/copy-input/copy-input.component.scss +101 -0
- package/src/lib/molecules/copy-input/copy-input.component.ts +41 -0
- package/src/lib/molecules/multiselect/multiselect.component.scss +298 -0
- package/src/lib/molecules/multiselect/multiselect.component.ts +487 -0
- package/src/lib/molecules/option/option.component.ts +45 -0
- package/src/lib/molecules/phone-input/phone-input.component.scss +78 -0
- package/src/lib/molecules/phone-input/phone-input.component.ts +43 -0
- package/src/lib/molecules/select/select.component.ts +482 -0
- package/src/lib/molecules/snackbar/snackbar.component.scss +201 -0
- package/src/lib/molecules/snackbar/snackbar.component.ts +321 -0
- package/src/lib/services/snackbar.service.ts +103 -0
- package/src/lib/tokens/asset-base-url.token.ts +10 -0
- package/src/public-api.ts +38 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
Input,
|
|
4
|
+
forwardRef,
|
|
5
|
+
ViewChild,
|
|
6
|
+
ElementRef,
|
|
7
|
+
HostListener,
|
|
8
|
+
ContentChildren,
|
|
9
|
+
QueryList,
|
|
10
|
+
AfterContentInit,
|
|
11
|
+
OnDestroy,
|
|
12
|
+
Renderer2,
|
|
13
|
+
Inject,
|
|
14
|
+
RendererStyleFlags2
|
|
15
|
+
} from '@angular/core';
|
|
16
|
+
import { CommonModule, DOCUMENT } from '@angular/common';
|
|
17
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms';
|
|
18
|
+
import { OptionComponent } from '../option/option.component';
|
|
19
|
+
|
|
20
|
+
export interface MultiselectOption {
|
|
21
|
+
value: any;
|
|
22
|
+
label: string;
|
|
23
|
+
icon?: string;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface DropdownPosition {
|
|
28
|
+
top?: string;
|
|
29
|
+
bottom?: string;
|
|
30
|
+
left: string;
|
|
31
|
+
width: string;
|
|
32
|
+
maxHeight: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Component({
|
|
36
|
+
selector: 'app-multiselect',
|
|
37
|
+
standalone: true,
|
|
38
|
+
imports: [CommonModule, ReactiveFormsModule, FormsModule],
|
|
39
|
+
template: `
|
|
40
|
+
<div class="multiselect-wrapper" [ngClass]="customClass">
|
|
41
|
+
<label *ngIf="label" [for]="id">{{ label }}</label>
|
|
42
|
+
<div class="multiselect-container">
|
|
43
|
+
<div
|
|
44
|
+
#trigger
|
|
45
|
+
class="multiselect-header"
|
|
46
|
+
(click)="toggleDropdown($event)"
|
|
47
|
+
[class.disabled]="disabled"
|
|
48
|
+
[class.open]="isOpen"
|
|
49
|
+
[class.error]="hasError">
|
|
50
|
+
<span class="multiselect-placeholder" *ngIf="getSelectedCount() === 0">
|
|
51
|
+
{{ placeholder || 'Selecciona uno o más...' }}
|
|
52
|
+
</span>
|
|
53
|
+
<span class="multiselect-selected" *ngIf="getSelectedCount() > 0">
|
|
54
|
+
{{ getSelectedLabels() }}
|
|
55
|
+
</span>
|
|
56
|
+
<svg class="multiselect-arrow" [class.open]="isOpen" width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
57
|
+
<path d="M1 1.5L6 6.5L11 1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
58
|
+
</svg>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div *ngIf="hasError && errorMessage" class="field-error">
|
|
62
|
+
{{ errorMessage }}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Hidden container for projected options -->
|
|
66
|
+
<div style="display: none;">
|
|
67
|
+
<ng-content></ng-content>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
`,
|
|
71
|
+
styleUrls: ['./multiselect.component.scss'],
|
|
72
|
+
providers: [
|
|
73
|
+
{
|
|
74
|
+
provide: NG_VALUE_ACCESSOR,
|
|
75
|
+
useExisting: forwardRef(() => MultiselectComponent),
|
|
76
|
+
multi: true
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
})
|
|
80
|
+
export class MultiselectComponent implements ControlValueAccessor, AfterContentInit, OnDestroy {
|
|
81
|
+
@Input() label = '';
|
|
82
|
+
@Input() id = '';
|
|
83
|
+
@Input() placeholder = '';
|
|
84
|
+
@Input() options: MultiselectOption[] | any[] = [];
|
|
85
|
+
@Input() hasError = false;
|
|
86
|
+
@Input() errorMessage = '';
|
|
87
|
+
@Input() disabled = false;
|
|
88
|
+
@Input() displayKey: string = 'label'; // Property to display as label
|
|
89
|
+
@Input() valueKey: string = 'value'; // Property to use as value
|
|
90
|
+
@Input() customClass: string = ''; // Custom CSS class for styling
|
|
91
|
+
@ViewChild('trigger', { read: ElementRef }) trigger!: ElementRef;
|
|
92
|
+
@ContentChildren(OptionComponent) contentOptions!: QueryList<OptionComponent>;
|
|
93
|
+
|
|
94
|
+
value: any[] = [];
|
|
95
|
+
isOpen = false;
|
|
96
|
+
onChange: any = () => {};
|
|
97
|
+
onTouched: any = () => {};
|
|
98
|
+
private _usingContentOptions = false;
|
|
99
|
+
|
|
100
|
+
private dropdownElement: HTMLElement | null = null;
|
|
101
|
+
private backdropElement: HTMLElement | null = null;
|
|
102
|
+
private scrollHandler: (() => void) | null = null;
|
|
103
|
+
private resizeHandler: (() => void) | null = null;
|
|
104
|
+
|
|
105
|
+
constructor(
|
|
106
|
+
private renderer: Renderer2,
|
|
107
|
+
@Inject(DOCUMENT) private document: Document
|
|
108
|
+
) {}
|
|
109
|
+
|
|
110
|
+
ngAfterContentInit(): void {
|
|
111
|
+
// Determine if using content projection or input options
|
|
112
|
+
this._usingContentOptions = this.contentOptions && this.contentOptions.length > 0;
|
|
113
|
+
|
|
114
|
+
// Subscribe to changes in content options
|
|
115
|
+
if (this.contentOptions) {
|
|
116
|
+
this.contentOptions.changes.subscribe(() => {
|
|
117
|
+
this._usingContentOptions = this.contentOptions.length > 0;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
ngOnDestroy(): void {
|
|
123
|
+
this.destroyDropdown();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get all options (from content or input)
|
|
128
|
+
*/
|
|
129
|
+
getAllOptions(): any[] {
|
|
130
|
+
if (this._usingContentOptions && this.contentOptions) {
|
|
131
|
+
return this.contentOptions.toArray();
|
|
132
|
+
}
|
|
133
|
+
return this.options || [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get display label from option
|
|
138
|
+
*/
|
|
139
|
+
getOptionLabel(option: any): string {
|
|
140
|
+
if (option instanceof OptionComponent) {
|
|
141
|
+
return option.viewValue;
|
|
142
|
+
}
|
|
143
|
+
return option?.[this.displayKey] || '';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get value from option
|
|
148
|
+
*/
|
|
149
|
+
getOptionValue(option: any): any {
|
|
150
|
+
if (option instanceof OptionComponent) {
|
|
151
|
+
return option.value;
|
|
152
|
+
}
|
|
153
|
+
return option?.[this.valueKey];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if option is disabled
|
|
158
|
+
*/
|
|
159
|
+
isOptionDisabled(option: any): boolean {
|
|
160
|
+
if (option instanceof OptionComponent) {
|
|
161
|
+
return option.disabled;
|
|
162
|
+
}
|
|
163
|
+
return option?.disabled || false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@HostListener('document:click', ['$event'])
|
|
167
|
+
onDocumentClick(event: MouseEvent): void {
|
|
168
|
+
// Close dropdown if click is outside
|
|
169
|
+
if (this.isOpen && this.dropdownElement && !this.dropdownElement.contains(event.target as Node)) {
|
|
170
|
+
const triggerElement = this.trigger?.nativeElement;
|
|
171
|
+
if (triggerElement && !triggerElement.contains(event.target as Node)) {
|
|
172
|
+
this.closeDropdown();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
toggleDropdown(event: Event): void {
|
|
178
|
+
event.stopPropagation();
|
|
179
|
+
if (this.disabled) return;
|
|
180
|
+
|
|
181
|
+
if (this.isOpen) {
|
|
182
|
+
this.closeDropdown();
|
|
183
|
+
} else {
|
|
184
|
+
this.openDropdown();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private openDropdown(): void {
|
|
189
|
+
if (!this.trigger) return;
|
|
190
|
+
|
|
191
|
+
// Create backdrop
|
|
192
|
+
this.backdropElement = this.renderer.createElement('div');
|
|
193
|
+
this.renderer.addClass(this.backdropElement, 'multiselect-custom-backdrop');
|
|
194
|
+
this.renderer.listen(this.backdropElement, 'click', () => this.closeDropdown());
|
|
195
|
+
this.renderer.appendChild(this.document.body, this.backdropElement);
|
|
196
|
+
|
|
197
|
+
// Create dropdown
|
|
198
|
+
this.dropdownElement = this.renderer.createElement('div');
|
|
199
|
+
this.renderer.addClass(this.dropdownElement, 'multiselect-custom-dropdown');
|
|
200
|
+
this.renderer.addClass(this.dropdownElement, 'multiselect-overlay-pane');
|
|
201
|
+
|
|
202
|
+
// Calculate position
|
|
203
|
+
const position = this.calculatePosition();
|
|
204
|
+
|
|
205
|
+
// Apply positioning styles
|
|
206
|
+
this.renderer.setStyle(this.dropdownElement, 'position', 'fixed');
|
|
207
|
+
this.renderer.setStyle(this.dropdownElement, 'left', position.left);
|
|
208
|
+
this.renderer.setStyle(this.dropdownElement, 'width', position.width);
|
|
209
|
+
this.renderer.setStyle(this.dropdownElement, 'max-height', position.maxHeight);
|
|
210
|
+
this.renderer.setStyle(this.dropdownElement, 'z-index', '100000', RendererStyleFlags2.Important);
|
|
211
|
+
|
|
212
|
+
if (position.top) {
|
|
213
|
+
this.renderer.setStyle(this.dropdownElement, 'top', position.top);
|
|
214
|
+
} else if (position.bottom) {
|
|
215
|
+
this.renderer.setStyle(this.dropdownElement, 'bottom', position.bottom);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Create dropdown content wrapper
|
|
219
|
+
const dropdownContent = this.renderer.createElement('div');
|
|
220
|
+
this.renderer.addClass(dropdownContent, 'multiselect-dropdown');
|
|
221
|
+
|
|
222
|
+
// Add options
|
|
223
|
+
const allOptions = this.getAllOptions();
|
|
224
|
+
allOptions.forEach(option => {
|
|
225
|
+
const optionElement = this.createOptionElement(option);
|
|
226
|
+
this.renderer.appendChild(dropdownContent, optionElement);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
this.renderer.appendChild(this.dropdownElement, dropdownContent);
|
|
230
|
+
this.renderer.appendChild(this.document.body, this.dropdownElement);
|
|
231
|
+
|
|
232
|
+
this.isOpen = true;
|
|
233
|
+
|
|
234
|
+
// Add scroll and resize listeners to reposition dropdown
|
|
235
|
+
this.addRepositionListeners();
|
|
236
|
+
|
|
237
|
+
// Trigger animation
|
|
238
|
+
setTimeout(() => {
|
|
239
|
+
if (this.dropdownElement) {
|
|
240
|
+
this.renderer.addClass(this.dropdownElement, 'multiselect-dropdown-open');
|
|
241
|
+
}
|
|
242
|
+
}, 10);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private calculatePosition(): DropdownPosition {
|
|
246
|
+
const triggerElement = this.trigger.nativeElement;
|
|
247
|
+
const rect = triggerElement.getBoundingClientRect();
|
|
248
|
+
|
|
249
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
250
|
+
const spaceAbove = rect.top;
|
|
251
|
+
const dropdownMaxHeight = 300;
|
|
252
|
+
const offsetY = 4;
|
|
253
|
+
|
|
254
|
+
const shouldOpenAbove = spaceBelow < 200 && spaceAbove > spaceBelow;
|
|
255
|
+
|
|
256
|
+
const position: DropdownPosition = {
|
|
257
|
+
left: `${rect.left}px`,
|
|
258
|
+
width: `${rect.width}px`,
|
|
259
|
+
maxHeight: `${dropdownMaxHeight}px`
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (shouldOpenAbove) {
|
|
263
|
+
position.bottom = `${window.innerHeight - rect.top + offsetY}px`;
|
|
264
|
+
} else {
|
|
265
|
+
position.top = `${rect.bottom + offsetY}px`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return position;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private addRepositionListeners(): void {
|
|
272
|
+
// Create handlers
|
|
273
|
+
const scrollHandler = () => this.updateDropdownPosition();
|
|
274
|
+
const resizeHandler = () => this.updateDropdownPosition();
|
|
275
|
+
|
|
276
|
+
// Add scroll listener to window with capture phase to catch all scroll events
|
|
277
|
+
window.addEventListener('scroll', scrollHandler, true);
|
|
278
|
+
this.scrollHandler = () => window.removeEventListener('scroll', scrollHandler, true);
|
|
279
|
+
|
|
280
|
+
// Add resize listener
|
|
281
|
+
window.addEventListener('resize', resizeHandler);
|
|
282
|
+
this.resizeHandler = () => window.removeEventListener('resize', resizeHandler);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private updateDropdownPosition(): void {
|
|
286
|
+
if (!this.dropdownElement || !this.trigger) return;
|
|
287
|
+
|
|
288
|
+
const position = this.calculatePosition();
|
|
289
|
+
|
|
290
|
+
// Update position styles
|
|
291
|
+
this.renderer.setStyle(this.dropdownElement, 'left', position.left);
|
|
292
|
+
this.renderer.setStyle(this.dropdownElement, 'width', position.width);
|
|
293
|
+
|
|
294
|
+
// Remove old top/bottom styles
|
|
295
|
+
this.renderer.removeStyle(this.dropdownElement, 'top');
|
|
296
|
+
this.renderer.removeStyle(this.dropdownElement, 'bottom');
|
|
297
|
+
|
|
298
|
+
// Apply new top/bottom position
|
|
299
|
+
if (position.top) {
|
|
300
|
+
this.renderer.setStyle(this.dropdownElement, 'top', position.top);
|
|
301
|
+
} else if (position.bottom) {
|
|
302
|
+
this.renderer.setStyle(this.dropdownElement, 'bottom', position.bottom);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private createOptionElement(option: any): HTMLElement {
|
|
307
|
+
const labelElement = this.renderer.createElement('label');
|
|
308
|
+
this.renderer.addClass(labelElement, 'multiselect-option');
|
|
309
|
+
|
|
310
|
+
const optionValue = this.getOptionValue(option);
|
|
311
|
+
const optionLabel = this.getOptionLabel(option);
|
|
312
|
+
const optionDisabled = this.isOptionDisabled(option);
|
|
313
|
+
|
|
314
|
+
if (this.isSelected(optionValue)) {
|
|
315
|
+
this.renderer.addClass(labelElement, 'selected');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (optionDisabled) {
|
|
319
|
+
this.renderer.addClass(labelElement, 'disabled');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Stop propagation on label click
|
|
323
|
+
this.renderer.listen(labelElement, 'click', (event: Event) => {
|
|
324
|
+
event.stopPropagation();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Checkbox container
|
|
328
|
+
const checkboxDiv = this.renderer.createElement('div');
|
|
329
|
+
this.renderer.addClass(checkboxDiv, 'option-checkbox');
|
|
330
|
+
|
|
331
|
+
// Hidden native checkbox
|
|
332
|
+
const checkbox = this.renderer.createElement('input');
|
|
333
|
+
this.renderer.setAttribute(checkbox, 'type', 'checkbox');
|
|
334
|
+
this.renderer.setProperty(checkbox, 'checked', this.isSelected(optionValue));
|
|
335
|
+
if (optionDisabled) {
|
|
336
|
+
this.renderer.setProperty(checkbox, 'disabled', true);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Custom checkbox span
|
|
340
|
+
const customCheckbox = this.renderer.createElement('span');
|
|
341
|
+
this.renderer.addClass(customCheckbox, 'checkbox-custom');
|
|
342
|
+
if (this.isSelected(optionValue)) {
|
|
343
|
+
this.renderer.addClass(customCheckbox, 'checked');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Check icon
|
|
347
|
+
if (this.isSelected(optionValue)) {
|
|
348
|
+
const svg = this.createCheckIcon();
|
|
349
|
+
this.renderer.appendChild(customCheckbox, svg);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Listen to change event
|
|
353
|
+
this.renderer.listen(checkbox, 'change', (event: Event) => {
|
|
354
|
+
event.stopPropagation();
|
|
355
|
+
|
|
356
|
+
// Toggle the value
|
|
357
|
+
this.toggleOption(option);
|
|
358
|
+
|
|
359
|
+
// Update visual state
|
|
360
|
+
const isNowSelected = this.isSelected(optionValue);
|
|
361
|
+
this.renderer.setProperty(checkbox, 'checked', isNowSelected);
|
|
362
|
+
|
|
363
|
+
if (isNowSelected) {
|
|
364
|
+
this.renderer.addClass(labelElement, 'selected');
|
|
365
|
+
this.renderer.addClass(customCheckbox, 'checked');
|
|
366
|
+
// Add check icon if not present
|
|
367
|
+
if (!customCheckbox.querySelector('.check-icon')) {
|
|
368
|
+
const svg = this.createCheckIcon();
|
|
369
|
+
this.renderer.appendChild(customCheckbox, svg);
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
this.renderer.removeClass(labelElement, 'selected');
|
|
373
|
+
this.renderer.removeClass(customCheckbox, 'checked');
|
|
374
|
+
// Remove check icon
|
|
375
|
+
const existingIcon = customCheckbox.querySelector('.check-icon');
|
|
376
|
+
if (existingIcon) {
|
|
377
|
+
this.renderer.removeChild(customCheckbox, existingIcon);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
this.renderer.appendChild(checkboxDiv, checkbox);
|
|
383
|
+
this.renderer.appendChild(checkboxDiv, customCheckbox);
|
|
384
|
+
this.renderer.appendChild(labelElement, checkboxDiv);
|
|
385
|
+
|
|
386
|
+
// Option text
|
|
387
|
+
const textSpan = this.renderer.createElement('span');
|
|
388
|
+
this.renderer.addClass(textSpan, 'option-text');
|
|
389
|
+
const text = this.renderer.createText(optionLabel);
|
|
390
|
+
this.renderer.appendChild(textSpan, text);
|
|
391
|
+
this.renderer.appendChild(labelElement, textSpan);
|
|
392
|
+
|
|
393
|
+
return labelElement;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private createCheckIcon(): SVGElement {
|
|
397
|
+
const svg = this.renderer.createElement('svg', 'svg');
|
|
398
|
+
this.renderer.addClass(svg, 'check-icon');
|
|
399
|
+
this.renderer.setAttribute(svg, 'width', '14');
|
|
400
|
+
this.renderer.setAttribute(svg, 'height', '10');
|
|
401
|
+
this.renderer.setAttribute(svg, 'viewBox', '0 0 10 8');
|
|
402
|
+
this.renderer.setAttribute(svg, 'fill', 'none');
|
|
403
|
+
this.renderer.setAttribute(svg, 'xmlns', 'http://www.w3.org/2000/svg');
|
|
404
|
+
|
|
405
|
+
const path = this.renderer.createElement('path', 'svg');
|
|
406
|
+
this.renderer.setAttribute(path, 'd', 'M9 1L3.5 6.5L1 4');
|
|
407
|
+
this.renderer.setAttribute(path, 'stroke', '#F59100');
|
|
408
|
+
this.renderer.setAttribute(path, 'stroke-width', '1.6666');
|
|
409
|
+
this.renderer.setAttribute(path, 'stroke-linecap', 'round');
|
|
410
|
+
this.renderer.setAttribute(path, 'stroke-linejoin', 'round');
|
|
411
|
+
|
|
412
|
+
this.renderer.appendChild(svg, path);
|
|
413
|
+
return svg;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
closeDropdown(): void {
|
|
417
|
+
this.destroyDropdown();
|
|
418
|
+
this.isOpen = false;
|
|
419
|
+
this.onTouched();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private destroyDropdown(): void {
|
|
423
|
+
// Remove listeners
|
|
424
|
+
if (this.scrollHandler) {
|
|
425
|
+
this.scrollHandler();
|
|
426
|
+
this.scrollHandler = null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (this.resizeHandler) {
|
|
430
|
+
this.resizeHandler();
|
|
431
|
+
this.resizeHandler = null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (this.dropdownElement) {
|
|
435
|
+
this.renderer.removeChild(this.document.body, this.dropdownElement);
|
|
436
|
+
this.dropdownElement = null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (this.backdropElement) {
|
|
440
|
+
this.renderer.removeChild(this.document.body, this.backdropElement);
|
|
441
|
+
this.backdropElement = null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
toggleOption(option: any): void {
|
|
446
|
+
if (this.isOptionDisabled(option)) return;
|
|
447
|
+
|
|
448
|
+
const optionValue = this.getOptionValue(option);
|
|
449
|
+
const index = this.value.indexOf(optionValue);
|
|
450
|
+
if (index > -1) {
|
|
451
|
+
this.value = this.value.filter(v => v !== optionValue);
|
|
452
|
+
} else {
|
|
453
|
+
this.value = [...this.value, optionValue];
|
|
454
|
+
}
|
|
455
|
+
this.onChange(this.value);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
isSelected(value: any): boolean {
|
|
459
|
+
return this.value.includes(value);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
getSelectedCount(): number {
|
|
463
|
+
return this.value.length;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
getSelectedLabels(): string {
|
|
467
|
+
const allOptions = this.getAllOptions();
|
|
468
|
+
const selectedOptions = allOptions.filter(opt => this.isSelected(this.getOptionValue(opt)));
|
|
469
|
+
return selectedOptions.map(opt => this.getOptionLabel(opt)).join(', ');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
writeValue(value: any): void {
|
|
473
|
+
this.value = Array.isArray(value) ? value : [];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
registerOnChange(fn: any): void {
|
|
477
|
+
this.onChange = fn;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
registerOnTouched(fn: any): void {
|
|
481
|
+
this.onTouched = fn;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
setDisabledState(isDisabled: boolean): void {
|
|
485
|
+
this.disabled = isDisabled;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Component, Input, HostBinding, ChangeDetectionStrategy, ElementRef } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: 'app-option',
|
|
6
|
+
standalone: true,
|
|
7
|
+
imports: [CommonModule],
|
|
8
|
+
template: `<ng-content></ng-content>`,
|
|
9
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10
|
+
host: {
|
|
11
|
+
'class': 'app-option',
|
|
12
|
+
'[class.disabled]': 'disabled',
|
|
13
|
+
'[class.selected]': 'selected'
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
export class OptionComponent {
|
|
17
|
+
@Input() value: any;
|
|
18
|
+
@Input() disabled: boolean = false;
|
|
19
|
+
|
|
20
|
+
@HostBinding('class.selected')
|
|
21
|
+
selected: boolean = false;
|
|
22
|
+
|
|
23
|
+
@HostBinding('class.app-option')
|
|
24
|
+
readonly optionClass = true;
|
|
25
|
+
|
|
26
|
+
// Internal property to store text content
|
|
27
|
+
private _viewValue: string = '';
|
|
28
|
+
|
|
29
|
+
get viewValue(): string {
|
|
30
|
+
return this._viewValue || this.getTextContent();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
set viewValue(value: string) {
|
|
34
|
+
this._viewValue = value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
constructor(private elementRef: ElementRef) {}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the text content from the element
|
|
41
|
+
*/
|
|
42
|
+
getTextContent(): string {
|
|
43
|
+
return this.elementRef.nativeElement.textContent?.trim() || '';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
.svu-phone-input {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
width: 100%;
|
|
5
|
+
margin-bottom: 6px;
|
|
6
|
+
padding: 0 4px;
|
|
7
|
+
|
|
8
|
+
&__label {
|
|
9
|
+
display: block;
|
|
10
|
+
margin-bottom: 0.5rem;
|
|
11
|
+
font-family: 'Quicksand', sans-serif;
|
|
12
|
+
font-weight: 500;
|
|
13
|
+
font-size: 14px;
|
|
14
|
+
color: #585D61;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
&__field {
|
|
18
|
+
position: relative;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: stretch;
|
|
21
|
+
min-height: 44px;
|
|
22
|
+
height: 44px;
|
|
23
|
+
border: 1px solid #EAECF5;
|
|
24
|
+
box-shadow: 0 1px 2px 0 #A1AFEC66;
|
|
25
|
+
border-radius: 8px;
|
|
26
|
+
transition: all 0.2s ease;
|
|
27
|
+
background: white;
|
|
28
|
+
|
|
29
|
+
&:hover {
|
|
30
|
+
border-color: #FBD399;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&:focus-within {
|
|
34
|
+
border-color: #FBD399;
|
|
35
|
+
box-shadow: 0 0 0 4px #FFE8C8, 0 1px 2px 0 #0A0D120D;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&__country {
|
|
40
|
+
flex: 0;
|
|
41
|
+
width: 120px;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: stretch;
|
|
44
|
+
min-width: 64px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
&__number {
|
|
48
|
+
flex: 1;
|
|
49
|
+
min-width: 0;
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: stretch;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Error state
|
|
55
|
+
&.svu-phone-input--error &__field {
|
|
56
|
+
border-color: #DC3545 !important;
|
|
57
|
+
|
|
58
|
+
&:focus-within {
|
|
59
|
+
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1) !important;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&__error {
|
|
64
|
+
color: #F04438;
|
|
65
|
+
font-size: 0.875rem;
|
|
66
|
+
margin-top: 0.25rem;
|
|
67
|
+
font-family: 'Quicksand', sans-serif;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Responsive adjustments
|
|
72
|
+
@media (max-width: 640px) {
|
|
73
|
+
.svu-phone-input {
|
|
74
|
+
&__country {
|
|
75
|
+
width: 100px;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Component, Input } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { InputComponent } from '../../atoms/input/input.component';
|
|
4
|
+
import { SelectComponent, SelectOption } from '../select/select.component';
|
|
5
|
+
|
|
6
|
+
export interface PhoneValue {
|
|
7
|
+
countryCode: string;
|
|
8
|
+
number: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@Component({
|
|
12
|
+
selector: 'app-phone-input',
|
|
13
|
+
standalone: true,
|
|
14
|
+
imports: [CommonModule, InputComponent, SelectComponent],
|
|
15
|
+
template: `
|
|
16
|
+
<div class="svu-phone-input" [class.svu-phone-input--error]="hasError">
|
|
17
|
+
<label *ngIf="label" class="svu-phone-input__label">
|
|
18
|
+
{{ label }}
|
|
19
|
+
</label>
|
|
20
|
+
|
|
21
|
+
<div class="svu-phone-input__field">
|
|
22
|
+
<div class="svu-phone-input__country">
|
|
23
|
+
<ng-content select="[countrySelect]"></ng-content>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="svu-phone-input__number">
|
|
27
|
+
<ng-content select="[phoneInput]"></ng-content>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<span *ngIf="hasError && errorMessage" class="svu-phone-input__error">
|
|
32
|
+
{{ errorMessage }}
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
`,
|
|
36
|
+
styleUrls: ['./phone-input.component.scss']
|
|
37
|
+
})
|
|
38
|
+
export class PhoneInputComponent {
|
|
39
|
+
@Input() label: string = 'Teléfono';
|
|
40
|
+
@Input() hasError: boolean = false;
|
|
41
|
+
@Input() errorMessage: string = '';
|
|
42
|
+
}
|
|
43
|
+
|