@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,155 @@
|
|
|
1
|
+
import { Component, Input, forwardRef, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR, ReactiveFormsModule, FormsModule } from '@angular/forms';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'app-textarea',
|
|
7
|
+
standalone: true,
|
|
8
|
+
imports: [CommonModule, ReactiveFormsModule, FormsModule],
|
|
9
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
10
|
+
template: `
|
|
11
|
+
<div class="textarea-wrapper">
|
|
12
|
+
<label *ngIf="label" [for]="id" class="textarea-label">{{ label }}</label>
|
|
13
|
+
<div class="textarea-container" [class.error]="hasError">
|
|
14
|
+
<textarea
|
|
15
|
+
[id]="id"
|
|
16
|
+
[placeholder]="placeholder"
|
|
17
|
+
[rows]="rows"
|
|
18
|
+
[disabled]="disabled"
|
|
19
|
+
[readonly]="readonly"
|
|
20
|
+
[attr.maxlength]="maxLength"
|
|
21
|
+
class="textarea-field"
|
|
22
|
+
[class.error]="hasError"
|
|
23
|
+
[value]="value"
|
|
24
|
+
(blur)="onBlur()"
|
|
25
|
+
(input)="onInputChange($event)"
|
|
26
|
+
></textarea>
|
|
27
|
+
</div>
|
|
28
|
+
<div *ngIf="hasError && errorMessage" class="field-error">
|
|
29
|
+
{{ errorMessage }}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
`,
|
|
33
|
+
styles: [`
|
|
34
|
+
.textarea-wrapper {
|
|
35
|
+
width: 100%;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
gap: 6px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.textarea-label {
|
|
42
|
+
font-family: 'Quicksand', sans-serif;
|
|
43
|
+
font-weight: 500;
|
|
44
|
+
font-size: 14px;
|
|
45
|
+
line-height: 20px;
|
|
46
|
+
color: #414651;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.textarea-container {
|
|
50
|
+
width: 100%;
|
|
51
|
+
|
|
52
|
+
&.error .textarea-field {
|
|
53
|
+
border-color: #F04438;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.textarea-field {
|
|
58
|
+
width: 100%;
|
|
59
|
+
padding: 10px 14px;
|
|
60
|
+
background: #FFFFFF;
|
|
61
|
+
border: 1px solid #D5D7DA;
|
|
62
|
+
border-radius: 8px;
|
|
63
|
+
font-family: 'Quicksand', sans-serif;
|
|
64
|
+
font-weight: 400;
|
|
65
|
+
font-size: 16px;
|
|
66
|
+
line-height: 24px;
|
|
67
|
+
color: #2E353A;
|
|
68
|
+
resize: vertical;
|
|
69
|
+
min-height: 100px;
|
|
70
|
+
transition: all 0.2s ease;
|
|
71
|
+
box-sizing: border-box;
|
|
72
|
+
|
|
73
|
+
&::placeholder {
|
|
74
|
+
color: #A4A7AE;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
&:focus {
|
|
78
|
+
outline: none;
|
|
79
|
+
border-color: #F59100;
|
|
80
|
+
box-shadow: 0 0 0 4px rgba(245, 145, 0, 0.1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
&:disabled {
|
|
84
|
+
background: #F9FAFB;
|
|
85
|
+
cursor: not-allowed;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&.error {
|
|
89
|
+
border-color: #F04438;
|
|
90
|
+
|
|
91
|
+
&:focus {
|
|
92
|
+
box-shadow: 0 0 0 4px rgba(240, 68, 56, 0.1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.field-error {
|
|
98
|
+
color: #F04438;
|
|
99
|
+
font-size: 0.875rem;
|
|
100
|
+
font-family: 'Quicksand', sans-serif;
|
|
101
|
+
}
|
|
102
|
+
`],
|
|
103
|
+
providers: [
|
|
104
|
+
{
|
|
105
|
+
provide: NG_VALUE_ACCESSOR,
|
|
106
|
+
useExisting: forwardRef(() => TextareaComponent),
|
|
107
|
+
multi: true
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
})
|
|
111
|
+
export class TextareaComponent implements ControlValueAccessor {
|
|
112
|
+
@Input() label = '';
|
|
113
|
+
@Input() id = '';
|
|
114
|
+
@Input() placeholder = '';
|
|
115
|
+
@Input() rows = 4;
|
|
116
|
+
@Input() maxLength?: number;
|
|
117
|
+
@Input() hasError = false;
|
|
118
|
+
@Input() errorMessage = '';
|
|
119
|
+
@Input() disabled = false;
|
|
120
|
+
@Input() readonly = false;
|
|
121
|
+
|
|
122
|
+
value = '';
|
|
123
|
+
onChange: any = () => {};
|
|
124
|
+
onTouched: any = () => {};
|
|
125
|
+
|
|
126
|
+
constructor(private cdr: ChangeDetectorRef) {}
|
|
127
|
+
|
|
128
|
+
writeValue(value: any): void {
|
|
129
|
+
this.value = value || '';
|
|
130
|
+
this.cdr.markForCheck();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
registerOnChange(fn: any): void {
|
|
134
|
+
this.onChange = fn;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
registerOnTouched(fn: any): void {
|
|
138
|
+
this.onTouched = fn;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
setDisabledState(isDisabled: boolean): void {
|
|
142
|
+
this.disabled = isDisabled;
|
|
143
|
+
this.cdr.markForCheck();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
onInputChange(event: Event): void {
|
|
147
|
+
const target = event.target as HTMLTextAreaElement;
|
|
148
|
+
this.value = target.value;
|
|
149
|
+
this.onChange(this.value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
onBlur(): void {
|
|
153
|
+
this.onTouched();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Directive, Input, ElementRef, OnInit, OnChanges, AfterViewInit, Inject, Optional } from '@angular/core';
|
|
2
|
+
import { ASSET_BASE_URL } from '../tokens/asset-base-url.token';
|
|
3
|
+
|
|
4
|
+
export type AssetSource = 'mf' | 'main';
|
|
5
|
+
|
|
6
|
+
@Directive({
|
|
7
|
+
selector: '[assetSource]',
|
|
8
|
+
standalone: true
|
|
9
|
+
})
|
|
10
|
+
export class AssetSourceDirective implements OnInit, OnChanges, AfterViewInit {
|
|
11
|
+
@Input() assetSource: AssetSource = 'mf';
|
|
12
|
+
@Input() assetPath?: string;
|
|
13
|
+
|
|
14
|
+
private originalSrc: string | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private el: ElementRef<HTMLImageElement>,
|
|
18
|
+
@Optional() @Inject(ASSET_BASE_URL) private assetBaseUrl: string | null
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
ngOnInit() {
|
|
22
|
+
this.captureOriginalSrc();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ngAfterViewInit() {
|
|
26
|
+
this.updateSrc();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ngOnChanges() {
|
|
30
|
+
this.updateSrc();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private captureOriginalSrc() {
|
|
34
|
+
if (this.el.nativeElement.tagName === 'IMG') {
|
|
35
|
+
this.originalSrc = this.el.nativeElement.getAttribute('src');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private updateSrc() {
|
|
40
|
+
const assetPath = this.assetPath || this.originalSrc;
|
|
41
|
+
|
|
42
|
+
if (!assetPath) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const baseUrl = this.getBaseUrl();
|
|
47
|
+
const fullPath = `${baseUrl}/${assetPath}`;
|
|
48
|
+
|
|
49
|
+
if (this.el.nativeElement.tagName === 'IMG') {
|
|
50
|
+
this.el.nativeElement.src = fullPath;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private getBaseUrl(): string {
|
|
55
|
+
switch (this.assetSource) {
|
|
56
|
+
case 'mf':
|
|
57
|
+
return this.assetBaseUrl ?? '';
|
|
58
|
+
case 'main':
|
|
59
|
+
return '/assets';
|
|
60
|
+
default:
|
|
61
|
+
return this.assetBaseUrl ?? '';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Directive, Input, ElementRef, OnInit, OnChanges, AfterViewInit, Inject, Optional } from '@angular/core';
|
|
2
|
+
import { ASSET_BASE_URL } from '../tokens/asset-base-url.token';
|
|
3
|
+
|
|
4
|
+
export type IconName =
|
|
5
|
+
'user'
|
|
6
|
+
| 'mail'
|
|
7
|
+
| 'build'
|
|
8
|
+
| 'map-pin'
|
|
9
|
+
| 'money'
|
|
10
|
+
| 'card'
|
|
11
|
+
| 'nav-arrow-up'
|
|
12
|
+
| 'nav-arrow-down'
|
|
13
|
+
| 'car'
|
|
14
|
+
| 'trash'
|
|
15
|
+
| 'hand-card'
|
|
16
|
+
| 'help-outlined'
|
|
17
|
+
| 'edit'
|
|
18
|
+
| 'shopping-cart';
|
|
19
|
+
|
|
20
|
+
@Directive({
|
|
21
|
+
selector: 'i[iconName]',
|
|
22
|
+
standalone: true
|
|
23
|
+
})
|
|
24
|
+
export class IconSourceDirective implements OnInit, OnChanges, AfterViewInit {
|
|
25
|
+
@Input() iconName!: IconName;
|
|
26
|
+
@Input() assetSource: 'mf' | 'main' = 'mf';
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private el: ElementRef<HTMLElement>,
|
|
30
|
+
@Optional() @Inject(ASSET_BASE_URL) private assetBaseUrl: string | null
|
|
31
|
+
) {
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ngOnInit() {
|
|
35
|
+
this.addIconClass();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ngAfterViewInit() {
|
|
39
|
+
this.setIconUrl();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
ngOnChanges() {
|
|
43
|
+
this.setIconUrl();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private addIconClass() {
|
|
47
|
+
if (this.iconName) {
|
|
48
|
+
this.el.nativeElement.classList.add(`icon-${this.iconName}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private setIconUrl() {
|
|
53
|
+
if (!this.iconName) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const baseUrl = this.getBaseUrl();
|
|
58
|
+
const iconUrl = `${baseUrl}/icons/${this.iconName}.svg`;
|
|
59
|
+
|
|
60
|
+
this.el.nativeElement.style.maskImage = `url('${iconUrl}')`;
|
|
61
|
+
this.el.nativeElement.style.webkitMaskImage = `url('${iconUrl}')`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private getBaseUrl(): string {
|
|
65
|
+
switch (this.assetSource) {
|
|
66
|
+
case 'mf':
|
|
67
|
+
return this.assetBaseUrl ?? '';
|
|
68
|
+
case 'main':
|
|
69
|
+
return '/assets';
|
|
70
|
+
default:
|
|
71
|
+
return this.assetBaseUrl ?? '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Directive, HostListener } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Directive({
|
|
4
|
+
selector: '[noLeadingSpace]',
|
|
5
|
+
standalone: true
|
|
6
|
+
})
|
|
7
|
+
export class NoLeadingSpaceDirective {
|
|
8
|
+
|
|
9
|
+
@HostListener('input', ['$event'])
|
|
10
|
+
onInput(event: any) {
|
|
11
|
+
const value = event.target.value;
|
|
12
|
+
|
|
13
|
+
// Elimina espacios al inicio
|
|
14
|
+
if (value.startsWith(' ')) {
|
|
15
|
+
event.target.value = value.trimStart();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Directive, HostListener } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Directive({
|
|
4
|
+
selector: '[onlyLetters]',
|
|
5
|
+
standalone: true
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
export class OnlyLettersDirective {
|
|
9
|
+
|
|
10
|
+
private regex = /^[A-Za-zÁÉÍÓÚÜÑáéíóúüñ' -]*$/;
|
|
11
|
+
|
|
12
|
+
@HostListener('input', ['$event'])
|
|
13
|
+
onInput(event: any) {
|
|
14
|
+
const value: string = event.target.value;
|
|
15
|
+
|
|
16
|
+
event.target.value = value
|
|
17
|
+
.split('')
|
|
18
|
+
.filter(char => this.regex.test(char))
|
|
19
|
+
.join('');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Directive, HostListener } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Directive({
|
|
4
|
+
selector: '[onlyNumber]',
|
|
5
|
+
standalone: true
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
export class OnlyNumberDirective {
|
|
9
|
+
@HostListener('input', ['$event'])
|
|
10
|
+
onInput(event: any) {
|
|
11
|
+
const value = event.target.value;
|
|
12
|
+
event.target.value = value.replace(/\D/g, '');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Directive, HostListener, Input, Optional, Self } from '@angular/core';
|
|
2
|
+
import { NgControl } from '@angular/forms';
|
|
3
|
+
|
|
4
|
+
@Directive({
|
|
5
|
+
selector: '[rfcValidator]',
|
|
6
|
+
standalone: true
|
|
7
|
+
})
|
|
8
|
+
export class RfcDirective {
|
|
9
|
+
|
|
10
|
+
@Input() tipoContribuyente: string | null = null;
|
|
11
|
+
|
|
12
|
+
constructor(@Optional() @Self() private ngControl: NgControl | null) {
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Función para obtener el límite según el tipo de contribuyente
|
|
16
|
+
private getMaxLength(): number {
|
|
17
|
+
if (this.tipoContribuyente === 'fisica') return 13;
|
|
18
|
+
if (this.tipoContribuyente === 'moral') return 12;
|
|
19
|
+
return 13; // Default por si no llega el valor
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@HostListener('input', ['$event'])
|
|
23
|
+
onInput(event: Event) {
|
|
24
|
+
const input = event.target as HTMLInputElement;
|
|
25
|
+
let value = input.value;
|
|
26
|
+
|
|
27
|
+
// Solo letras y números
|
|
28
|
+
value = value.replace(/[^a-zA-Z0-9]/g, '');
|
|
29
|
+
|
|
30
|
+
// Mayúsculas
|
|
31
|
+
value = value.toUpperCase();
|
|
32
|
+
|
|
33
|
+
// Aplicar límite dinámico
|
|
34
|
+
const max = this.getMaxLength();
|
|
35
|
+
if (value.length > max) {
|
|
36
|
+
value = value.substring(0, max);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.ngControl?.control?.setValue(value, { emitEvent: false });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@HostListener('paste', ['$event'])
|
|
43
|
+
onPaste(event: ClipboardEvent) {
|
|
44
|
+
const pasted = event.clipboardData?.getData('text') || '';
|
|
45
|
+
const max = this.getMaxLength();
|
|
46
|
+
|
|
47
|
+
// Bloquear pegado con caracteres inválidos
|
|
48
|
+
if (/[^a-zA-Z0-9]/.test(pasted)) {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Bloquear pegado si excede el máximo
|
|
54
|
+
if (pasted.length > max) {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Directive,
|
|
3
|
+
Input,
|
|
4
|
+
ElementRef,
|
|
5
|
+
HostListener,
|
|
6
|
+
Renderer2,
|
|
7
|
+
OnDestroy,
|
|
8
|
+
HostBinding
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
|
|
11
|
+
type TooltipPosition = 'top' | 'bottom' | 'left' | 'right' | 'auto';
|
|
12
|
+
|
|
13
|
+
@Directive({
|
|
14
|
+
selector: '[appTooltip]',
|
|
15
|
+
standalone: true
|
|
16
|
+
})
|
|
17
|
+
export class TooltipDirective implements OnDestroy {
|
|
18
|
+
@Input('appTooltip') tooltipText = '';
|
|
19
|
+
@Input() tooltipPosition: TooltipPosition = 'auto';
|
|
20
|
+
@Input() tooltipDelay = 300;
|
|
21
|
+
@Input() tooltipMaxWidth = 280;
|
|
22
|
+
@Input() tooltipClass = '';
|
|
23
|
+
|
|
24
|
+
@HostBinding('style.cursor') cursor = 'help';
|
|
25
|
+
|
|
26
|
+
private tooltipElement: HTMLElement | null = null;
|
|
27
|
+
private showTimeout?: number;
|
|
28
|
+
private hideTimeout?: number;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private el: ElementRef,
|
|
32
|
+
private renderer: Renderer2
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
@HostListener('mouseenter')
|
|
36
|
+
onMouseEnter(): void {
|
|
37
|
+
if (this.hideTimeout) {
|
|
38
|
+
clearTimeout(this.hideTimeout);
|
|
39
|
+
this.hideTimeout = undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.showTimeout = window.setTimeout(() => {
|
|
43
|
+
this.showTooltip();
|
|
44
|
+
}, this.tooltipDelay);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@HostListener('mouseleave')
|
|
48
|
+
onMouseLeave(): void {
|
|
49
|
+
if (this.showTimeout) {
|
|
50
|
+
clearTimeout(this.showTimeout);
|
|
51
|
+
this.showTimeout = undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.hideTimeout = window.setTimeout(() => {
|
|
55
|
+
this.hideTooltip();
|
|
56
|
+
}, 100);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@HostListener('click')
|
|
60
|
+
onClick(): void {
|
|
61
|
+
// Allow click events to propagate
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private showTooltip(): void {
|
|
65
|
+
if (!this.tooltipText || this.tooltipElement) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create tooltip element
|
|
70
|
+
this.tooltipElement = this.renderer.createElement('div');
|
|
71
|
+
this.renderer.addClass(this.tooltipElement, 'app-tooltip');
|
|
72
|
+
|
|
73
|
+
if (this.tooltipClass) {
|
|
74
|
+
this.renderer.addClass(this.tooltipElement, this.tooltipClass);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create tooltip content
|
|
78
|
+
const tooltipContent = this.renderer.createElement('div');
|
|
79
|
+
this.renderer.addClass(tooltipContent, 'app-tooltip-content');
|
|
80
|
+
this.renderer.setStyle(tooltipContent, 'max-width', `${this.tooltipMaxWidth}px`);
|
|
81
|
+
|
|
82
|
+
const textNode = this.renderer.createText(this.tooltipText);
|
|
83
|
+
this.renderer.appendChild(tooltipContent, textNode);
|
|
84
|
+
this.renderer.appendChild(this.tooltipElement, tooltipContent);
|
|
85
|
+
|
|
86
|
+
// Create arrow element
|
|
87
|
+
const arrow = this.renderer.createElement('div');
|
|
88
|
+
this.renderer.addClass(arrow, 'app-tooltip-arrow');
|
|
89
|
+
this.renderer.appendChild(this.tooltipElement, arrow);
|
|
90
|
+
|
|
91
|
+
// Add to body
|
|
92
|
+
this.renderer.appendChild(document.body, this.tooltipElement);
|
|
93
|
+
|
|
94
|
+
// Position tooltip
|
|
95
|
+
this.positionTooltip();
|
|
96
|
+
|
|
97
|
+
// Trigger animation
|
|
98
|
+
requestAnimationFrame(() => {
|
|
99
|
+
if (this.tooltipElement) {
|
|
100
|
+
this.renderer.addClass(this.tooltipElement, 'app-tooltip-show');
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private hideTooltip(): void {
|
|
106
|
+
if (!this.tooltipElement) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.renderer.removeClass(this.tooltipElement, 'app-tooltip-show');
|
|
111
|
+
|
|
112
|
+
setTimeout(() => {
|
|
113
|
+
if (this.tooltipElement) {
|
|
114
|
+
this.renderer.removeChild(document.body, this.tooltipElement);
|
|
115
|
+
this.tooltipElement = null;
|
|
116
|
+
}
|
|
117
|
+
}, 200);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private positionTooltip(): void {
|
|
121
|
+
if (!this.tooltipElement) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const hostRect = this.el.nativeElement.getBoundingClientRect();
|
|
126
|
+
const tooltipRect = this.tooltipElement.getBoundingClientRect();
|
|
127
|
+
const scrollX = window.scrollX || window.pageXOffset;
|
|
128
|
+
const scrollY = window.scrollY || window.pageYOffset;
|
|
129
|
+
const gap = 16; // Space between element and tooltip
|
|
130
|
+
|
|
131
|
+
// Calculate available space in each direction
|
|
132
|
+
const spaceTop = hostRect.top;
|
|
133
|
+
const spaceBottom = window.innerHeight - hostRect.bottom;
|
|
134
|
+
const spaceLeft = hostRect.left;
|
|
135
|
+
const spaceRight = window.innerWidth - hostRect.right;
|
|
136
|
+
|
|
137
|
+
let position = this.tooltipPosition;
|
|
138
|
+
let top = 0;
|
|
139
|
+
let left = 0;
|
|
140
|
+
|
|
141
|
+
// Auto-detect best position
|
|
142
|
+
if (position === 'auto') {
|
|
143
|
+
if (spaceTop >= tooltipRect.height + gap) {
|
|
144
|
+
position = 'top';
|
|
145
|
+
} else if (spaceBottom >= tooltipRect.height + gap) {
|
|
146
|
+
position = 'bottom';
|
|
147
|
+
} else if (spaceRight >= tooltipRect.width + gap) {
|
|
148
|
+
position = 'right';
|
|
149
|
+
} else if (spaceLeft >= tooltipRect.width + gap) {
|
|
150
|
+
position = 'left';
|
|
151
|
+
} else {
|
|
152
|
+
// Default to top if no space is sufficient
|
|
153
|
+
position = 'top';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Calculate position based on detected/specified position
|
|
158
|
+
switch (position) {
|
|
159
|
+
case 'top':
|
|
160
|
+
top = hostRect.top + scrollY - tooltipRect.height - gap;
|
|
161
|
+
left = hostRect.left + scrollX + (hostRect.width / 2) - (tooltipRect.width / 2);
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
case 'bottom':
|
|
165
|
+
top = hostRect.bottom + scrollY + gap;
|
|
166
|
+
left = hostRect.left + scrollX + (hostRect.width / 2) - (tooltipRect.width / 2);
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case 'left':
|
|
170
|
+
top = hostRect.top + scrollY + (hostRect.height / 2) - (tooltipRect.height / 2);
|
|
171
|
+
left = hostRect.left + scrollX - tooltipRect.width - gap;
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case 'right':
|
|
175
|
+
top = hostRect.top + scrollY + (hostRect.height / 2) - (tooltipRect.height / 2);
|
|
176
|
+
left = hostRect.right + scrollX + gap;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Adjust horizontal position to keep tooltip within viewport
|
|
181
|
+
if (position === 'top' || position === 'bottom') {
|
|
182
|
+
const minLeft = scrollX + 10;
|
|
183
|
+
const maxLeft = scrollX + window.innerWidth - tooltipRect.width - 10;
|
|
184
|
+
left = Math.max(minLeft, Math.min(left, maxLeft));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Adjust vertical position to keep tooltip within viewport
|
|
188
|
+
if (position === 'left' || position === 'right') {
|
|
189
|
+
const minTop = scrollY + 10;
|
|
190
|
+
const maxTop = scrollY + window.innerHeight - tooltipRect.height - 10;
|
|
191
|
+
top = Math.max(minTop, Math.min(top, maxTop));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Apply position
|
|
195
|
+
this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`);
|
|
196
|
+
this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`);
|
|
197
|
+
|
|
198
|
+
// Add position class for arrow styling
|
|
199
|
+
this.renderer.addClass(this.tooltipElement, `app-tooltip-${position}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
ngOnDestroy(): void {
|
|
203
|
+
if (this.showTimeout) {
|
|
204
|
+
clearTimeout(this.showTimeout);
|
|
205
|
+
}
|
|
206
|
+
if (this.hideTimeout) {
|
|
207
|
+
clearTimeout(this.hideTimeout);
|
|
208
|
+
}
|
|
209
|
+
this.hideTooltip();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type SnackbarType = 'success' | 'error' | 'warning' | 'info';
|
|
2
|
+
export type SnackbarPosition = 'top-right' | 'top-left' | 'top-center' | 'bottom-right' | 'bottom-left' | 'bottom-center';
|
|
3
|
+
|
|
4
|
+
export interface SnackbarConfig {
|
|
5
|
+
message: string;
|
|
6
|
+
type: SnackbarType;
|
|
7
|
+
duration?: number;
|
|
8
|
+
position?: SnackbarPosition;
|
|
9
|
+
action?: string;
|
|
10
|
+
actionCallback?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SnackbarData extends SnackbarConfig {
|
|
14
|
+
id: string;
|
|
15
|
+
position: SnackbarPosition;
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<div class="copy-input-wrapper">
|
|
2
|
+
|
|
3
|
+
<!-- Tooltip -->
|
|
4
|
+
<div
|
|
5
|
+
class="copy-tooltip"
|
|
6
|
+
*ngIf="isCopied"
|
|
7
|
+
>
|
|
8
|
+
Copiado
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<!-- Componente -->
|
|
12
|
+
<div
|
|
13
|
+
class="copy-input"
|
|
14
|
+
(click)="copyToClipboard()"
|
|
15
|
+
tabindex="0"
|
|
16
|
+
role="button"
|
|
17
|
+
[attr.aria-label]="ariaLabel"
|
|
18
|
+
(keydown.enter)="copyToClipboard()"
|
|
19
|
+
>
|
|
20
|
+
<i
|
|
21
|
+
class="copy-input__icon"
|
|
22
|
+
[ngClass]="isCopied ? copiedIcon : icon">
|
|
23
|
+
</i>
|
|
24
|
+
|
|
25
|
+
<span class="copy-input__text">
|
|
26
|
+
{{ value }}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|