fix scss
This commit is contained in:
@@ -31,6 +31,8 @@ const TRANSLATIONS: TranslationObject = {
|
|||||||
max: 'Máx',
|
max: 'Máx',
|
||||||
save: 'Guardar',
|
save: 'Guardar',
|
||||||
reset: 'Borrar',
|
reset: 'Borrar',
|
||||||
|
search: 'Buscar…',
|
||||||
|
noResults: 'Sin resultados',
|
||||||
validator: {
|
validator: {
|
||||||
requiredField: 'Este campo es obligatorio',
|
requiredField: 'Este campo es obligatorio',
|
||||||
onlyNumbers: 'Sólo se admiten números',
|
onlyNumbers: 'Sólo se admiten números',
|
||||||
|
|||||||
@@ -81,6 +81,28 @@
|
|||||||
<pi-label>Fecha de nacimiento</pi-label>
|
<pi-label>Fecha de nacimiento</pi-label>
|
||||||
</pi-date-field>
|
</pi-date-field>
|
||||||
|
|
||||||
|
<pi-select-field [formField]="f.language" [options]="languages">
|
||||||
|
<pi-label>Idioma</pi-label>
|
||||||
|
</pi-select-field>
|
||||||
|
|
||||||
|
<pi-searchable-select-field
|
||||||
|
[formField]="f.country2"
|
||||||
|
[options]="countryOptions"
|
||||||
|
>
|
||||||
|
<pi-label>País</pi-label>
|
||||||
|
</pi-searchable-select-field>
|
||||||
|
|
||||||
|
<pi-searchable-multi-select-field
|
||||||
|
[formField]="f.visited"
|
||||||
|
[options]="countryOptions"
|
||||||
|
>
|
||||||
|
<pi-label>Países visitados</pi-label>
|
||||||
|
</pi-searchable-multi-select-field>
|
||||||
|
|
||||||
|
<pi-multi-select-field [formField]="f.skills" [options]="skillOptions">
|
||||||
|
<pi-label>Conocimientos</pi-label>
|
||||||
|
</pi-multi-select-field>
|
||||||
|
|
||||||
<pi-radio-group
|
<pi-radio-group
|
||||||
[formField]="f.shipping"
|
[formField]="f.shipping"
|
||||||
[options]="shippingOptions"
|
[options]="shippingOptions"
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ import {
|
|||||||
PiSelectCarouselComponent,
|
PiSelectCarouselComponent,
|
||||||
PiSelectComponent,
|
PiSelectComponent,
|
||||||
PiSelectMultipleComponent,
|
PiSelectMultipleComponent,
|
||||||
|
PiSearchableMultiSelectFieldComponent,
|
||||||
|
PiSearchableSelectFieldComponent,
|
||||||
|
PiSelectFieldComponent,
|
||||||
|
PiSelectFieldOption,
|
||||||
|
PiMultiSelectFieldComponent,
|
||||||
PiSelectOptionComponent,
|
PiSelectOptionComponent,
|
||||||
PiSelectPeriodComponent,
|
PiSelectPeriodComponent,
|
||||||
PiSlideToggleComponent,
|
PiSlideToggleComponent,
|
||||||
@@ -148,6 +153,10 @@ interface DemoModel {
|
|||||||
plan: SelectOptionValue | null;
|
plan: SelectOptionValue | null;
|
||||||
terms: boolean;
|
terms: boolean;
|
||||||
shipping: string | null;
|
shipping: string | null;
|
||||||
|
language: string | null;
|
||||||
|
country2: string | null;
|
||||||
|
visited: string[];
|
||||||
|
skills: string[];
|
||||||
birthDate: Date | null;
|
birthDate: Date | null;
|
||||||
attachments: File[];
|
attachments: File[];
|
||||||
}
|
}
|
||||||
@@ -174,7 +183,7 @@ interface DemoModel {
|
|||||||
PiSmallButtonDirective,
|
PiSmallButtonDirective,
|
||||||
PiW100ButtonDirective,
|
PiW100ButtonDirective,
|
||||||
PiRadioButtonComponent,
|
PiRadioButtonComponent,
|
||||||
PiSwitchButtonComponent,
|
PiSwitchButtonComponent,
|
||||||
PiToggleButtonComponent,
|
PiToggleButtonComponent,
|
||||||
// Contenido
|
// Contenido
|
||||||
PiIconComponent,
|
PiIconComponent,
|
||||||
@@ -240,6 +249,10 @@ interface DemoModel {
|
|||||||
PiSelectPeriodComponent,
|
PiSelectPeriodComponent,
|
||||||
PiSelectCarouselComponent,
|
PiSelectCarouselComponent,
|
||||||
PiSelectOptionComponent,
|
PiSelectOptionComponent,
|
||||||
|
PiSelectFieldComponent,
|
||||||
|
PiMultiSelectFieldComponent,
|
||||||
|
PiSearchableSelectFieldComponent,
|
||||||
|
PiSearchableMultiSelectFieldComponent,
|
||||||
// Datos
|
// Datos
|
||||||
PiDataGridComponent,
|
PiDataGridComponent,
|
||||||
PiCellDirective,
|
PiCellDirective,
|
||||||
@@ -285,6 +298,10 @@ export class CatalogPage {
|
|||||||
plan: null,
|
plan: null,
|
||||||
terms: false,
|
terms: false,
|
||||||
shipping: null,
|
shipping: null,
|
||||||
|
language: null,
|
||||||
|
country2: null,
|
||||||
|
visited: [],
|
||||||
|
skills: [],
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
});
|
});
|
||||||
@@ -312,6 +329,71 @@ export class CatalogPage {
|
|||||||
{ id: 4, name: 'Ventas' },
|
{ id: 4, name: 'Ventas' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected readonly languages: PiSelectFieldOption<string>[] = [
|
||||||
|
{ value: 'es', label: 'Español' },
|
||||||
|
{ value: 'en', label: 'Inglés' },
|
||||||
|
{ value: 'fr', label: 'Francés' },
|
||||||
|
{ value: 'de', label: 'Alemán', disabled: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Lista larga a propósito: es donde el buscador aporta algo. */
|
||||||
|
protected readonly countryOptions: PiSelectFieldOption<string>[] = [
|
||||||
|
'Alemania',
|
||||||
|
'Andorra',
|
||||||
|
'Argentina',
|
||||||
|
'Australia',
|
||||||
|
'Austria',
|
||||||
|
'Bélgica',
|
||||||
|
'Bolivia',
|
||||||
|
'Brasil',
|
||||||
|
'Canadá',
|
||||||
|
'Chile',
|
||||||
|
'China',
|
||||||
|
'Colombia',
|
||||||
|
'Corea del Sur',
|
||||||
|
'Costa Rica',
|
||||||
|
'Cuba',
|
||||||
|
'Dinamarca',
|
||||||
|
'Ecuador',
|
||||||
|
'Egipto',
|
||||||
|
'España',
|
||||||
|
'Estados Unidos',
|
||||||
|
'Finlandia',
|
||||||
|
'Francia',
|
||||||
|
'Grecia',
|
||||||
|
'Guatemala',
|
||||||
|
'Honduras',
|
||||||
|
'Hungría',
|
||||||
|
'India',
|
||||||
|
'Irlanda',
|
||||||
|
'Islandia',
|
||||||
|
'Italia',
|
||||||
|
'Japón',
|
||||||
|
'Marruecos',
|
||||||
|
'México',
|
||||||
|
'Noruega',
|
||||||
|
'Países Bajos',
|
||||||
|
'Panamá',
|
||||||
|
'Paraguay',
|
||||||
|
'Perú',
|
||||||
|
'Polonia',
|
||||||
|
'Portugal',
|
||||||
|
'Reino Unido',
|
||||||
|
'República Dominicana',
|
||||||
|
'Rumanía',
|
||||||
|
'Suecia',
|
||||||
|
'Suiza',
|
||||||
|
'Uruguay',
|
||||||
|
'Venezuela',
|
||||||
|
].map((name) => ({ value: name.toLowerCase(), label: name }));
|
||||||
|
|
||||||
|
protected readonly skillOptions: PiSelectFieldOption<string>[] = [
|
||||||
|
{ value: 'angular', label: 'Angular' },
|
||||||
|
{ value: 'rxjs', label: 'RxJS' },
|
||||||
|
{ value: 'scss', label: 'SCSS' },
|
||||||
|
{ value: 'a11y', label: 'Accesibilidad' },
|
||||||
|
];
|
||||||
|
|
||||||
protected readonly plans = [
|
protected readonly plans = [
|
||||||
{ id: 1, name: 'Básico', description: '9 €/mes' },
|
{ id: 1, name: 'Básico', description: '9 €/mes' },
|
||||||
{ id: 2, name: 'Pro', description: '29 €/mes' },
|
{ id: 2, name: 'Pro', description: '29 €/mes' },
|
||||||
|
|||||||
@@ -47,3 +47,35 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Superficie del panel -----------------------------------------------
|
||||||
|
//
|
||||||
|
// El calendario venía con el gris y el radio de Material de serie. Se
|
||||||
|
// reconstruye con los mismos tokens que el panel del select y el resto de
|
||||||
|
// superficies de la librería.
|
||||||
|
.cdk-overlay-pane .mat-datepicker-content,
|
||||||
|
.mat-datepicker-content {
|
||||||
|
border-radius: var(--pi-theme-panel-radius);
|
||||||
|
border: 1px solid var(--pi-theme-field-border-color);
|
||||||
|
background-color: var(--pi-theme-field-surface);
|
||||||
|
box-shadow: var(--pi-box-shadow-panel);
|
||||||
|
|
||||||
|
.mat-calendar-body-label,
|
||||||
|
.mat-calendar-table-header th {
|
||||||
|
color: var(--pi-color-grey-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Día de hoy y día seleccionado, con los colores de marca.
|
||||||
|
.mat-calendar-body-today:not(.mat-calendar-body-selected) {
|
||||||
|
border-color: var(--pi-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-calendar-body-selected {
|
||||||
|
background-color: var(--pi-color-primary);
|
||||||
|
color: var(--pi-color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-calendar-body-cell-content:hover:not(.mat-calendar-body-selected) {
|
||||||
|
background-color: var(--pi-color-primary-50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
163
libs/ui/src/lib/styles/common/vendors/_mat-select.scss
vendored
Normal file
163
libs/ui/src/lib/styles/common/vendors/_mat-select.scss
vendored
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// Select
|
||||||
|
//
|
||||||
|
// Cubre lo que usan `pi-select-field` y `pi-multi-select-field`. Son dos cosas
|
||||||
|
// distintas y por eso están separadas:
|
||||||
|
//
|
||||||
|
// 1. El *trigger*, dentro del campo. `_mat-form-field.scss` estira el `<input>`
|
||||||
|
// para que llene el alto de la caja, pero no hay equivalente para el select:
|
||||||
|
// su trigger mide lo que su contenido (24px) y quedaba pegado arriba, con el
|
||||||
|
// texto y la flecha ~10px por encima del centro.
|
||||||
|
//
|
||||||
|
// 2. El *panel*, que se monta en el overlay y por tanto fuera del campo. Venía
|
||||||
|
// con el gris y el radio de 4px de Material de serie, sin relación con la
|
||||||
|
// librería. Se reconstruye con los mismos tokens que el resto de superficies.
|
||||||
|
|
||||||
|
// --- 1. Trigger: centrado vertical dentro del campo ----------------------
|
||||||
|
.pi-field {
|
||||||
|
// El `<mat-select>` es `inline-block` y mide lo que su contenido (24px)
|
||||||
|
// dentro de un infix de 48px. Estirarlo es lo que de verdad centra: poner
|
||||||
|
// `height: 100%` solo en el trigger no servía, porque ese 100% se resolvía
|
||||||
|
// contra los 24px del propio select.
|
||||||
|
.mat-mdc-select {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-select-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// El valor deja de ser un bloque suelto arriba y se centra con el trigger.
|
||||||
|
.mat-mdc-select-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// La flecha va dentro del infix, mientras que los iconos sufijo del resto de
|
||||||
|
// campos van fuera de él. Entre ambos anclajes hay 28px: los 12 del
|
||||||
|
// `translateX(-0.75rem)` que `_mat-form-field.scss` aplica al infix, más la
|
||||||
|
// diferencia de padding entre el flex y el wrapper. Se posiciona respecto al
|
||||||
|
// infix (que ya es `position: relative`) sacándola esos 28px, para que quede
|
||||||
|
// al mismo inset que los demás iconos.
|
||||||
|
.mat-mdc-select-arrow-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
right: -1.75rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Material dimensiona `.mat-mdc-select-arrow` para el triángulo CSS antiguo
|
||||||
|
// (10x5) pero dentro pinta un SVG de 24x24, que se salía 7px por cada lado.
|
||||||
|
// Igualando la caja al glifo deja de desbordar y queda a la misma altura y
|
||||||
|
// color que los iconos sufijo del resto de campos.
|
||||||
|
.mat-mdc-select-arrow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: var(--pi-spacing-large);
|
||||||
|
height: var(--pi-spacing-large);
|
||||||
|
color: var(--pi-color-grey-300);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: static;
|
||||||
|
transform: none;
|
||||||
|
width: var(--pi-spacing-large);
|
||||||
|
height: var(--pi-spacing-large);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mismos estados que los afijos de icono en `_mat-form-field.scss`.
|
||||||
|
&.is-focused .mat-mdc-select-arrow {
|
||||||
|
color: var(--pi-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estados del propio campo, en línea con `_mat-form-field.scss`.
|
||||||
|
.mat-mdc-select-placeholder {
|
||||||
|
color: var(--pi-color-grey-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-error .mat-mdc-select-arrow {
|
||||||
|
color: var(--pi-color-warn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. Panel desplegable ------------------------------------------------
|
||||||
|
//
|
||||||
|
// Se ancla a `.cdk-overlay-pane`, no a `.cdk-overlay-container`: esta versión
|
||||||
|
// de Material monta el panel con la API de popover *dentro* del propio
|
||||||
|
// `<mat-select>`, así que el contenedor global no está en su ascendencia.
|
||||||
|
.cdk-overlay-pane {
|
||||||
|
.mat-mdc-select-panel {
|
||||||
|
// Radio de superficie, no el del campo: en el tema alternativo el campo es
|
||||||
|
// una pastilla (999px) y un panel con ese radio quedaría deforme.
|
||||||
|
border-radius: var(--pi-theme-panel-radius);
|
||||||
|
border: 1px solid var(--pi-theme-field-border-color);
|
||||||
|
background-color: var(--pi-theme-field-surface);
|
||||||
|
box-shadow: var(--pi-box-shadow-panel);
|
||||||
|
padding: var(--pi-spacing-xx-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscador de `pi-searchable-select-field`. Va pegado arriba para que siga
|
||||||
|
// visible al desplazar una lista larga.
|
||||||
|
.pi-select-search {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--pi-spacing-xx-small);
|
||||||
|
padding: var(--pi-spacing-xx-small) var(--pi-spacing-small);
|
||||||
|
margin-bottom: var(--pi-spacing-xx-small);
|
||||||
|
border-bottom: 1px solid var(--pi-theme-field-border-color);
|
||||||
|
background-color: var(--pi-theme-field-surface);
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--pi-color-black);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--pi-font-size-small);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--pi-color-grey-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__empty {
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--pi-spacing-small);
|
||||||
|
color: var(--pi-color-grey-300);
|
||||||
|
font-size: var(--pi-font-size-small);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-mdc-option {
|
||||||
|
border-radius: var(--pi-border-radius-small);
|
||||||
|
min-height: var(--pi-spacing-xxx-large);
|
||||||
|
|
||||||
|
// Material pinta hover/foco/selección con una capa de estado propia, no
|
||||||
|
// con `background-color`; por eso se ajustan sus tokens en vez de
|
||||||
|
// sobreescribir el fondo, que quedaba tapado por esa capa.
|
||||||
|
--mat-option-hover-state-layer-color: var(--pi-color-primary-50);
|
||||||
|
--mat-option-focus-state-layer-color: var(--pi-color-primary-50);
|
||||||
|
--mat-option-selected-state-layer-color: var(--pi-color-primary-50);
|
||||||
|
--mat-option-label-text-color: var(--pi-color-black);
|
||||||
|
--mat-option-selected-state-label-text-color: var(--pi-color-primary-900);
|
||||||
|
|
||||||
|
// Opción resaltada por teclado en `pi-searchable-select-field`, que lleva
|
||||||
|
// su propio índice activo (ver el componente).
|
||||||
|
&.is-active {
|
||||||
|
background-color: var(--pi-color-primary-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mdc-list-item--disabled {
|
||||||
|
color: var(--pi-color-grey-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
@forward 'mat-progress-bar';
|
@forward 'mat-progress-bar';
|
||||||
@forward 'mat-progress-spinner';
|
@forward 'mat-progress-spinner';
|
||||||
@forward 'mat-radio-button';
|
@forward 'mat-radio-button';
|
||||||
|
@forward 'mat-select';
|
||||||
@forward 'mat-slider';
|
@forward 'mat-slider';
|
||||||
@forward 'mat-snackbar';
|
@forward 'mat-snackbar';
|
||||||
@forward 'mat-tooltip';
|
@forward 'mat-tooltip';
|
||||||
|
|||||||
@@ -95,9 +95,3 @@
|
|||||||
.pi-shell__alternative {
|
.pi-shell__alternative {
|
||||||
@include alternativeThemeTokens;
|
@include alternativeThemeTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Los overlays de Material se montan fuera del shell: se les propaga el tema
|
|
||||||
// marcando el contenedor (lo hace la app al cambiar de tema).
|
|
||||||
.cdk-overlay-container.pi-shell__alternative {
|
|
||||||
@include alternativeThemeTokens;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,9 +6,13 @@
|
|||||||
// tokens (ver `_alternative.scss`).
|
// tokens (ver `_alternative.scss`).
|
||||||
//
|
//
|
||||||
// Los valores viven en `.pi-shell` (no en `:root`) para que puedan convivir
|
// Los valores viven en `.pi-shell` (no en `:root`) para que puedan convivir
|
||||||
// varios temas en la misma página, y se repiten en `.cdk-overlay-container`
|
// varios temas en la misma página.
|
||||||
// porque los overlays de Material (diálogos, snackbars, menús) se montan fuera
|
//
|
||||||
// del árbol del shell y no heredarían nada.
|
// No hace falta repetirlos en `.cdk-overlay-container`: ese contenedor lo
|
||||||
|
// cuelga el CDK del propio `<body>`, que es donde va la clase de tema, así que
|
||||||
|
// hereda los tokens. Declararlos ahí además sería contraproducente, porque el
|
||||||
|
// contenedor no lleva la clase del tema y sus valores por defecto pisarían por
|
||||||
|
// proximidad a los que hereda.
|
||||||
|
|
||||||
@mixin defaultThemeTokens {
|
@mixin defaultThemeTokens {
|
||||||
// --- Botones ---------------------------------------------------------
|
// --- Botones ---------------------------------------------------------
|
||||||
@@ -69,7 +73,6 @@
|
|||||||
// Base para *todos* los shells: así un tema solo tiene que declarar lo que
|
// Base para *todos* los shells: así un tema solo tiene que declarar lo que
|
||||||
// cambia, y lo que no redefina sigue teniendo valor (incluido el puente con
|
// cambia, y lo que no redefina sigue teniendo valor (incluido el puente con
|
||||||
// Material). Los temas concretos se aplican encima; ver `_alternative.scss`.
|
// Material). Los temas concretos se aplican encima; ver `_alternative.scss`.
|
||||||
.pi-shell,
|
.pi-shell {
|
||||||
.cdk-overlay-container {
|
|
||||||
@include defaultThemeTokens;
|
@include defaultThemeTokens;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
TemplateRef,
|
TemplateRef,
|
||||||
input,
|
input,
|
||||||
contentChild,
|
contentChild,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { status } from '../../../util/status';
|
import { status } from '../../../util/status';
|
||||||
|
|
||||||
@@ -25,18 +26,18 @@ export class PiAccordionHeaderDirective {
|
|||||||
// Accordion Item Header Content
|
// Accordion Item Header Content
|
||||||
@Directive({ selector: '[pi-accordion-item-header-content]' })
|
@Directive({ selector: '[pi-accordion-item-header-content]' })
|
||||||
export class PiAccordionItemHeaderDirective {
|
export class PiAccordionItemHeaderDirective {
|
||||||
constructor(public templateRef: TemplateRef<any>) {}
|
public readonly templateRef = inject<any>(TemplateRef);
|
||||||
}
|
}
|
||||||
// Accordion Item Summary Content
|
// Accordion Item Summary Content
|
||||||
@Directive({ selector: '[pi-accordion-item-summary-content]' })
|
@Directive({ selector: '[pi-accordion-item-summary-content]' })
|
||||||
export class PiAccordionSummaryContentDirective {
|
export class PiAccordionSummaryContentDirective {
|
||||||
constructor(public templateRef: TemplateRef<any>) {}
|
readonly templateRef = inject<TemplateRef<any>>(TemplateRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accordion Item Expandable Content
|
// Accordion Item Expandable Content
|
||||||
@Directive({ selector: '[pi-accordion-item-expandable-content]' })
|
@Directive({ selector: '[pi-accordion-item-expandable-content]' })
|
||||||
export class PiAccordionItemContentDirective {
|
export class PiAccordionItemContentDirective {
|
||||||
constructor(public templateRef: TemplateRef<any>) {}
|
readonly templateRef = inject<TemplateRef<any>>(TemplateRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { Directive, OnInit, input, output } from '@angular/core';
|
import { Directive, input, output } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
buttonSize,
|
buttonSize,
|
||||||
styleType,
|
styleType,
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
// Style definitions
|
// Style definitions
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class PiBaseButtonComponent implements OnInit {
|
export abstract class PiBaseButtonComponent {
|
||||||
readonly size = input<buttonSize>('medium');
|
readonly size = input<buttonSize>('medium');
|
||||||
readonly color = input<styleType>('primary');
|
readonly color = input<styleType>('primary');
|
||||||
readonly appearance = input<buttonStyle>('fill');
|
readonly appearance = input<buttonStyle>('fill');
|
||||||
@@ -19,11 +19,6 @@ export abstract class PiBaseButtonComponent implements OnInit {
|
|||||||
|
|
||||||
readonly execute = output<MouseEvent>();
|
readonly execute = output<MouseEvent>();
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.sizeClass;
|
|
||||||
this.flexDirectionClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected get colorClass(): string {
|
protected get colorClass(): string {
|
||||||
return `pi-color__${this.color()}`;
|
return `pi-color__${this.color()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { Directive, HostBinding, OnInit, input, output } from '@angular/core';
|
import { Directive, input, output } from '@angular/core';
|
||||||
|
|
||||||
// Style definitions
|
// Style definitions
|
||||||
import { alignment, context, gap, mode, opacity, padding } from '../../styles';
|
import { alignment, context, gap, mode, opacity, padding } from '../../styles';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class PiBaseCardComponent implements OnInit {
|
export abstract class PiBaseCardComponent {
|
||||||
readonly hasShadow = input(true);
|
readonly hasShadow = input(true);
|
||||||
readonly isClickable = input(false);
|
readonly isClickable = input(false);
|
||||||
readonly isStroked = input(false);
|
readonly isStroked = input(false);
|
||||||
@@ -25,19 +25,6 @@ export abstract class PiBaseCardComponent implements OnInit {
|
|||||||
|
|
||||||
readonly execute = output<void>();
|
readonly execute = output<void>();
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.colorClass;
|
|
||||||
this.contextClass;
|
|
||||||
this.gapClass;
|
|
||||||
this.hasEqualPaddingClass;
|
|
||||||
this.hasLinkLabelClass;
|
|
||||||
this.hasShadowClass;
|
|
||||||
this.isClickableClass;
|
|
||||||
this.modeClass;
|
|
||||||
this.opacityClass;
|
|
||||||
this.isStrokedClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected get hasShadowClass(): string | boolean | undefined {
|
protected get hasShadowClass(): string | boolean | undefined {
|
||||||
return this.hasShadow() && 'has-shadow';
|
return this.hasShadow() && 'has-shadow';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
|
import {
|
||||||
|
Directive,
|
||||||
|
TemplateRef,
|
||||||
|
ViewContainerRef,
|
||||||
|
inject,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
@Directive({
|
@Directive({
|
||||||
selector: '[pi-carousel-item]',
|
selector: '[pi-carousel-item]',
|
||||||
host: { class: 'pi-carousel pi-carousel__item' },
|
host: { class: 'pi-carousel pi-carousel__item' },
|
||||||
})
|
})
|
||||||
export class PiCarouselItemDirective {
|
export class PiCarouselItemDirective {
|
||||||
constructor(
|
public readonly templateRef = inject<TemplateRef<any>>(TemplateRef);
|
||||||
public templateRef: TemplateRef<any>,
|
public readonly viewContainerRef = inject(ViewContainerRef);
|
||||||
public viewContainerRef: ViewContainerRef,
|
|
||||||
) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
OnChanges,
|
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
|
||||||
Renderer2,
|
Renderer2,
|
||||||
SimpleChanges,
|
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
viewChild,
|
viewChild,
|
||||||
contentChildren,
|
contentChildren,
|
||||||
|
inject,
|
||||||
|
effect,
|
||||||
|
untracked,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
// Shared
|
// Shared
|
||||||
@@ -34,7 +34,7 @@ import { PiIconButtonComponent } from '../../button/icon-button/icon-button.comp
|
|||||||
imports: [PiIconButtonComponent, NgTemplateOutlet],
|
imports: [PiIconButtonComponent, NgTemplateOutlet],
|
||||||
})
|
})
|
||||||
export class PiCarouselComponent
|
export class PiCarouselComponent
|
||||||
implements OnInit, OnChanges, AfterViewInit, AfterViewChecked, OnDestroy
|
implements AfterViewInit, AfterViewChecked, OnDestroy
|
||||||
{
|
{
|
||||||
@HostBinding('class') get classes(): string {
|
@HostBinding('class') get classes(): string {
|
||||||
const carouselItems = this.carouselItems();
|
const carouselItems = this.carouselItems();
|
||||||
@@ -168,12 +168,26 @@ export class PiCarouselComponent
|
|||||||
private touchmoveListener!: () => void;
|
private touchmoveListener!: () => void;
|
||||||
private touchendListener!: () => void;
|
private touchendListener!: () => void;
|
||||||
|
|
||||||
constructor(
|
private readonly cdr = inject(ChangeDetectorRef);
|
||||||
private cdr: ChangeDetectorRef,
|
private readonly el = inject(ElementRef);
|
||||||
private el: ElementRef,
|
private readonly renderer = inject(Renderer2);
|
||||||
private renderer: Renderer2,
|
private readonly deviceConfig = inject(DeviceConfigsFacade);
|
||||||
private deviceConfig: DeviceConfigsFacade,
|
|
||||||
) {}
|
constructor() {
|
||||||
|
// No depende de inputs: se puede suscribir ya en la construcción.
|
||||||
|
// Sustituye al `ngOnChanges`: recalcula al cambiar `itemWidth`, siempre
|
||||||
|
// que la vista ya esté montada.
|
||||||
|
effect(() => {
|
||||||
|
this.itemWidth();
|
||||||
|
if (this.carouselContentWrapper()) {
|
||||||
|
untracked(() => this.setItemsPerPage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
combineLatest([this.deviceConfig.device$, this.deviceConfig.size$])
|
||||||
|
.pipe(skip(1), takeUntil(this.destroyed$))
|
||||||
|
.subscribe(() => this.reloadComponent());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used for symmetric carousel to know how many items to show on each side of the current item.
|
* Used for symmetric carousel to know how many items to show on each side of the current item.
|
||||||
@@ -239,20 +253,6 @@ export class PiCarouselComponent
|
|||||||
return Math.trunc(this.currentItem / this.itemsPerPage);
|
return Math.trunc(this.currentItem / this.itemsPerPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
combineLatest([this.deviceConfig.device$, this.deviceConfig.size$])
|
|
||||||
.pipe(skip(1), takeUntil(this.destroyed$))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.reloadComponent();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges(simpleChanges: SimpleChanges): void {
|
|
||||||
if (simpleChanges['itemWidth'] && !!this.carouselContentWrapper()) {
|
|
||||||
this.setItemsPerPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.setItemsPerPage();
|
this.setItemsPerPage();
|
||||||
this.setPages();
|
this.setPages();
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {
|
|||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
OnInit,
|
|
||||||
input,
|
input,
|
||||||
viewChild,
|
viewChild,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Device } from '../../../util/device-configs';
|
import { Device } from '../../../util/device-configs';
|
||||||
import { Dataset } from '../../../util/models';
|
import { Dataset } from '../../../util/models';
|
||||||
@@ -48,14 +48,14 @@ registerChartComponents();
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [PiChartLegendComponent],
|
imports: [PiChartLegendComponent],
|
||||||
})
|
})
|
||||||
export class PiChartComponent implements OnInit, AfterViewInit {
|
export class PiChartComponent implements AfterViewInit {
|
||||||
readonly type = input<'line' | 'bar'>('line');
|
readonly type = input<'line' | 'bar'>('line');
|
||||||
readonly datasets = input.required<Array<Dataset>>();
|
readonly datasets = input.required<Array<Dataset>>();
|
||||||
readonly device = input.required<Device>();
|
readonly device = input.required<Device>();
|
||||||
readonly xAxisMarks = input<Array<string>>();
|
readonly xAxisMarks = input<Array<string>>();
|
||||||
readonly isComparativeChart = input<boolean>(true);
|
readonly isComparativeChart = input<boolean>(true);
|
||||||
|
|
||||||
private _hoveredDatasetIndex!: number;
|
private _hoveredDatasetIndex = -1;
|
||||||
set hoveredDatasetIndex(index: number) {
|
set hoveredDatasetIndex(index: number) {
|
||||||
this._hoveredDatasetIndex = index;
|
this._hoveredDatasetIndex = index;
|
||||||
}
|
}
|
||||||
@@ -69,10 +69,8 @@ export class PiChartComponent implements OnInit, AfterViewInit {
|
|||||||
|
|
||||||
chart!: Chart;
|
chart!: Chart;
|
||||||
|
|
||||||
constructor(
|
private readonly translate = inject(TranslateService);
|
||||||
private translate: TranslateService,
|
private readonly cdr = inject(ChangeDetectorRef);
|
||||||
private cdr: ChangeDetectorRef,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
get completedDatasets() {
|
get completedDatasets() {
|
||||||
return this.datasets().map((dataset) => {
|
return this.datasets().map((dataset) => {
|
||||||
@@ -87,10 +85,6 @@ export class PiChartComponent implements OnInit, AfterViewInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.hoveredDatasetIndex = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.getLineChartConfigs();
|
this.getLineChartConfigs();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
OnInit,
|
|
||||||
TemplateRef,
|
TemplateRef,
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
input,
|
input,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { itemType } from '../entities/data-grid.model';
|
import { itemType } from '../entities/data-grid.model';
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ export class PiCellDirective implements AfterViewInit {
|
|||||||
readonly index = input.required<number>();
|
readonly index = input.required<number>();
|
||||||
readonly textAlign = input<'left' | 'center' | 'right'>('left');
|
readonly textAlign = input<'left' | 'center' | 'right'>('left');
|
||||||
|
|
||||||
constructor(private elRef: ElementRef) {}
|
private readonly elRef = inject(ElementRef);
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
const index = this.index();
|
const index = this.index();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
HostListener,
|
HostListener,
|
||||||
Renderer2,
|
Renderer2,
|
||||||
input,
|
input,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { columnsNumber } from '../entities/data-grid.model';
|
import { columnsNumber } from '../entities/data-grid.model';
|
||||||
|
|
||||||
@@ -32,10 +33,8 @@ export class PiGridItemDirective {
|
|||||||
|
|
||||||
readonly isVisited = input(false);
|
readonly isVisited = input(false);
|
||||||
|
|
||||||
constructor(
|
private readonly elRef = inject(ElementRef);
|
||||||
private elRef: ElementRef,
|
private readonly renderer = inject(Renderer2);
|
||||||
private renderer: Renderer2,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@HostListener('click') selectItem(): void {
|
@HostListener('click') selectItem(): void {
|
||||||
const gridItem = this.elRef.nativeElement;
|
const gridItem = this.elRef.nativeElement;
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
OnInit,
|
|
||||||
input,
|
input,
|
||||||
|
effect,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ import { take } from 'rxjs/operators';
|
|||||||
styleUrls: ['./base-data-table.component.scss'],
|
styleUrls: ['./base-data-table.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class PiBaseDataTableComponent implements OnInit {
|
export class PiBaseDataTableComponent {
|
||||||
@HostBinding('class') get classes(): string {
|
@HostBinding('class') get classes(): string {
|
||||||
return [
|
return [
|
||||||
'pi-base-data-table',
|
'pi-base-data-table',
|
||||||
@@ -36,12 +37,12 @@ export class PiBaseDataTableComponent implements OnInit {
|
|||||||
readonly type = input<'header' | 'data'>('data');
|
readonly type = input<'header' | 'data'>('data');
|
||||||
readonly hideValues = input(false);
|
readonly hideValues = input(false);
|
||||||
|
|
||||||
constructor(private store: Store<DeviceConfigsState>) {}
|
private readonly store = inject<Store<DeviceConfigsState>>(Store);
|
||||||
|
|
||||||
ngOnInit(): void {
|
constructor() {
|
||||||
this.dataDirectionClass;
|
// `hideHeader` lee inputs; las otras dos líneas del antiguo `ngOnInit` solo
|
||||||
this.typeClass;
|
// leían getters y no hacían nada.
|
||||||
this.hideHeader();
|
effect(() => this.hideHeader());
|
||||||
}
|
}
|
||||||
|
|
||||||
get dataDirectionClass(): string {
|
get dataDirectionClass(): string {
|
||||||
|
|||||||
@@ -8,15 +8,13 @@ import {
|
|||||||
input,
|
input,
|
||||||
contentChild,
|
contentChild,
|
||||||
viewChild,
|
viewChild,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { textStyle, justify, borderType } from '../../../styles';
|
import { textStyle, justify, borderType } from '../../../styles';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
// Store
|
// Store
|
||||||
import {
|
import { DeviceConfigsState } from '../../../util/device-configs';
|
||||||
DeviceConfigsState,
|
|
||||||
DeviceTypeSelectors,
|
|
||||||
} from '../../../util/device-configs';
|
|
||||||
|
|
||||||
// RxJS
|
// RxJS
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
@@ -44,7 +42,7 @@ export class PiDataTableItemComponent implements AfterViewInit {
|
|||||||
readonly item = contentChild<TemplateRef<any>>('item');
|
readonly item = contentChild<TemplateRef<any>>('item');
|
||||||
readonly componentItem = viewChild<ElementRef>('componentItem');
|
readonly componentItem = viewChild<ElementRef>('componentItem');
|
||||||
|
|
||||||
constructor(private store: Store<DeviceConfigsState>) {}
|
private readonly store = inject<Store<DeviceConfigsState>>(Store);
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.setCellClassByIndex();
|
this.setCellClassByIndex();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { LayoutTypeClass } from '../../../styles';
|
import { LayoutTypeClass } from '../../../styles';
|
||||||
@@ -39,7 +40,7 @@ import { TranslatePipe } from '@ngx-translate/core';
|
|||||||
export class PiNotFoundComponent {
|
export class PiNotFoundComponent {
|
||||||
@HostBinding('class') className = LayoutTypeClass.VerticallyCentered;
|
@HostBinding('class') className = LayoutTypeClass.VerticallyCentered;
|
||||||
|
|
||||||
constructor(private location: Location) {}
|
private readonly location = inject(Location);
|
||||||
|
|
||||||
previous(): void {
|
previous(): void {
|
||||||
this.location.back();
|
this.location.back();
|
||||||
|
|||||||
@@ -166,7 +166,9 @@ export class PiFileUploadComponent implements FormValueControl<File[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// En modo simple el último sustituye al anterior.
|
// En modo simple el último sustituye al anterior.
|
||||||
let next = this.multiple() ? [...this.value(), ...accepted] : accepted.slice(-1);
|
let next = this.multiple()
|
||||||
|
? [...this.value(), ...accepted]
|
||||||
|
: accepted.slice(-1);
|
||||||
|
|
||||||
const maxFiles = this.maxFiles();
|
const maxFiles = this.maxFiles();
|
||||||
if (this.multiple() && maxFiles !== undefined && next.length > maxFiles) {
|
if (this.multiple() && maxFiles !== undefined && next.length > maxFiles) {
|
||||||
|
|||||||
@@ -20,12 +20,11 @@
|
|||||||
pi-autofocus
|
pi-autofocus
|
||||||
[isAutofocusSelected]="isAutofocus()"
|
[isAutofocusSelected]="isAutofocus()"
|
||||||
/>
|
/>
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
@if (enableErrorMessage() && hasError()) {
|
|
||||||
<mat-error>
|
|
||||||
@for (message of errorMessages(); track message) {
|
|
||||||
{{ message | translate }}
|
|
||||||
}
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component, HostBinding, input } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
HostBinding,
|
||||||
|
input,
|
||||||
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
MatFormField,
|
MatFormField,
|
||||||
MatLabel,
|
MatLabel,
|
||||||
|
|||||||
@@ -1,27 +1,37 @@
|
|||||||
import {
|
import {
|
||||||
Directive,
|
Directive,
|
||||||
OnChanges,
|
|
||||||
OnInit,
|
|
||||||
SimpleChanges,
|
|
||||||
input,
|
input,
|
||||||
|
afterNextRender,
|
||||||
|
inject,
|
||||||
|
effect,
|
||||||
|
untracked,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatInput } from '@angular/material/input';
|
import { MatInput } from '@angular/material/input';
|
||||||
|
|
||||||
@Directive({ selector: '[pi-autofocus]' })
|
@Directive({ selector: '[pi-autofocus]' })
|
||||||
export class PiAutofocusDirective implements OnInit, OnChanges {
|
export class PiAutofocusDirective {
|
||||||
readonly isAutofocusSelected = input<boolean>();
|
readonly isAutofocusSelected = input<boolean>();
|
||||||
|
|
||||||
constructor(private matInput: MatInput) {}
|
private readonly matInput = inject(MatInput);
|
||||||
|
|
||||||
ngOnInit() {
|
constructor() {
|
||||||
if (this.isAutofocusSelected()) {
|
// `afterNextRender` sustituye al `ngOnInit` + `setTimeout`: ya corre con la
|
||||||
setTimeout(() => this.matInput.focus());
|
// vista pintada, así que el `focus()` no necesita aplazarse a mano.
|
||||||
}
|
// Cubre lo que hacían el `ngOnInit` (primer render) y el `ngOnChanges`
|
||||||
}
|
// (cada cambio de la entrada) en una sola pieza.
|
||||||
|
afterNextRender(() => {
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
|
||||||
if (changes) {
|
|
||||||
if (this.isAutofocusSelected()) this.matInput.focus();
|
if (this.isAutofocusSelected()) this.matInput.focus();
|
||||||
}
|
});
|
||||||
|
|
||||||
|
let first = true;
|
||||||
|
effect(() => {
|
||||||
|
const shouldFocus = this.isAutofocusSelected();
|
||||||
|
// El primer paso lo cubre `afterNextRender`, con la vista ya pintada.
|
||||||
|
if (first) {
|
||||||
|
first = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (shouldFocus) untracked(() => this.matInput.focus());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import {
|
|||||||
Directive,
|
Directive,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
OnInit,
|
|
||||||
Renderer2,
|
Renderer2,
|
||||||
input,
|
input,
|
||||||
|
afterNextRender,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
@Directive({ selector: 'pi-label' })
|
@Directive({ selector: 'pi-label' })
|
||||||
export class PiLabelDirective implements OnInit {
|
export class PiLabelDirective {
|
||||||
@HostBinding('class') get classes(): string {
|
@HostBinding('class') get classes(): string {
|
||||||
const validationClass = this.isRequired() ? 'is-required' : '';
|
const validationClass = this.isRequired() ? 'is-required' : '';
|
||||||
return ['pi-label', validationClass].join(' ');
|
return ['pi-label', validationClass].join(' ');
|
||||||
@@ -17,12 +18,15 @@ export class PiLabelDirective implements OnInit {
|
|||||||
|
|
||||||
readonly isRequired = input<boolean>(false);
|
readonly isRequired = input<boolean>(false);
|
||||||
|
|
||||||
constructor(
|
private readonly renderer = inject(Renderer2);
|
||||||
private renderer: Renderer2,
|
private readonly el = inject(ElementRef);
|
||||||
private el: ElementRef,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
constructor() {
|
||||||
|
// Igual que en `pi-affix`: depende de un input y toca el DOM una vez.
|
||||||
|
afterNextRender(() => this.renderRequiredMark());
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderRequiredMark(): void {
|
||||||
if (this.isRequired()) {
|
if (this.isRequired()) {
|
||||||
const span = this.renderer.createElement('span');
|
const span = this.renderer.createElement('span');
|
||||||
const text = this.renderer.createText('*');
|
const text = this.renderer.createText('*');
|
||||||
|
|||||||
@@ -17,12 +17,11 @@
|
|||||||
pi-autofocus
|
pi-autofocus
|
||||||
[isAutofocusSelected]="isAutofocus()"
|
[isAutofocusSelected]="isAutofocus()"
|
||||||
/>
|
/>
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
@if (enableErrorMessage() && hasError()) {
|
|
||||||
<mat-error>
|
|
||||||
@for (message of errorMessages(); track message) {
|
|
||||||
{{ message | translate }}
|
|
||||||
}
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
pi-autofocus
|
pi-autofocus
|
||||||
[isAutofocusSelected]="isAutofocus()"
|
[isAutofocusSelected]="isAutofocus()"
|
||||||
/>
|
/>
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</section>
|
</section>
|
||||||
@if (enableErrorMessage() && hasError()) {
|
|
||||||
<mat-error>
|
|
||||||
@for (message of errorMessages(); track message) {
|
|
||||||
{{ message | translate }}
|
|
||||||
}
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,7 +11,4 @@
|
|||||||
gap: var(--pi-spacing-medium);
|
gap: var(--pi-spacing-medium);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.mat-mdc-form-field-error {
|
|
||||||
margin-left: 3.8rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,12 +38,11 @@
|
|||||||
pi-autofocus
|
pi-autofocus
|
||||||
[isAutofocusSelected]="isAutofocus()"
|
[isAutofocusSelected]="isAutofocus()"
|
||||||
/>
|
/>
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
@if (enableErrorMessage() && hasError()) {
|
|
||||||
<mat-error>
|
|
||||||
@for (message of errorMessages(); track message) {
|
|
||||||
{{ message | translate }}
|
|
||||||
}
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,3 +28,9 @@ export * from './base-form-field.component';
|
|||||||
export * from './incremental-range-field/incremental-range-field.component';
|
export * from './incremental-range-field/incremental-range-field.component';
|
||||||
|
|
||||||
export * from './slide-toggle/slide-toggle.component';
|
export * from './slide-toggle/slide-toggle.component';
|
||||||
|
export * from './select-field/select-field.model';
|
||||||
|
export * from './select-field/select-field.component';
|
||||||
|
export * from './select-field/multi-select-field.component';
|
||||||
|
export * from './select-field/searchable-select-field.component';
|
||||||
|
export * from './select-field/base-searchable-select-field.component';
|
||||||
|
export * from './select-field/searchable-multi-select-field.component';
|
||||||
|
|||||||
@@ -20,12 +20,11 @@
|
|||||||
pi-autofocus
|
pi-autofocus
|
||||||
[isAutofocusSelected]="isAutofocus()"
|
[isAutofocusSelected]="isAutofocus()"
|
||||||
/>
|
/>
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
@if (enableErrorMessage() && hasError()) {
|
|
||||||
<mat-error>
|
|
||||||
@for (message of errorMessages(); track message) {
|
|
||||||
{{ message | translate }}
|
|
||||||
}
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,12 +24,11 @@
|
|||||||
pi-autofocus
|
pi-autofocus
|
||||||
[isAutofocusSelected]="isAutofocus()"
|
[isAutofocusSelected]="isAutofocus()"
|
||||||
/>
|
/>
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
@if (enableErrorMessage() && hasError()) {
|
|
||||||
<mat-error>
|
|
||||||
@for (message of errorMessages(); track message) {
|
|
||||||
{{ message | translate }}
|
|
||||||
}
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,12 +16,11 @@
|
|||||||
pi-autofocus
|
pi-autofocus
|
||||||
[isAutofocusSelected]="isAutofocus()"
|
[isAutofocusSelected]="isAutofocus()"
|
||||||
/>
|
/>
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
@if (enableErrorMessage() && hasError()) {
|
|
||||||
<mat-error>
|
|
||||||
@for (message of errorMessages(); track message) {
|
|
||||||
{{ message | translate }}
|
|
||||||
}
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ export interface PiRadioOption<TValue = unknown> {
|
|||||||
imports: [PiRadioButtonComponent, TranslatePipe],
|
imports: [PiRadioButtonComponent, TranslatePipe],
|
||||||
host: { role: 'radiogroup' },
|
host: { role: 'radiogroup' },
|
||||||
})
|
})
|
||||||
export class PiRadioGroupComponent<TValue = unknown>
|
export class PiRadioGroupComponent<
|
||||||
implements FormValueControl<TValue | null>
|
TValue = unknown,
|
||||||
{
|
> implements FormValueControl<TValue | null> {
|
||||||
@HostBinding('class') get classes(): string {
|
@HostBinding('class') get classes(): string {
|
||||||
return [
|
return [
|
||||||
ComponentFamilyClass.Field,
|
ComponentFamilyClass.Field,
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
computed,
|
||||||
|
effect,
|
||||||
|
input,
|
||||||
|
signal,
|
||||||
|
viewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatSelect } from '@angular/material/select';
|
||||||
|
|
||||||
|
import { PiBaseFormFieldComponent } from '../base-form-field.component';
|
||||||
|
import { PiSelectFieldOption } from './select-field.model';
|
||||||
|
|
||||||
|
/** Quita acentos y pasa a minúsculas, para que «Peru» encuentre «Perú». */
|
||||||
|
function normalize(text: string): string {
|
||||||
|
return text.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parte común de los selects con buscador (simple y múltiple).
|
||||||
|
*
|
||||||
|
* Reúne el filtro, la navegación por teclado y el enganche del estado de
|
||||||
|
* error. Lo único que cambia entre ambos es qué hace `Enter` sobre la opción
|
||||||
|
* resaltada y cómo se resume el valor en el campo cerrado, así que eso queda
|
||||||
|
* abstracto.
|
||||||
|
*
|
||||||
|
* @template TValue Tipo de cada opción.
|
||||||
|
* @template TModel Lo que guarda el formulario: una opción o un array de ellas.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
template: '',
|
||||||
|
})
|
||||||
|
export abstract class PiBaseSearchableSelectFieldComponent<
|
||||||
|
TValue,
|
||||||
|
TModel,
|
||||||
|
> extends PiBaseFormFieldComponent<TModel> {
|
||||||
|
readonly options = input.required<readonly PiSelectFieldOption<TValue>[]>();
|
||||||
|
|
||||||
|
/** Texto de la caja de búsqueda. */
|
||||||
|
readonly searchPlaceholder = input('common.search');
|
||||||
|
|
||||||
|
/** Se muestra cuando el filtro no deja ninguna opción. */
|
||||||
|
readonly emptyLabel = input('common.noResults');
|
||||||
|
|
||||||
|
protected readonly search = signal('');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Índice resaltado dentro de la lista filtrada. Se gestiona aquí en vez de
|
||||||
|
* delegar en `mat-select`: con el foco en el buscador su gestor de teclas no
|
||||||
|
* llega a activarse, y enfocar la opción a mano no lo sincroniza (Enter
|
||||||
|
* acababa confirmando una opción distinta de la resaltada).
|
||||||
|
*/
|
||||||
|
protected readonly activeIndex = signal(0);
|
||||||
|
|
||||||
|
protected readonly filteredOptions = computed(() => {
|
||||||
|
const term = normalize(this.search().trim());
|
||||||
|
if (!term) return this.options();
|
||||||
|
return this.options().filter((o) => normalize(o.label).includes(term));
|
||||||
|
});
|
||||||
|
|
||||||
|
protected readonly matSelectRef = viewChild(MatSelect);
|
||||||
|
|
||||||
|
private readonly searchInputRef =
|
||||||
|
viewChild<ElementRef<HTMLInputElement>>('searchInput');
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
// La clase base engancha el `errorStateMatcher` a `MatInput`, que aquí no
|
||||||
|
// hay; `MatSelect` expone la misma API.
|
||||||
|
effect(() => {
|
||||||
|
this.hasError();
|
||||||
|
const select = this.matSelectRef();
|
||||||
|
if (!select) return;
|
||||||
|
select.errorStateMatcher = this.errorStateMatcher;
|
||||||
|
select.updateErrorState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Qué hacer con la opción resaltada al pulsar Enter. */
|
||||||
|
protected abstract commitActiveOption(
|
||||||
|
option: PiSelectFieldOption<TValue>,
|
||||||
|
): void;
|
||||||
|
|
||||||
|
protected onSearchInput(term: string): void {
|
||||||
|
this.search.set(term);
|
||||||
|
this.activeIndex.set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isOptionActive(index: number): boolean {
|
||||||
|
return this.activeIndex() === index;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onOpenedChange(opened: boolean): void {
|
||||||
|
if (opened) {
|
||||||
|
this.onFocus();
|
||||||
|
// El panel se monta al abrir, así que hay que esperar al siguiente frame
|
||||||
|
// para que el input exista y se pueda enfocar.
|
||||||
|
requestAnimationFrame(() => this.searchInputRef()?.nativeElement.focus());
|
||||||
|
} else {
|
||||||
|
// El filtro no se conserva entre aperturas: al reabrir se ve la lista
|
||||||
|
// completa, que es lo esperable.
|
||||||
|
this.search.set('');
|
||||||
|
this.activeIndex.set(0);
|
||||||
|
this.onBlur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `mat-select` salta a la opción que empieza por la letra pulsada, así que
|
||||||
|
* lo escrito en el buscador no debe llegarle. Las flechas y el Enter se
|
||||||
|
* atienden aquí, sobre la lista ya filtrada.
|
||||||
|
*/
|
||||||
|
protected onSearchKeydown(event: KeyboardEvent): void {
|
||||||
|
const options = this.filteredOptions();
|
||||||
|
const last = options.length - 1;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.activeIndex.set(Math.min(this.activeIndex() + 1, last));
|
||||||
|
return;
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.activeIndex.set(Math.max(this.activeIndex() - 1, 0));
|
||||||
|
return;
|
||||||
|
case 'Enter': {
|
||||||
|
const option = options[this.activeIndex()];
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (option && !option.disabled) this.commitActiveOption(option);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'Escape':
|
||||||
|
case 'Tab':
|
||||||
|
// Las gestiona `mat-select` (cerrar el panel, salir del campo).
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<mat-form-field [appearance]="appearance()" floatLabel="always">
|
||||||
|
<mat-label><ng-content select="pi-label"></ng-content></mat-label>
|
||||||
|
|
||||||
|
<mat-select
|
||||||
|
multiple
|
||||||
|
[value]="value()"
|
||||||
|
(valueChange)="onSelectionChange($event)"
|
||||||
|
(openedChange)="$event ? onFocus() : onBlur()"
|
||||||
|
[disabled]="disabled()"
|
||||||
|
[required]="required()"
|
||||||
|
[placeholder]="placeholder()!"
|
||||||
|
>
|
||||||
|
<!--
|
||||||
|
`mat-select-trigger` sustituye al texto que pinta Material por defecto,
|
||||||
|
que concatenaría los valores en crudo (objetos, en el caso general).
|
||||||
|
-->
|
||||||
|
<mat-select-trigger>{{ displayText() }}</mat-select-trigger>
|
||||||
|
|
||||||
|
@for (option of options(); track option.value) {
|
||||||
|
<mat-option [value]="option.value" [disabled]="!!option.disabled">
|
||||||
|
{{ option.label | translate }}
|
||||||
|
</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
HostBinding,
|
||||||
|
computed,
|
||||||
|
effect,
|
||||||
|
input,
|
||||||
|
viewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatError, MatFormField, MatLabel } from '@angular/material/form-field';
|
||||||
|
import { MatOption } from '@angular/material/core';
|
||||||
|
import { MatSelect, MatSelectTrigger } from '@angular/material/select';
|
||||||
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { PiBaseFormFieldComponent } from '../base-form-field.component';
|
||||||
|
import { PiSelectFieldOption } from './select-field.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Campo con desplegable de selección múltiple.
|
||||||
|
*
|
||||||
|
* Mismo planteamiento que `pi-select-field`; `mat-select` en modo `multiple`
|
||||||
|
* ya pinta las casillas dentro del panel, así que no hace falta componer nada
|
||||||
|
* a mano.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'pi-multi-select-field',
|
||||||
|
templateUrl: './multi-select-field.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
MatFormField,
|
||||||
|
MatLabel,
|
||||||
|
MatSelect,
|
||||||
|
MatSelectTrigger,
|
||||||
|
MatOption,
|
||||||
|
MatError,
|
||||||
|
TranslatePipe,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PiMultiSelectFieldComponent<
|
||||||
|
TValue = unknown,
|
||||||
|
> extends PiBaseFormFieldComponent<TValue[]> {
|
||||||
|
@HostBinding('class') className = 'pi-form-field__select';
|
||||||
|
|
||||||
|
readonly options = input.required<readonly PiSelectFieldOption<TValue>[]>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A partir de este número de opciones marcadas se resume como «N
|
||||||
|
* seleccionados» en vez de listarlas. Con `0` nunca resume.
|
||||||
|
*/
|
||||||
|
readonly summaryThreshold = input(3);
|
||||||
|
|
||||||
|
/** Texto del resumen; recibe el número por el pipe `translate`. */
|
||||||
|
readonly summaryLabel = input('seleccionados');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lo que se ve en el campo cuando está cerrado. `mat-select` por defecto
|
||||||
|
* concatena los valores en crudo, que aquí serían objetos.
|
||||||
|
*/
|
||||||
|
protected readonly displayText = computed(() => {
|
||||||
|
const selected = this.value() ?? [];
|
||||||
|
if (!selected.length) return '';
|
||||||
|
|
||||||
|
const threshold = this.summaryThreshold();
|
||||||
|
if (threshold > 0 && selected.length >= threshold) {
|
||||||
|
return `${selected.length} ${this.summaryLabel()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byValue = new Map(this.options().map((o) => [o.value, o.label]));
|
||||||
|
return selected.map((v) => byValue.get(v) ?? String(v)).join(', ');
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Mismo enganche de estado de error que en `pi-select-field`. */
|
||||||
|
private readonly matSelectRef = viewChild(MatSelect);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
effect(() => {
|
||||||
|
this.hasError();
|
||||||
|
const select = this.matSelectRef();
|
||||||
|
if (!select) return;
|
||||||
|
select.errorStateMatcher = this.errorStateMatcher;
|
||||||
|
select.updateErrorState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onSelectionChange(next: TValue[] | null): void {
|
||||||
|
this.value.set(next ?? []);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<mat-form-field [appearance]="appearance()" floatLabel="always">
|
||||||
|
<mat-label><ng-content select="pi-label"></ng-content></mat-label>
|
||||||
|
|
||||||
|
<mat-select
|
||||||
|
multiple
|
||||||
|
[value]="value()"
|
||||||
|
(valueChange)="onSelectionChange($event)"
|
||||||
|
(openedChange)="onOpenedChange($event)"
|
||||||
|
[disabled]="disabled()"
|
||||||
|
[required]="required()"
|
||||||
|
[placeholder]="placeholder()!"
|
||||||
|
>
|
||||||
|
<!--
|
||||||
|
El texto del campo cerrado sale del valor y no de la opción pintada:
|
||||||
|
con el filtro activo esa opción puede no estar en la lista.
|
||||||
|
-->
|
||||||
|
<mat-select-trigger>{{ displayText() }}</mat-select-trigger>
|
||||||
|
|
||||||
|
<div class="pi-select-search">
|
||||||
|
<pi-icon icon="search" size="small" color="grey" />
|
||||||
|
<input
|
||||||
|
#searchInput
|
||||||
|
type="text"
|
||||||
|
[value]="search()"
|
||||||
|
(input)="onSearchInput($any($event.target).value)"
|
||||||
|
(keydown)="onSearchKeydown($event)"
|
||||||
|
[placeholder]="searchPlaceholder() | translate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@for (option of filteredOptions(); track option.value; let i = $index) {
|
||||||
|
<mat-option
|
||||||
|
[value]="option.value"
|
||||||
|
[disabled]="!!option.disabled"
|
||||||
|
[class.is-active]="isOptionActive(i)"
|
||||||
|
>
|
||||||
|
{{ option.label | translate }}
|
||||||
|
</mat-option>
|
||||||
|
} @empty {
|
||||||
|
<p class="pi-select-search__empty">{{ emptyLabel() | translate }}</p>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
HostBinding,
|
||||||
|
computed,
|
||||||
|
input,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatError, MatFormField, MatLabel } from '@angular/material/form-field';
|
||||||
|
import { MatOption } from '@angular/material/core';
|
||||||
|
import { MatSelect, MatSelectTrigger } from '@angular/material/select';
|
||||||
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||||
|
import { PiBaseSearchableSelectFieldComponent } from './base-searchable-select-field.component';
|
||||||
|
import { PiSelectFieldOption } from './select-field.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Campo con desplegable, buscador y selección múltiple.
|
||||||
|
*
|
||||||
|
* Combina `pi-multi-select-field` y `pi-searchable-select-field`: casillas en
|
||||||
|
* el panel más caja de filtro, para listas largas de las que hay que elegir
|
||||||
|
* varias.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'pi-searchable-multi-select-field',
|
||||||
|
templateUrl: './searchable-multi-select-field.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
MatFormField,
|
||||||
|
MatLabel,
|
||||||
|
MatSelect,
|
||||||
|
MatSelectTrigger,
|
||||||
|
MatOption,
|
||||||
|
MatError,
|
||||||
|
PiIconComponent,
|
||||||
|
TranslatePipe,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PiSearchableMultiSelectFieldComponent<
|
||||||
|
TValue = unknown,
|
||||||
|
> extends PiBaseSearchableSelectFieldComponent<TValue, TValue[]> {
|
||||||
|
@HostBinding('class') className = 'pi-form-field__select';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A partir de este número de opciones marcadas se resume como «N
|
||||||
|
* seleccionados» en vez de listarlas. Con `0` nunca resume.
|
||||||
|
*/
|
||||||
|
readonly summaryThreshold = input(3);
|
||||||
|
|
||||||
|
/** Texto del resumen; recibe el número por el pipe `translate`. */
|
||||||
|
readonly summaryLabel = input('seleccionados');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Igual que en el simple, el texto sale del valor y no de las opciones
|
||||||
|
* pintadas: con el filtro activo puede haber selecciones fuera de la lista.
|
||||||
|
*/
|
||||||
|
protected readonly displayText = computed(() => {
|
||||||
|
const selected = this.value() ?? [];
|
||||||
|
if (!selected.length) return '';
|
||||||
|
|
||||||
|
const threshold = this.summaryThreshold();
|
||||||
|
if (threshold > 0 && selected.length >= threshold) {
|
||||||
|
return `${selected.length} ${this.summaryLabel()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byValue = new Map(this.options().map((o) => [o.value, o.label]));
|
||||||
|
return selected.map((v) => byValue.get(v) ?? String(v)).join(', ');
|
||||||
|
});
|
||||||
|
|
||||||
|
protected isSelected(option: PiSelectFieldOption<TValue>): boolean {
|
||||||
|
return (this.value() ?? []).includes(option.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* En selección múltiple, Enter marca o desmarca la opción resaltada y deja
|
||||||
|
* el panel abierto, para poder seguir eligiendo sin reabrirlo.
|
||||||
|
*/
|
||||||
|
protected override commitActiveOption(
|
||||||
|
option: PiSelectFieldOption<TValue>,
|
||||||
|
): void {
|
||||||
|
const current = this.value() ?? [];
|
||||||
|
this.value.set(
|
||||||
|
current.includes(option.value)
|
||||||
|
? current.filter((v) => v !== option.value)
|
||||||
|
: [...current, option.value],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onSelectionChange(next: TValue[] | null): void {
|
||||||
|
this.value.set(next ?? []);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<mat-form-field [appearance]="appearance()" floatLabel="always">
|
||||||
|
<mat-label><ng-content select="pi-label"></ng-content></mat-label>
|
||||||
|
|
||||||
|
<mat-select
|
||||||
|
[value]="value()"
|
||||||
|
(valueChange)="onSelectionChange($event)"
|
||||||
|
(openedChange)="onOpenedChange($event)"
|
||||||
|
[disabled]="disabled()"
|
||||||
|
[required]="required()"
|
||||||
|
[placeholder]="placeholder()!"
|
||||||
|
>
|
||||||
|
<!--
|
||||||
|
El texto del campo cerrado sale del valor y no de la opción pintada:
|
||||||
|
con el filtro activo esa opción puede no estar en la lista.
|
||||||
|
-->
|
||||||
|
<mat-select-trigger>{{ displayText() }}</mat-select-trigger>
|
||||||
|
|
||||||
|
<div class="pi-select-search">
|
||||||
|
<pi-icon icon="search" size="small" color="grey" />
|
||||||
|
<input
|
||||||
|
#searchInput
|
||||||
|
type="text"
|
||||||
|
[value]="search()"
|
||||||
|
(input)="onSearchInput($any($event.target).value)"
|
||||||
|
(keydown)="onSearchKeydown($event)"
|
||||||
|
[placeholder]="searchPlaceholder() | translate"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@for (option of filteredOptions(); track option.value; let i = $index) {
|
||||||
|
<mat-option
|
||||||
|
[value]="option.value"
|
||||||
|
[disabled]="!!option.disabled"
|
||||||
|
[class.is-active]="isOptionActive(i)"
|
||||||
|
>
|
||||||
|
{{ option.label | translate }}
|
||||||
|
</mat-option>
|
||||||
|
} @empty {
|
||||||
|
<p class="pi-select-search__empty">{{ emptyLabel() | translate }}</p>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
HostBinding,
|
||||||
|
computed,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatError, MatFormField, MatLabel } from '@angular/material/form-field';
|
||||||
|
import { MatOption } from '@angular/material/core';
|
||||||
|
import { MatSelect, MatSelectTrigger } from '@angular/material/select';
|
||||||
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||||
|
import { PiBaseSearchableSelectFieldComponent } from './base-searchable-select-field.component';
|
||||||
|
import { PiSelectFieldOption } from './select-field.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Campo con desplegable y buscador (selección simple).
|
||||||
|
*
|
||||||
|
* Igual que `pi-select-field` pero con una caja de filtro dentro del panel,
|
||||||
|
* pensado para listas largas donde recorrer las opciones a mano no es viable.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'pi-searchable-select-field',
|
||||||
|
templateUrl: './searchable-select-field.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
MatFormField,
|
||||||
|
MatLabel,
|
||||||
|
MatSelect,
|
||||||
|
MatSelectTrigger,
|
||||||
|
MatOption,
|
||||||
|
MatError,
|
||||||
|
PiIconComponent,
|
||||||
|
TranslatePipe,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PiSearchableSelectFieldComponent<
|
||||||
|
TValue = unknown,
|
||||||
|
> extends PiBaseSearchableSelectFieldComponent<TValue, TValue | null> {
|
||||||
|
@HostBinding('class') className = 'pi-form-field__select';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* El texto del campo cerrado se calcula del valor, no de la opción
|
||||||
|
* renderizada: al filtrar, la opción seleccionada puede no estar en la lista
|
||||||
|
* y `mat-select` se quedaría sin nada que mostrar.
|
||||||
|
*/
|
||||||
|
protected readonly displayText = computed(() => {
|
||||||
|
const current = this.value();
|
||||||
|
if (current === null || current === undefined) return '';
|
||||||
|
return (
|
||||||
|
this.options().find((o) => o.value === current)?.label ?? String(current)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** En selección simple, Enter confirma y cierra. */
|
||||||
|
protected override commitActiveOption(
|
||||||
|
option: PiSelectFieldOption<TValue>,
|
||||||
|
): void {
|
||||||
|
this.value.set(option.value);
|
||||||
|
this.matSelectRef()?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onSelectionChange(next: TValue | null): void {
|
||||||
|
this.value.set(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<mat-form-field [appearance]="appearance()" floatLabel="always">
|
||||||
|
<mat-label><ng-content select="pi-label"></ng-content></mat-label>
|
||||||
|
|
||||||
|
<mat-select
|
||||||
|
[value]="value()"
|
||||||
|
(valueChange)="onSelectionChange($event)"
|
||||||
|
(openedChange)="$event ? onFocus() : onBlur()"
|
||||||
|
[disabled]="disabled()"
|
||||||
|
[required]="required()"
|
||||||
|
[placeholder]="placeholder()!"
|
||||||
|
>
|
||||||
|
@for (option of options(); track option.value) {
|
||||||
|
<mat-option [value]="option.value" [disabled]="!!option.disabled">
|
||||||
|
{{ option.label | translate }}
|
||||||
|
</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
HostBinding,
|
||||||
|
effect,
|
||||||
|
input,
|
||||||
|
viewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatError, MatFormField, MatLabel } from '@angular/material/form-field';
|
||||||
|
import { MatOption } from '@angular/material/core';
|
||||||
|
import { MatSelect } from '@angular/material/select';
|
||||||
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { PiBaseFormFieldComponent } from '../base-form-field.component';
|
||||||
|
import { PiSelectFieldOption } from './select-field.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Campo con desplegable (selección simple).
|
||||||
|
*
|
||||||
|
* Es un *campo de formulario*, no la familia `pi-select`: aquélla pinta una
|
||||||
|
* lista siempre visible y se usa para filtros y menús; ésta es un input con su
|
||||||
|
* panel desplegable, y encaja con el resto de campos (`pi-text-field`,
|
||||||
|
* `pi-email-field`...).
|
||||||
|
*
|
||||||
|
* Extiende `PiBaseFormFieldComponent` y envuelve `mat-form-field`, así hereda
|
||||||
|
* los tokens de tema (radio, borde, superficie, etiqueta) y el tratamiento de
|
||||||
|
* errores del resto de campos.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'pi-select-field',
|
||||||
|
templateUrl: './select-field.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
MatFormField,
|
||||||
|
MatLabel,
|
||||||
|
MatSelect,
|
||||||
|
MatOption,
|
||||||
|
MatError,
|
||||||
|
TranslatePipe,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PiSelectFieldComponent<
|
||||||
|
TValue = unknown,
|
||||||
|
> extends PiBaseFormFieldComponent<TValue | null> {
|
||||||
|
@HostBinding('class') className = 'pi-form-field__select';
|
||||||
|
|
||||||
|
readonly options = input.required<readonly PiSelectFieldOption<TValue>[]>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* La clase base engancha el `errorStateMatcher` a `MatInput`, que aquí no
|
||||||
|
* hay. `MatSelect` expone la misma API, así que se repite el enganche para
|
||||||
|
* que el borde rojo y el mensaje también aparezcan en el desplegable.
|
||||||
|
*/
|
||||||
|
private readonly matSelectRef = viewChild(MatSelect);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
effect(() => {
|
||||||
|
this.hasError();
|
||||||
|
const select = this.matSelectRef();
|
||||||
|
if (!select) return;
|
||||||
|
select.errorStateMatcher = this.errorStateMatcher;
|
||||||
|
select.updateErrorState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onSelectionChange(next: TValue | null): void {
|
||||||
|
this.value.set(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/** Opción de `pi-select-field` y `pi-multi-select-field`. */
|
||||||
|
export interface PiSelectFieldOption<TValue = unknown> {
|
||||||
|
value: TValue;
|
||||||
|
/** Se pasa por el pipe `translate`, así que admite clave de i18n. */
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
@@ -15,12 +15,11 @@
|
|||||||
pi-autofocus
|
pi-autofocus
|
||||||
[isAutofocusSelected]="isAutofocus()"
|
[isAutofocusSelected]="isAutofocus()"
|
||||||
/>
|
/>
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
@if (enableErrorMessage() && hasError()) {
|
|
||||||
<mat-error>
|
|
||||||
@for (message of errorMessages(); track message) {
|
|
||||||
{{ message | translate }}
|
|
||||||
}
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,14 +17,14 @@
|
|||||||
pi-autofocus
|
pi-autofocus
|
||||||
[isAutofocusSelected]="isAutofocus()"
|
[isAutofocusSelected]="isAutofocus()"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
@if (enableErrorMessage() && hasError()) {
|
||||||
|
<mat-error>
|
||||||
|
@for (message of errorMessages(); track message) {
|
||||||
|
{{ message | translate }}
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@if (enableErrorMessage() && hasError()) {
|
|
||||||
<mat-error>
|
|
||||||
@for (message of errorMessages(); track message) {
|
|
||||||
{{ message | translate }}
|
|
||||||
}
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
@if (maxLength() !== 1e6) {
|
@if (maxLength() !== 1e6) {
|
||||||
<span>{{ value().length }} / {{ maxLength() }}</span>
|
<span>{{ value().length }} / {{ maxLength() }}</span>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Renderer2,
|
Renderer2,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
input,
|
input,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
// RxJS
|
// RxJS
|
||||||
@@ -45,11 +46,9 @@ export class PiBackgroundImageDirective implements AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
private destroy$: Subject<void> = new Subject<void>();
|
private destroy$: Subject<void> = new Subject<void>();
|
||||||
|
|
||||||
constructor(
|
private readonly deviceConfigsFacade = inject(DeviceConfigsFacade);
|
||||||
private deviceConfigsFacade: DeviceConfigsFacade,
|
private readonly element = inject(ElementRef);
|
||||||
private element: ElementRef,
|
private readonly renderer = inject(Renderer2);
|
||||||
private renderer: Renderer2,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.deviceConfigsFacade.device$
|
this.deviceConfigsFacade.device$
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
Inject,
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import {
|
import {
|
||||||
@@ -58,10 +58,11 @@ export class PiModalComponent {
|
|||||||
title: string | undefined;
|
title: string | undefined;
|
||||||
subtitle: string | undefined;
|
subtitle: string | undefined;
|
||||||
|
|
||||||
constructor(
|
readonly dialogRef = inject<MatDialogRef<PiModalComponent>>(MatDialogRef);
|
||||||
public dialogRef: MatDialogRef<PiModalComponent>,
|
readonly data = inject<CustomModalData | DialogModalData>(MAT_DIALOG_DATA);
|
||||||
@Inject(MAT_DIALOG_DATA) public data: CustomModalData | DialogModalData,
|
|
||||||
) {
|
constructor() {
|
||||||
|
const data = this.data;
|
||||||
this.title = data.title;
|
this.title = data.title;
|
||||||
this.subtitle = data.subtitle;
|
this.subtitle = data.subtitle;
|
||||||
if (data.type === ModalType.DIALOG) {
|
if (data.type === ModalType.DIALOG) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div>
|
<div>
|
||||||
<mat-icon [svgIcon]="noteStatus?.icon!"></mat-icon>
|
<mat-icon [svgIcon]="noteStatus()?.icon!"></mat-icon>
|
||||||
@if (title()) {
|
@if (title()) {
|
||||||
<h5 s>{{ title() }}</h5>
|
<h5 s>{{ title() }}</h5>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
Directive,
|
Directive,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
OnInit,
|
|
||||||
input,
|
input,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { NoteDataService } from '../infrastructure/note-data.service';
|
import { NoteDataService } from '../infrastructure/note-data.service';
|
||||||
import { NoteStatus } from '../entities/note.model';
|
import { NoteStatus } from '../entities/note.model';
|
||||||
@@ -37,7 +38,7 @@ export class PiNoteActionContentDirective {}
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [MatIcon, PiTextSmallDirective, PiTextXSmallDirective],
|
imports: [MatIcon, PiTextSmallDirective, PiTextXSmallDirective],
|
||||||
})
|
})
|
||||||
export class PiNoteComponent implements OnInit {
|
export class PiNoteComponent {
|
||||||
@HostBinding('class') get classes(): string {
|
@HostBinding('class') get classes(): string {
|
||||||
const alignmentValue = this.alignment();
|
const alignmentValue = this.alignment();
|
||||||
const alignmentClass =
|
const alignmentClass =
|
||||||
@@ -97,18 +98,16 @@ export class PiNoteComponent implements OnInit {
|
|||||||
readonly appearance = input<'fill' | 'ghost'>('fill');
|
readonly appearance = input<'fill' | 'ghost'>('fill');
|
||||||
readonly type = input<'status' | 'simple'>('status');
|
readonly type = input<'status' | 'simple'>('status');
|
||||||
|
|
||||||
noteStatus?: NoteStatus;
|
private readonly noteDataService = inject(NoteDataService);
|
||||||
|
|
||||||
constructor(private noteDataService: NoteDataService) {}
|
/**
|
||||||
|
* Antes se calculaba en `ngOnInit`, así que no seguía a `status` si cambiaba
|
||||||
ngOnInit(): void {
|
* después. Como `computed`, se recalcula solo.
|
||||||
this.noteStatus = this.getNoteStatus();
|
*/
|
||||||
}
|
readonly noteStatus = computed<NoteStatus | undefined>(() => {
|
||||||
|
|
||||||
getNoteStatus(): NoteStatus | undefined {
|
|
||||||
const noteStatusOptions = this.noteDataService.getNoteStatusData();
|
const noteStatusOptions = this.noteDataService.getNoteStatusData();
|
||||||
return noteStatusOptions.find(
|
return noteStatusOptions.find(
|
||||||
(noteStatus) => this.status() === noteStatus.statusType,
|
(noteStatus) => this.status() === noteStatus.statusType,
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
|
||||||
// Material
|
// Material
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
@@ -10,7 +10,7 @@ import { PiSnackBarComponent } from '../snack-bar/snack-bar.component';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class NotificationService {
|
export class NotificationService {
|
||||||
constructor(private _snackBar: MatSnackBar) {}
|
private readonly _snackBar = inject(MatSnackBar);
|
||||||
|
|
||||||
success(message: string, description?: string): void {
|
success(message: string, description?: string): void {
|
||||||
this._snackBar.openFromComponent(PiSnackBarComponent, {
|
this._snackBar.openFromComponent(PiSnackBarComponent, {
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
Inject,
|
|
||||||
Optional,
|
|
||||||
forwardRef,
|
forwardRef,
|
||||||
signal,
|
signal,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
MatSnackBarRef,
|
MatSnackBarRef,
|
||||||
@@ -73,7 +72,10 @@ export class PiSnackBarComponent {
|
|||||||
/** The instance of the component making up the content of the snack bar. */
|
/** The instance of the component making up the content of the snack bar. */
|
||||||
private readonly snackBarRef!: MatSnackBarRef<PiSnackBarComponent>;
|
private readonly snackBarRef!: MatSnackBarRef<PiSnackBarComponent>;
|
||||||
|
|
||||||
constructor(@Optional() @Inject(MAT_SNACK_BAR_DATA) public data: any) {
|
readonly data = inject(MAT_SNACK_BAR_DATA, { optional: true });
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const data = this.data;
|
||||||
if (data) {
|
if (data) {
|
||||||
this.message.set(data.message);
|
this.message.set(data.message);
|
||||||
this.icon.set(data.icon);
|
this.icon.set(data.icon);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
OnInit,
|
|
||||||
HostBinding,
|
HostBinding,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
input,
|
input,
|
||||||
model,
|
model,
|
||||||
output,
|
output,
|
||||||
|
effect,
|
||||||
|
untracked,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component';
|
import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component';
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ import { PiIconComponent } from '../../icon/icon/icon.component';
|
|||||||
forwardRef(() => PiPaginatorComponent),
|
forwardRef(() => PiPaginatorComponent),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PiPaginatorComponent implements OnInit {
|
export class PiPaginatorComponent {
|
||||||
@HostBinding('class') className = 'pi-paginator';
|
@HostBinding('class') className = 'pi-paginator';
|
||||||
readonly currentPage = model(1);
|
readonly currentPage = model(1);
|
||||||
readonly pageSize = input.required<number>();
|
readonly pageSize = input.required<number>();
|
||||||
@@ -36,8 +37,13 @@ export class PiPaginatorComponent implements OnInit {
|
|||||||
|
|
||||||
firstPagesRange = 0;
|
firstPagesRange = 0;
|
||||||
|
|
||||||
ngOnInit(): void {
|
constructor() {
|
||||||
this.initFirstPages();
|
// `initFirstPages` lee el input `totalPages`, que no está disponible en el
|
||||||
|
// constructor; con un efecto además se recalcula si cambia.
|
||||||
|
effect(() => {
|
||||||
|
this.totalPages();
|
||||||
|
untracked(() => this.initFirstPages());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get pages() {
|
get pages() {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { Directive, OnInit, input } from '@angular/core';
|
import { Directive, input } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
alignment,
|
alignment,
|
||||||
borderRadius,
|
borderRadius,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { status } from '../../util/status';
|
import { status } from '../../util/status';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class PiBasePanelComponent implements OnInit {
|
export abstract class PiBasePanelComponent {
|
||||||
readonly alignment = input<alignment>('center');
|
readonly alignment = input<alignment>('center');
|
||||||
readonly borderRadius = input<borderRadius>('large');
|
readonly borderRadius = input<borderRadius>('large');
|
||||||
readonly color = input<styleType>('white');
|
readonly color = input<styleType>('white');
|
||||||
@@ -38,22 +38,6 @@ export abstract class PiBasePanelComponent implements OnInit {
|
|||||||
|
|
||||||
readonly status = input.required<status | ''>();
|
readonly status = input.required<status | ''>();
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.alignmentClass;
|
|
||||||
this.borderRadiusClass;
|
|
||||||
this.colorClass;
|
|
||||||
this.flexDirectionClass;
|
|
||||||
this.justifyClass;
|
|
||||||
this.widthClass;
|
|
||||||
this.outlineClass;
|
|
||||||
this.paddingClass;
|
|
||||||
this.gapClass;
|
|
||||||
this.statusClass;
|
|
||||||
this.isStrokedClass;
|
|
||||||
this.modeClass;
|
|
||||||
this.opacityClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected get widthClass(): string | undefined {
|
protected get widthClass(): string | undefined {
|
||||||
const width = this.width();
|
const width = this.width();
|
||||||
return width && `pi-panel__width--${width}`;
|
return width && `pi-panel__width--${width}`;
|
||||||
|
|||||||
@@ -2,14 +2,13 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
OnChanges,
|
|
||||||
OnInit,
|
|
||||||
SimpleChanges,
|
|
||||||
TemplateRef,
|
TemplateRef,
|
||||||
input,
|
input,
|
||||||
model,
|
model,
|
||||||
output,
|
output,
|
||||||
contentChild,
|
contentChild,
|
||||||
|
effect,
|
||||||
|
untracked,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { ComponentFamilyClass } from '../../../styles';
|
import { ComponentFamilyClass } from '../../../styles';
|
||||||
@@ -50,10 +49,7 @@ import { TranslatePipe } from '@ngx-translate/core';
|
|||||||
TranslatePipe,
|
TranslatePipe,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PiExpansionPanelComponent
|
export class PiExpansionPanelComponent extends PiBasePanelComponent {
|
||||||
extends PiBasePanelComponent
|
|
||||||
implements OnInit, OnChanges
|
|
||||||
{
|
|
||||||
@HostBinding('class') get classes(): string {
|
@HostBinding('class') get classes(): string {
|
||||||
const type = this.type();
|
const type = this.type();
|
||||||
const typeClass = type && `pi-panel__expansion--type-${type}`;
|
const typeClass = type && `pi-panel__expansion--type-${type}`;
|
||||||
@@ -90,12 +86,16 @@ export class PiExpansionPanelComponent
|
|||||||
readonly expandableContentRef =
|
readonly expandableContentRef =
|
||||||
contentChild<TemplateRef<unknown>>('expandableContent');
|
contentChild<TemplateRef<unknown>>('expandableContent');
|
||||||
|
|
||||||
override ngOnInit(): void {
|
constructor() {
|
||||||
if (this.status() === 'disabled') this.isCollapsed.set(true);
|
super();
|
||||||
}
|
// Antes eran un `ngOnInit` y un `ngOnChanges` que hacían lo mismo (uno para
|
||||||
|
// el valor inicial y otro para los cambios). Con `status` siendo un input
|
||||||
ngOnChanges({ status }: SimpleChanges): void {
|
// de señal, un solo efecto cubre ambos casos.
|
||||||
if (status?.currentValue === 'disabled') this.isCollapsed.set(true);
|
effect(() => {
|
||||||
|
if (this.status() === 'disabled') {
|
||||||
|
untracked(() => this.isCollapsed.set(true));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle(): void {
|
toggle(): void {
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import { Directive, HostBinding, OnInit, input } from '@angular/core';
|
import { Directive, HostBinding, input } from '@angular/core';
|
||||||
import { padding } from '../../styles';
|
import { padding } from '../../styles';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class PiBasePanelContentDirective implements OnInit {
|
export abstract class PiBasePanelContentDirective {
|
||||||
@HostBinding('class') get commonClasses(): string {
|
@HostBinding('class') get commonClasses(): string {
|
||||||
return ['pi-panel-content', this.innerPaddingClass].join(' ');
|
return ['pi-panel-content', this.innerPaddingClass].join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly innerPadding = input<padding>();
|
readonly innerPadding = input<padding>();
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.innerPaddingClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected get innerPaddingClass(): string {
|
protected get innerPaddingClass(): string {
|
||||||
const innerPadding = this.innerPadding();
|
const innerPadding = this.innerPadding();
|
||||||
return innerPadding
|
return innerPadding
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
contentChildren,
|
contentChildren,
|
||||||
viewChildren,
|
viewChildren,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormValueControl, ValidationError } from '@angular/forms/signals';
|
import { FormValueControl, ValidationError } from '@angular/forms/signals';
|
||||||
import { PiSelectOptionComponent } from './select-option/select-option.component';
|
import { PiSelectOptionComponent } from './select-option/select-option.component';
|
||||||
@@ -197,10 +198,8 @@ export abstract class PiBaseSelectComponent<
|
|||||||
read: ElementRef,
|
read: ElementRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(
|
protected readonly el = inject(ElementRef);
|
||||||
protected el: ElementRef,
|
protected readonly _renderer = inject(Renderer2);
|
||||||
protected readonly _renderer: Renderer2,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
trackByFn(index: number): number | undefined {
|
trackByFn(index: number): number | undefined {
|
||||||
return index;
|
return index;
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
Renderer2,
|
|
||||||
effect,
|
effect,
|
||||||
input,
|
input,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@@ -78,13 +78,11 @@ export class PiSelectCarouselComponent
|
|||||||
* @param el - Element reference of the component.
|
* @param el - Element reference of the component.
|
||||||
* @param _renderer - Renderer to modify native elements.
|
* @param _renderer - Renderer to modify native elements.
|
||||||
*/
|
*/
|
||||||
constructor(
|
private readonly translateService = inject(TranslateService);
|
||||||
private readonly translateService: TranslateService,
|
private readonly _cdr = inject(ChangeDetectorRef);
|
||||||
protected override el: ElementRef,
|
|
||||||
protected override readonly _renderer: Renderer2,
|
constructor() {
|
||||||
private _cdr: ChangeDetectorRef,
|
super();
|
||||||
) {
|
|
||||||
super(el, _renderer);
|
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
this.childrenOptionsComponents();
|
this.childrenOptionsComponents();
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
Renderer2,
|
|
||||||
effect,
|
effect,
|
||||||
input,
|
input,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||||
import { PiBaseSelectComponent } from '../base-select.component';
|
import { PiBaseSelectComponent } from '../base-select.component';
|
||||||
@@ -88,13 +88,11 @@ export class PiSelectMultipleComponent
|
|||||||
*/
|
*/
|
||||||
readonly selectAllLabel = input<string>();
|
readonly selectAllLabel = input<string>();
|
||||||
|
|
||||||
constructor(
|
private readonly translateService = inject(TranslateService);
|
||||||
private readonly translateService: TranslateService,
|
private readonly _cdr = inject(ChangeDetectorRef);
|
||||||
protected override el: ElementRef,
|
|
||||||
protected override readonly _renderer: Renderer2,
|
constructor() {
|
||||||
private _cdr: ChangeDetectorRef,
|
super();
|
||||||
) {
|
|
||||||
super(el, _renderer);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sustituye al `merge(...).subscribe(...)` sobre `control.valueChanges` +
|
* Sustituye al `merge(...).subscribe(...)` sobre `control.valueChanges` +
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
HostListener,
|
HostListener,
|
||||||
Renderer2,
|
|
||||||
effect,
|
effect,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||||
import { PiBaseSelectComponent } from '../base-select.component';
|
import { PiBaseSelectComponent } from '../base-select.component';
|
||||||
@@ -84,13 +84,11 @@ export class PiSelectPeriodComponent
|
|||||||
* @param el - Element reference of the component.
|
* @param el - Element reference of the component.
|
||||||
* @param _renderer - Renderer to modify native elements.
|
* @param _renderer - Renderer to modify native elements.
|
||||||
*/
|
*/
|
||||||
constructor(
|
private readonly translateService = inject(TranslateService);
|
||||||
private readonly translateService: TranslateService,
|
private readonly _cdr = inject(ChangeDetectorRef);
|
||||||
protected override el: ElementRef,
|
|
||||||
protected override readonly _renderer: Renderer2,
|
constructor() {
|
||||||
private _cdr: ChangeDetectorRef,
|
super();
|
||||||
) {
|
|
||||||
super(el, _renderer);
|
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
this.childrenOptionsComponents();
|
this.childrenOptionsComponents();
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import {
|
|||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
HostListener,
|
HostListener,
|
||||||
Renderer2,
|
|
||||||
effect,
|
effect,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
input,
|
input,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||||
import { PiBaseSelectComponent } from '../base-select.component';
|
import { PiBaseSelectComponent } from '../base-select.component';
|
||||||
@@ -92,13 +92,11 @@ export class PiSelectComponent
|
|||||||
* @param el - Element reference of the component.
|
* @param el - Element reference of the component.
|
||||||
* @param _renderer - Renderer to modify native elements.
|
* @param _renderer - Renderer to modify native elements.
|
||||||
*/
|
*/
|
||||||
constructor(
|
private readonly translateService = inject(TranslateService);
|
||||||
private readonly translateService: TranslateService,
|
private readonly _cdr = inject(ChangeDetectorRef);
|
||||||
protected override el: ElementRef,
|
|
||||||
protected override readonly _renderer: Renderer2,
|
constructor() {
|
||||||
private _cdr: ChangeDetectorRef,
|
super();
|
||||||
) {
|
|
||||||
super(el, _renderer);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sustituye al `merge(...).subscribe(...)` sobre `control.valueChanges` +
|
* Sustituye al `merge(...).subscribe(...)` sobre `control.valueChanges` +
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Directive, TemplateRef } from '@angular/core';
|
import { Directive, TemplateRef, inject } from '@angular/core';
|
||||||
|
|
||||||
@Directive({ selector: '[pi-tab-content]' })
|
@Directive({ selector: '[pi-tab-content]' })
|
||||||
export class PiTabContentDirective {
|
export class PiTabContentDirective {
|
||||||
constructor(public templateRef: TemplateRef<any>) {}
|
public readonly templateRef = inject<any>(TemplateRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Directive({ selector: '[pi-tab-element]' })
|
@Directive({ selector: '[pi-tab-element]' })
|
||||||
export class PiTabElementDirective {
|
export class PiTabElementDirective {
|
||||||
constructor(public templateRef: TemplateRef<any>) {}
|
readonly templateRef = inject<TemplateRef<any>>(TemplateRef);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { Directive, HostBinding, OnInit, input } from '@angular/core';
|
import { Directive, HostBinding, input } from '@angular/core';
|
||||||
|
|
||||||
// Style definitions
|
// Style definitions
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from '../../styles';
|
} from '../../styles';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class PiBaseTextViewerComponent implements OnInit {
|
export abstract class PiBaseTextViewerComponent {
|
||||||
@HostBinding('class') get classes(): string {
|
@HostBinding('class') get classes(): string {
|
||||||
return [
|
return [
|
||||||
ComponentFamilyClass.Text,
|
ComponentFamilyClass.Text,
|
||||||
@@ -58,23 +58,6 @@ export abstract class PiBaseTextViewerComponent implements OnInit {
|
|||||||
readonly label = input<string | unknown>();
|
readonly label = input<string | unknown>();
|
||||||
readonly value = input<string>();
|
readonly value = input<string>();
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.alignmentClass;
|
|
||||||
this.borderTypeClass;
|
|
||||||
this.colorClass;
|
|
||||||
this.flexDirectionClass;
|
|
||||||
this.fontSizeClass;
|
|
||||||
this.fontStyleClass;
|
|
||||||
this.fontWeightClass;
|
|
||||||
this.justifyClass;
|
|
||||||
this.iconSizeClass;
|
|
||||||
this.iconColorClass;
|
|
||||||
this.isDisabledClass;
|
|
||||||
this.hasLabelClass;
|
|
||||||
this.hasValueClass;
|
|
||||||
this.iconAppearanceClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected get alignmentClass(): string {
|
protected get alignmentClass(): string {
|
||||||
const alignmentValue = this.alignment();
|
const alignmentValue = this.alignment();
|
||||||
return alignmentValue && `pi-text-viewer__align--${alignmentValue}`;
|
return alignmentValue && `pi-text-viewer__align--${alignmentValue}`;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import {
|
|||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
|
||||||
Renderer2,
|
Renderer2,
|
||||||
input,
|
input,
|
||||||
|
effect,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatTooltip, TooltipPosition } from '@angular/material/tooltip';
|
import { MatTooltip, TooltipPosition } from '@angular/material/tooltip';
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ import { MatTooltip, TooltipPosition } from '@angular/material/tooltip';
|
|||||||
selector: '[pi-tooltip]',
|
selector: '[pi-tooltip]',
|
||||||
providers: [MatTooltip],
|
providers: [MatTooltip],
|
||||||
})
|
})
|
||||||
export class PiTooltipDirective implements OnInit, OnDestroy {
|
export class PiTooltipDirective implements OnDestroy {
|
||||||
@HostBinding('class') className = 'pi-tooltip-dispatcher';
|
@HostBinding('class') className = 'pi-tooltip-dispatcher';
|
||||||
|
|
||||||
readonly tooltipText = input<string>();
|
readonly tooltipText = input<string>();
|
||||||
@@ -25,19 +26,24 @@ export class PiTooltipDirective implements OnInit, OnDestroy {
|
|||||||
private mouseoutListener!: () => void;
|
private mouseoutListener!: () => void;
|
||||||
private clickListener!: () => void;
|
private clickListener!: () => void;
|
||||||
|
|
||||||
constructor(
|
private readonly tooltip = inject(MatTooltip);
|
||||||
private tooltip: MatTooltip,
|
private readonly el = inject(ElementRef);
|
||||||
private el: ElementRef,
|
private readonly renderer = inject(Renderer2);
|
||||||
private renderer: Renderer2,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
constructor() {
|
||||||
this.tooltip.tooltipClass = [
|
// La clase depende de inputs, así que va en un efecto; los listeners no
|
||||||
'pi-tooltip',
|
// dependen de nada y se registran una sola vez.
|
||||||
this.typeClass,
|
effect(() => {
|
||||||
this.colorClass,
|
this.tooltip.tooltipClass = [
|
||||||
].join(' ');
|
'pi-tooltip',
|
||||||
|
this.typeClass,
|
||||||
|
this.colorClass,
|
||||||
|
].join(' ');
|
||||||
|
});
|
||||||
|
this.registerListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerListeners(): void {
|
||||||
this.mouseoverListener = this.renderer.listen(
|
this.mouseoverListener = this.renderer.listen(
|
||||||
this.el.nativeElement,
|
this.el.nativeElement,
|
||||||
'mouseover',
|
'mouseover',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { Directive, OnInit, input } from '@angular/core';
|
import { Directive, input } from '@angular/core';
|
||||||
import { itemsWidth } from '../../styles';
|
import { itemsWidth } from '../../styles';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '../../styles';
|
} from '../../styles';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class PiBaseWrapper implements OnInit {
|
export abstract class PiBaseWrapper {
|
||||||
readonly alignment = input<alignment>('center');
|
readonly alignment = input<alignment>('center');
|
||||||
readonly flexDirection = input<flexDirection>('row');
|
readonly flexDirection = input<flexDirection>('row');
|
||||||
readonly gap = input.required<gap>();
|
readonly gap = input.required<gap>();
|
||||||
@@ -23,17 +23,6 @@ export abstract class PiBaseWrapper implements OnInit {
|
|||||||
readonly maxWidth = input<maxWidth>('edge-to-edge');
|
readonly maxWidth = input<maxWidth>('edge-to-edge');
|
||||||
readonly itemsWidth = input<itemsWidth>();
|
readonly itemsWidth = input<itemsWidth>();
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.alignmentClass;
|
|
||||||
this.flexDirectionClass;
|
|
||||||
this.gridClass;
|
|
||||||
this.hasBackgroundClass;
|
|
||||||
this.hasPaddingClass;
|
|
||||||
this.justifyClass;
|
|
||||||
this.itemsWidthClass;
|
|
||||||
this.maxWidthClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected get maxWidthClass(): string | undefined {
|
protected get maxWidthClass(): string | undefined {
|
||||||
const maxWidthValue = this.maxWidth();
|
const maxWidthValue = this.maxWidth();
|
||||||
return maxWidthValue && `pi-wrapper__${maxWidthValue}`;
|
return maxWidthValue && `pi-wrapper__${maxWidthValue}`;
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import {
|
|||||||
Directive,
|
Directive,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
OnInit,
|
|
||||||
Renderer2,
|
Renderer2,
|
||||||
input,
|
input,
|
||||||
|
afterNextRender,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { unit } from '../models';
|
import { unit } from '../models';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class PiBaseAffixDirective implements OnInit {
|
export abstract class PiBaseAffixDirective {
|
||||||
@HostBinding('class') get classes(): string {
|
@HostBinding('class') get classes(): string {
|
||||||
return ['pi-affix', `pi-affix__appearance--${this.appearance()}`].join(' ');
|
return ['pi-affix', `pi-affix__appearance--${this.appearance()}`].join(' ');
|
||||||
}
|
}
|
||||||
@@ -17,12 +18,17 @@ export abstract class PiBaseAffixDirective implements OnInit {
|
|||||||
readonly unit = input<unit | string>();
|
readonly unit = input<unit | string>();
|
||||||
readonly appearance = input<'ghost' | 'fill'>('ghost');
|
readonly appearance = input<'ghost' | 'fill'>('ghost');
|
||||||
|
|
||||||
constructor(
|
private readonly renderer = inject(Renderer2);
|
||||||
private renderer: Renderer2,
|
private readonly el = inject(ElementRef);
|
||||||
private el: ElementRef,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
constructor() {
|
||||||
|
// Pinta DOM a partir de un input, así que no puede ir en el cuerpo del
|
||||||
|
// constructor (los inputs aún no tienen valor). `afterNextRender` corre una
|
||||||
|
// sola vez tras el primer render, que es lo que hacía el `ngOnInit`.
|
||||||
|
afterNextRender(() => this.renderUnit());
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderUnit(): void {
|
||||||
const unitValue = this.unit();
|
const unitValue = this.unit();
|
||||||
if (unitValue) {
|
if (unitValue) {
|
||||||
const span = this.renderer.createElement('span');
|
const span = this.renderer.createElement('span');
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
|
import { Inject, Injectable, PLATFORM_ID, inject } from '@angular/core';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -8,7 +8,9 @@ export class AmplitudeService {
|
|||||||
private amp: any;
|
private amp: any;
|
||||||
private ampEnabled = false;
|
private ampEnabled = false;
|
||||||
|
|
||||||
constructor(@Inject(PLATFORM_ID) public platformId: object) {
|
readonly platformId = inject<object>(PLATFORM_ID);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
if (isPlatformBrowser(this.platformId)) {
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
this.amp = (window as any).amplitude;
|
this.amp = (window as any).amplitude;
|
||||||
if (this.amp) this.ampEnabled = true;
|
if (this.amp) this.ampEnabled = true;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Directive, HostBinding, OnInit, input } from '@angular/core';
|
import { Directive, HostBinding, input } from '@angular/core';
|
||||||
import { density, mode, opacity, styleType } from '../../../styles';
|
import { density, mode, opacity, styleType } from '../../../styles';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class PiBaseColor implements OnInit {
|
export abstract class PiBaseColor {
|
||||||
@HostBinding('class') get commonClasses(): string {
|
@HostBinding('class') get commonClasses(): string {
|
||||||
return [this.densityClass, this.modeClass].join(' ');
|
return [this.densityClass, this.modeClass].join(' ');
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,6 @@ export abstract class PiBaseColor implements OnInit {
|
|||||||
readonly opacity = input<opacity>('100');
|
readonly opacity = input<opacity>('100');
|
||||||
readonly mode = input<mode>('dark');
|
readonly mode = input<mode>('dark');
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.densityClass;
|
|
||||||
}
|
|
||||||
protected get densityClass(): string {
|
protected get densityClass(): string {
|
||||||
const densityValue = this.density();
|
const densityValue = this.density();
|
||||||
return !!densityValue ? `pi-color__density--${densityValue}` : '';
|
return !!densityValue ? `pi-color__density--${densityValue}` : '';
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { HttpParams } from '@angular/common/http';
|
import { HttpParams } from '@angular/common/http';
|
||||||
import { Inject, PLATFORM_ID } from '@angular/core';
|
import { Inject, PLATFORM_ID, inject } from '@angular/core';
|
||||||
|
|
||||||
// Shared
|
// Shared
|
||||||
import { Params } from '../models';
|
import { Params } from '../models';
|
||||||
import { timer } from 'rxjs';
|
import { timer } from 'rxjs';
|
||||||
export class Commons {
|
export class Commons {
|
||||||
constructor(@Inject(PLATFORM_ID) public platformId: object) {}
|
readonly platformId = inject<object>(PLATFORM_ID);
|
||||||
|
|
||||||
static followUrl($event: MouseEvent, item: { url: string }): void {
|
static followUrl($event: MouseEvent, item: { url: string }): void {
|
||||||
Commons.followExternalUrl($event, item);
|
Commons.followExternalUrl($event, item);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { Inject, PLATFORM_ID } from '@angular/core';
|
import { Inject, PLATFORM_ID, inject } from '@angular/core';
|
||||||
import { Sector } from '../models';
|
import { Sector } from '../models';
|
||||||
import { SectorType, SectorUidMatchLocale } from '../models';
|
import { SectorType, SectorUidMatchLocale } from '../models';
|
||||||
|
|
||||||
export class SectorCommons {
|
export class SectorCommons {
|
||||||
constructor(@Inject(PLATFORM_ID) public platformId: object) {}
|
readonly platformId = inject<object>(PLATFORM_ID);
|
||||||
|
|
||||||
static getSectorLocaleFromUid(uid: string): string {
|
static getSectorLocaleFromUid(uid: string): string {
|
||||||
const sectorLocale: string = SectorUidMatchLocale[uid as SectorType];
|
const sectorLocale: string = SectorUidMatchLocale[uid as SectorType];
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Inject, PLATFORM_ID } from '@angular/core';
|
import { Inject, PLATFORM_ID, inject } from '@angular/core';
|
||||||
import { SelectOption } from '../../ui/select';
|
import { SelectOption } from '../../ui/select';
|
||||||
|
|
||||||
export class SelectOptionsCommons {
|
export class SelectOptionsCommons {
|
||||||
constructor(@Inject(PLATFORM_ID) public platformId: object) {}
|
readonly platformId = inject<object>(PLATFORM_ID);
|
||||||
|
|
||||||
static convertStringstoSelectOptions(
|
static convertStringstoSelectOptions(
|
||||||
values: Array<any>,
|
values: Array<any>,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||||
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
||||||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
|
import { Inject, Injectable, PLATFORM_ID, inject } from '@angular/core';
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
@@ -32,12 +32,12 @@ import { DeviceConfigsState } from '../state/device-configs.reducer';
|
|||||||
export class DeviceConfigsService {
|
export class DeviceConfigsService {
|
||||||
private htmlTag!: HTMLElement;
|
private htmlTag!: HTMLElement;
|
||||||
|
|
||||||
constructor(
|
private readonly store = inject<Store<DeviceConfigsState>>(Store);
|
||||||
private store: Store<DeviceConfigsState>,
|
private readonly breakpointObserver = inject(BreakpointObserver);
|
||||||
private breakpointObserver: BreakpointObserver,
|
private readonly document = inject<Document>(DOCUMENT);
|
||||||
@Inject(DOCUMENT) private document: Document,
|
readonly platformId = inject<Object>(PLATFORM_ID);
|
||||||
@Inject(PLATFORM_ID) public platformId: Object,
|
|
||||||
) {
|
constructor() {
|
||||||
this.deviceConfigsHandler();
|
this.deviceConfigsHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
|
||||||
// Rxjs
|
// Rxjs
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
@@ -15,10 +15,8 @@ import { DeviceConfigsService } from '../services/device-configs.service';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeviceConfigsEffects {
|
export class DeviceConfigsEffects {
|
||||||
constructor(
|
private readonly deviceConfigsService = inject(DeviceConfigsService);
|
||||||
private deviceConfigsService: DeviceConfigsService,
|
private readonly actions = inject(Actions);
|
||||||
private actions: Actions,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
getBrowser$ = createEffect(() => {
|
getBrowser$ = createEffect(() => {
|
||||||
return this.actions.pipe(
|
return this.actions.pipe(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { MatIconRegistry } from '@angular/material/icon';
|
import { MatIconRegistry } from '@angular/material/icon';
|
||||||
import { DomSanitizer } from '@angular/platform-browser';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
import { customIcons } from './icons.data';
|
import { customIcons } from './icons.data';
|
||||||
@@ -7,10 +7,8 @@ import { customIcons } from './icons.data';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class IconRegistryService {
|
export class IconRegistryService {
|
||||||
constructor(
|
private readonly matIconRegistry = inject(MatIconRegistry);
|
||||||
private matIconRegistry: MatIconRegistry,
|
private readonly domSanitizer = inject(DomSanitizer);
|
||||||
private domSanitizer: DomSanitizer,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
init(): void {
|
init(): void {
|
||||||
this.registerIcons(customIcons, 'assets/icons/');
|
this.registerIcons(customIcons, 'assets/icons/');
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { isPlatformBrowser } from '@angular/common';
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
|
import { Inject, Injectable, PLATFORM_ID, inject } from '@angular/core';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class LocalStorageService {
|
export class LocalStorageService {
|
||||||
constructor(@Inject(PLATFORM_ID) public platformId: Object) {}
|
readonly platformId = inject<Object>(PLATFORM_ID);
|
||||||
|
|
||||||
getItem(tag: string) {
|
getItem(tag: string) {
|
||||||
if (isPlatformBrowser(this.platformId)) {
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
|||||||
@@ -4,16 +4,15 @@ import {
|
|||||||
HostBinding,
|
HostBinding,
|
||||||
HostListener,
|
HostListener,
|
||||||
Renderer2,
|
Renderer2,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export abstract class PiBaseMenuEventItem {
|
export abstract class PiBaseMenuEventItem {
|
||||||
@HostBinding('class') className = 'pi-menu-event-item';
|
@HostBinding('class') className = 'pi-menu-event-item';
|
||||||
|
|
||||||
constructor(
|
protected readonly el = inject(ElementRef);
|
||||||
protected el: ElementRef,
|
protected readonly renderer = inject(Renderer2);
|
||||||
protected renderer: Renderer2,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@HostListener('click')
|
@HostListener('click')
|
||||||
onClick() {
|
onClick() {
|
||||||
|
|||||||
@@ -4,15 +4,14 @@ import {
|
|||||||
HostBinding,
|
HostBinding,
|
||||||
HostListener,
|
HostListener,
|
||||||
Renderer2,
|
Renderer2,
|
||||||
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
@Directive({ selector: '[pi-menu-event-children]' })
|
@Directive({ selector: '[pi-menu-event-children]' })
|
||||||
export class PiMenuEventChildrenDirective {
|
export class PiMenuEventChildrenDirective {
|
||||||
@HostBinding('class') className = 'pi-menu pi-menu__children';
|
@HostBinding('class') className = 'pi-menu pi-menu__children';
|
||||||
constructor(
|
private readonly el = inject(ElementRef);
|
||||||
private el: ElementRef,
|
private readonly renderer = inject(Renderer2);
|
||||||
private renderer: Renderer2,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@HostListener('click') onClickMenuItem() {
|
@HostListener('click') onClickMenuItem() {
|
||||||
const menuChildren = this.el.nativeElement;
|
const menuChildren = this.el.nativeElement;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ErrorHandler, Injectable, Injector } from '@angular/core';
|
import { ErrorHandler, Injectable, Injector, inject } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GlobalErrorHandler implements ErrorHandler {
|
export class GlobalErrorHandler implements ErrorHandler {
|
||||||
constructor(private injector: Injector) {}
|
private readonly injector = inject(Injector);
|
||||||
handleError(error: any) {
|
handleError(error: any) {
|
||||||
const router = this.injector.get(Router);
|
const router = this.injector.get(Router);
|
||||||
console.error(error.stack.toString());
|
console.error(error.stack.toString());
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { Inject, Injectable, Renderer2 } from '@angular/core';
|
import { Inject, Injectable, Renderer2, inject } from '@angular/core';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -8,7 +8,7 @@ import { Inject, Injectable, Renderer2 } from '@angular/core';
|
|||||||
export class SchemaService {
|
export class SchemaService {
|
||||||
script!: HTMLScriptElement;
|
script!: HTMLScriptElement;
|
||||||
|
|
||||||
constructor(@Inject(DOCUMENT) private doc: Document) {}
|
private readonly doc = inject<Document>(DOCUMENT);
|
||||||
|
|
||||||
loadJsScript(renderer: Renderer2, data: any): HTMLScriptElement {
|
loadJsScript(renderer: Renderer2, data: any): HTMLScriptElement {
|
||||||
const script = renderer.createElement('script');
|
const script = renderer.createElement('script');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ import { Observable } from 'rxjs';
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class I18nService {
|
export class I18nService {
|
||||||
constructor(private translateService: TranslateService) {}
|
private readonly translateService = inject(TranslateService);
|
||||||
|
|
||||||
setLanguage(lang: string): Observable<any> {
|
setLanguage(lang: string): Observable<any> {
|
||||||
return new Observable((observer) => {
|
return new Observable((observer) => {
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { TranslateLoader } from '@ngx-translate/core';
|
import { TranslateLoader } from '@ngx-translate/core';
|
||||||
import { makeStateKey, StateKey, TransferState } from '@angular/core';
|
import { makeStateKey, StateKey, TransferState, inject } from '@angular/core';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
export class TranslateServerLoader implements TranslateLoader {
|
export class TranslateServerLoader implements TranslateLoader {
|
||||||
constructor(
|
private readonly transferState = inject(TransferState);
|
||||||
private transferState: TransferState,
|
private readonly prefix = inject((string = 'i18n'));
|
||||||
private prefix: string = 'i18n',
|
private readonly suffix = inject((string = '.json'));
|
||||||
private suffix: string = '.json',
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public getTranslation(lang: string): Observable<any> {
|
public getTranslation(lang: string): Observable<any> {
|
||||||
return new Observable((observer) => {
|
return new Observable((observer) => {
|
||||||
|
|||||||
0
nx/core/package-json
Normal file
0
nx/core/package-json
Normal file
0
nx/core/project-json
Normal file
0
nx/core/project-json
Normal file
0
nx/js/dependencies-and-lockfile
Normal file
0
nx/js/dependencies-and-lockfile
Normal file
265
package-lock.json
generated
265
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user