fix scss
This commit is contained in:
@@ -31,6 +31,8 @@ const TRANSLATIONS: TranslationObject = {
|
||||
max: 'Máx',
|
||||
save: 'Guardar',
|
||||
reset: 'Borrar',
|
||||
search: 'Buscar…',
|
||||
noResults: 'Sin resultados',
|
||||
validator: {
|
||||
requiredField: 'Este campo es obligatorio',
|
||||
onlyNumbers: 'Sólo se admiten números',
|
||||
|
||||
@@ -81,6 +81,28 @@
|
||||
<pi-label>Fecha de nacimiento</pi-label>
|
||||
</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
|
||||
[formField]="f.shipping"
|
||||
[options]="shippingOptions"
|
||||
|
||||
@@ -91,6 +91,11 @@ import {
|
||||
PiSelectCarouselComponent,
|
||||
PiSelectComponent,
|
||||
PiSelectMultipleComponent,
|
||||
PiSearchableMultiSelectFieldComponent,
|
||||
PiSearchableSelectFieldComponent,
|
||||
PiSelectFieldComponent,
|
||||
PiSelectFieldOption,
|
||||
PiMultiSelectFieldComponent,
|
||||
PiSelectOptionComponent,
|
||||
PiSelectPeriodComponent,
|
||||
PiSlideToggleComponent,
|
||||
@@ -148,6 +153,10 @@ interface DemoModel {
|
||||
plan: SelectOptionValue | null;
|
||||
terms: boolean;
|
||||
shipping: string | null;
|
||||
language: string | null;
|
||||
country2: string | null;
|
||||
visited: string[];
|
||||
skills: string[];
|
||||
birthDate: Date | null;
|
||||
attachments: File[];
|
||||
}
|
||||
@@ -174,7 +183,7 @@ interface DemoModel {
|
||||
PiSmallButtonDirective,
|
||||
PiW100ButtonDirective,
|
||||
PiRadioButtonComponent,
|
||||
PiSwitchButtonComponent,
|
||||
PiSwitchButtonComponent,
|
||||
PiToggleButtonComponent,
|
||||
// Contenido
|
||||
PiIconComponent,
|
||||
@@ -240,6 +249,10 @@ interface DemoModel {
|
||||
PiSelectPeriodComponent,
|
||||
PiSelectCarouselComponent,
|
||||
PiSelectOptionComponent,
|
||||
PiSelectFieldComponent,
|
||||
PiMultiSelectFieldComponent,
|
||||
PiSearchableSelectFieldComponent,
|
||||
PiSearchableMultiSelectFieldComponent,
|
||||
// Datos
|
||||
PiDataGridComponent,
|
||||
PiCellDirective,
|
||||
@@ -285,6 +298,10 @@ export class CatalogPage {
|
||||
plan: null,
|
||||
terms: false,
|
||||
shipping: null,
|
||||
language: null,
|
||||
country2: null,
|
||||
visited: [],
|
||||
skills: [],
|
||||
birthDate: null,
|
||||
attachments: [],
|
||||
});
|
||||
@@ -312,6 +329,71 @@ export class CatalogPage {
|
||||
{ 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 = [
|
||||
{ id: 1, name: 'Básico', description: '9 €/mes' },
|
||||
{ id: 2, name: 'Pro', description: '29 €/mes' },
|
||||
|
||||
@@ -47,3 +47,35 @@
|
||||
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-spinner';
|
||||
@forward 'mat-radio-button';
|
||||
@forward 'mat-select';
|
||||
@forward 'mat-slider';
|
||||
@forward 'mat-snackbar';
|
||||
@forward 'mat-tooltip';
|
||||
|
||||
@@ -95,9 +95,3 @@
|
||||
.pi-shell__alternative {
|
||||
@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`).
|
||||
//
|
||||
// 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`
|
||||
// porque los overlays de Material (diálogos, snackbars, menús) se montan fuera
|
||||
// del árbol del shell y no heredarían nada.
|
||||
// varios temas en la misma página.
|
||||
//
|
||||
// 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 {
|
||||
// --- Botones ---------------------------------------------------------
|
||||
@@ -69,7 +73,6 @@
|
||||
// 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
|
||||
// Material). Los temas concretos se aplican encima; ver `_alternative.scss`.
|
||||
.pi-shell,
|
||||
.cdk-overlay-container {
|
||||
.pi-shell {
|
||||
@include defaultThemeTokens;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
TemplateRef,
|
||||
input,
|
||||
contentChild,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { status } from '../../../util/status';
|
||||
|
||||
@@ -25,18 +26,18 @@ export class PiAccordionHeaderDirective {
|
||||
// Accordion Item Header Content
|
||||
@Directive({ selector: '[pi-accordion-item-header-content]' })
|
||||
export class PiAccordionItemHeaderDirective {
|
||||
constructor(public templateRef: TemplateRef<any>) {}
|
||||
public readonly templateRef = inject<any>(TemplateRef);
|
||||
}
|
||||
// Accordion Item Summary Content
|
||||
@Directive({ selector: '[pi-accordion-item-summary-content]' })
|
||||
export class PiAccordionSummaryContentDirective {
|
||||
constructor(public templateRef: TemplateRef<any>) {}
|
||||
readonly templateRef = inject<TemplateRef<any>>(TemplateRef);
|
||||
}
|
||||
|
||||
// Accordion Item Expandable Content
|
||||
@Directive({ selector: '[pi-accordion-item-expandable-content]' })
|
||||
export class PiAccordionItemContentDirective {
|
||||
constructor(public templateRef: TemplateRef<any>) {}
|
||||
readonly templateRef = inject<TemplateRef<any>>(TemplateRef);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Angular
|
||||
import { Directive, OnInit, input, output } from '@angular/core';
|
||||
import { Directive, input, output } from '@angular/core';
|
||||
import {
|
||||
buttonSize,
|
||||
styleType,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
// Style definitions
|
||||
@Directive()
|
||||
export abstract class PiBaseButtonComponent implements OnInit {
|
||||
export abstract class PiBaseButtonComponent {
|
||||
readonly size = input<buttonSize>('medium');
|
||||
readonly color = input<styleType>('primary');
|
||||
readonly appearance = input<buttonStyle>('fill');
|
||||
@@ -19,11 +19,6 @@ export abstract class PiBaseButtonComponent implements OnInit {
|
||||
|
||||
readonly execute = output<MouseEvent>();
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sizeClass;
|
||||
this.flexDirectionClass;
|
||||
}
|
||||
|
||||
protected get colorClass(): string {
|
||||
return `pi-color__${this.color()}`;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// Angular
|
||||
import { Directive, HostBinding, OnInit, input, output } from '@angular/core';
|
||||
import { Directive, input, output } from '@angular/core';
|
||||
|
||||
// Style definitions
|
||||
import { alignment, context, gap, mode, opacity, padding } from '../../styles';
|
||||
|
||||
@Directive()
|
||||
export abstract class PiBaseCardComponent implements OnInit {
|
||||
export abstract class PiBaseCardComponent {
|
||||
readonly hasShadow = input(true);
|
||||
readonly isClickable = input(false);
|
||||
readonly isStroked = input(false);
|
||||
@@ -25,19 +25,6 @@ export abstract class PiBaseCardComponent implements OnInit {
|
||||
|
||||
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 {
|
||||
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({
|
||||
selector: '[pi-carousel-item]',
|
||||
host: { class: 'pi-carousel pi-carousel__item' },
|
||||
})
|
||||
export class PiCarouselItemDirective {
|
||||
constructor(
|
||||
public templateRef: TemplateRef<any>,
|
||||
public viewContainerRef: ViewContainerRef,
|
||||
) {}
|
||||
public readonly templateRef = inject<TemplateRef<any>>(TemplateRef);
|
||||
public readonly viewContainerRef = inject(ViewContainerRef);
|
||||
}
|
||||
|
||||
@@ -7,15 +7,15 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Renderer2,
|
||||
SimpleChanges,
|
||||
input,
|
||||
output,
|
||||
viewChild,
|
||||
contentChildren,
|
||||
inject,
|
||||
effect,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
|
||||
// Shared
|
||||
@@ -34,7 +34,7 @@ import { PiIconButtonComponent } from '../../button/icon-button/icon-button.comp
|
||||
imports: [PiIconButtonComponent, NgTemplateOutlet],
|
||||
})
|
||||
export class PiCarouselComponent
|
||||
implements OnInit, OnChanges, AfterViewInit, AfterViewChecked, OnDestroy
|
||||
implements AfterViewInit, AfterViewChecked, OnDestroy
|
||||
{
|
||||
@HostBinding('class') get classes(): string {
|
||||
const carouselItems = this.carouselItems();
|
||||
@@ -168,12 +168,26 @@ export class PiCarouselComponent
|
||||
private touchmoveListener!: () => void;
|
||||
private touchendListener!: () => void;
|
||||
|
||||
constructor(
|
||||
private cdr: ChangeDetectorRef,
|
||||
private el: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
private deviceConfig: DeviceConfigsFacade,
|
||||
) {}
|
||||
private readonly cdr = inject(ChangeDetectorRef);
|
||||
private readonly el = inject(ElementRef);
|
||||
private readonly renderer = inject(Renderer2);
|
||||
private readonly deviceConfig = inject(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.
|
||||
@@ -239,20 +253,6 @@ export class PiCarouselComponent
|
||||
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 {
|
||||
this.setItemsPerPage();
|
||||
this.setPages();
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
OnInit,
|
||||
input,
|
||||
viewChild,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { Device } from '../../../util/device-configs';
|
||||
import { Dataset } from '../../../util/models';
|
||||
@@ -48,14 +48,14 @@ registerChartComponents();
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [PiChartLegendComponent],
|
||||
})
|
||||
export class PiChartComponent implements OnInit, AfterViewInit {
|
||||
export class PiChartComponent implements AfterViewInit {
|
||||
readonly type = input<'line' | 'bar'>('line');
|
||||
readonly datasets = input.required<Array<Dataset>>();
|
||||
readonly device = input.required<Device>();
|
||||
readonly xAxisMarks = input<Array<string>>();
|
||||
readonly isComparativeChart = input<boolean>(true);
|
||||
|
||||
private _hoveredDatasetIndex!: number;
|
||||
private _hoveredDatasetIndex = -1;
|
||||
set hoveredDatasetIndex(index: number) {
|
||||
this._hoveredDatasetIndex = index;
|
||||
}
|
||||
@@ -69,10 +69,8 @@ export class PiChartComponent implements OnInit, AfterViewInit {
|
||||
|
||||
chart!: Chart;
|
||||
|
||||
constructor(
|
||||
private translate: TranslateService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
) {}
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly cdr = inject(ChangeDetectorRef);
|
||||
|
||||
get completedDatasets() {
|
||||
return this.datasets().map((dataset) => {
|
||||
@@ -87,10 +85,6 @@ export class PiChartComponent implements OnInit, AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.hoveredDatasetIndex = -1;
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.getLineChartConfigs();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
OnInit,
|
||||
TemplateRef,
|
||||
input,
|
||||
output,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
input,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { itemType } from '../entities/data-grid.model';
|
||||
|
||||
@@ -24,7 +25,7 @@ export class PiCellDirective implements AfterViewInit {
|
||||
readonly index = input.required<number>();
|
||||
readonly textAlign = input<'left' | 'center' | 'right'>('left');
|
||||
|
||||
constructor(private elRef: ElementRef) {}
|
||||
private readonly elRef = inject(ElementRef);
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
const index = this.index();
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
HostListener,
|
||||
Renderer2,
|
||||
input,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { columnsNumber } from '../entities/data-grid.model';
|
||||
|
||||
@@ -32,10 +33,8 @@ export class PiGridItemDirective {
|
||||
|
||||
readonly isVisited = input(false);
|
||||
|
||||
constructor(
|
||||
private elRef: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
) {}
|
||||
private readonly elRef = inject(ElementRef);
|
||||
private readonly renderer = inject(Renderer2);
|
||||
|
||||
@HostListener('click') selectItem(): void {
|
||||
const gridItem = this.elRef.nativeElement;
|
||||
|
||||
@@ -2,8 +2,9 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostBinding,
|
||||
OnInit,
|
||||
input,
|
||||
effect,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
@@ -22,7 +23,7 @@ import { take } from 'rxjs/operators';
|
||||
styleUrls: ['./base-data-table.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PiBaseDataTableComponent implements OnInit {
|
||||
export class PiBaseDataTableComponent {
|
||||
@HostBinding('class') get classes(): string {
|
||||
return [
|
||||
'pi-base-data-table',
|
||||
@@ -36,12 +37,12 @@ export class PiBaseDataTableComponent implements OnInit {
|
||||
readonly type = input<'header' | 'data'>('data');
|
||||
readonly hideValues = input(false);
|
||||
|
||||
constructor(private store: Store<DeviceConfigsState>) {}
|
||||
private readonly store = inject<Store<DeviceConfigsState>>(Store);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.dataDirectionClass;
|
||||
this.typeClass;
|
||||
this.hideHeader();
|
||||
constructor() {
|
||||
// `hideHeader` lee inputs; las otras dos líneas del antiguo `ngOnInit` solo
|
||||
// leían getters y no hacían nada.
|
||||
effect(() => this.hideHeader());
|
||||
}
|
||||
|
||||
get dataDirectionClass(): string {
|
||||
|
||||
@@ -8,15 +8,13 @@ import {
|
||||
input,
|
||||
contentChild,
|
||||
viewChild,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { textStyle, justify, borderType } from '../../../styles';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
// Store
|
||||
import {
|
||||
DeviceConfigsState,
|
||||
DeviceTypeSelectors,
|
||||
} from '../../../util/device-configs';
|
||||
import { DeviceConfigsState } from '../../../util/device-configs';
|
||||
|
||||
// RxJS
|
||||
import { take } from 'rxjs/operators';
|
||||
@@ -44,7 +42,7 @@ export class PiDataTableItemComponent implements AfterViewInit {
|
||||
readonly item = contentChild<TemplateRef<any>>('item');
|
||||
readonly componentItem = viewChild<ElementRef>('componentItem');
|
||||
|
||||
constructor(private store: Store<DeviceConfigsState>) {}
|
||||
private readonly store = inject<Store<DeviceConfigsState>>(Store);
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.setCellClassByIndex();
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Component,
|
||||
HostBinding,
|
||||
forwardRef,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { LayoutTypeClass } from '../../../styles';
|
||||
@@ -39,7 +40,7 @@ import { TranslatePipe } from '@ngx-translate/core';
|
||||
export class PiNotFoundComponent {
|
||||
@HostBinding('class') className = LayoutTypeClass.VerticallyCentered;
|
||||
|
||||
constructor(private location: Location) {}
|
||||
private readonly location = inject(Location);
|
||||
|
||||
previous(): void {
|
||||
this.location.back();
|
||||
|
||||
@@ -166,7 +166,9 @@ export class PiFileUploadComponent implements FormValueControl<File[]> {
|
||||
}
|
||||
|
||||
// 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();
|
||||
if (this.multiple() && maxFiles !== undefined && next.length > maxFiles) {
|
||||
|
||||
@@ -20,12 +20,11 @@
|
||||
pi-autofocus
|
||||
[isAutofocusSelected]="isAutofocus()"
|
||||
/>
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</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 {
|
||||
MatFormField,
|
||||
MatLabel,
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
import {
|
||||
Directive,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
input,
|
||||
afterNextRender,
|
||||
inject,
|
||||
effect,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
|
||||
@Directive({ selector: '[pi-autofocus]' })
|
||||
export class PiAutofocusDirective implements OnInit, OnChanges {
|
||||
export class PiAutofocusDirective {
|
||||
readonly isAutofocusSelected = input<boolean>();
|
||||
|
||||
constructor(private matInput: MatInput) {}
|
||||
private readonly matInput = inject(MatInput);
|
||||
|
||||
ngOnInit() {
|
||||
if (this.isAutofocusSelected()) {
|
||||
setTimeout(() => this.matInput.focus());
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes) {
|
||||
constructor() {
|
||||
// `afterNextRender` sustituye al `ngOnInit` + `setTimeout`: ya corre con la
|
||||
// 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(() => {
|
||||
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,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
OnInit,
|
||||
Renderer2,
|
||||
input,
|
||||
afterNextRender,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
|
||||
@Directive({ selector: 'pi-label' })
|
||||
export class PiLabelDirective implements OnInit {
|
||||
export class PiLabelDirective {
|
||||
@HostBinding('class') get classes(): string {
|
||||
const validationClass = this.isRequired() ? 'is-required' : '';
|
||||
return ['pi-label', validationClass].join(' ');
|
||||
@@ -17,12 +18,15 @@ export class PiLabelDirective implements OnInit {
|
||||
|
||||
readonly isRequired = input<boolean>(false);
|
||||
|
||||
constructor(
|
||||
private renderer: Renderer2,
|
||||
private el: ElementRef,
|
||||
) {}
|
||||
private readonly renderer = inject(Renderer2);
|
||||
private readonly el = inject(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()) {
|
||||
const span = this.renderer.createElement('span');
|
||||
const text = this.renderer.createText('*');
|
||||
|
||||
@@ -17,12 +17,11 @@
|
||||
pi-autofocus
|
||||
[isAutofocusSelected]="isAutofocus()"
|
||||
/>
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
pi-autofocus
|
||||
[isAutofocusSelected]="isAutofocus()"
|
||||
/>
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</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);
|
||||
width: 100%;
|
||||
}
|
||||
.mat-mdc-form-field-error {
|
||||
margin-left: 3.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +38,11 @@
|
||||
pi-autofocus
|
||||
[isAutofocusSelected]="isAutofocus()"
|
||||
/>
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</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 './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
|
||||
[isAutofocusSelected]="isAutofocus()"
|
||||
/>
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
@@ -24,12 +24,11 @@
|
||||
pi-autofocus
|
||||
[isAutofocusSelected]="isAutofocus()"
|
||||
/>
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
@@ -16,12 +16,11 @@
|
||||
pi-autofocus
|
||||
[isAutofocusSelected]="isAutofocus()"
|
||||
/>
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</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],
|
||||
host: { role: 'radiogroup' },
|
||||
})
|
||||
export class PiRadioGroupComponent<TValue = unknown>
|
||||
implements FormValueControl<TValue | null>
|
||||
{
|
||||
export class PiRadioGroupComponent<
|
||||
TValue = unknown,
|
||||
> implements FormValueControl<TValue | null> {
|
||||
@HostBinding('class') get classes(): string {
|
||||
return [
|
||||
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
|
||||
[isAutofocusSelected]="isAutofocus()"
|
||||
/>
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
pi-autofocus
|
||||
[isAutofocusSelected]="isAutofocus()"
|
||||
></textarea>
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@if (enableErrorMessage() && hasError()) {
|
||||
<mat-error>
|
||||
@for (message of errorMessages(); track message) {
|
||||
{{ message | translate }}
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
@if (maxLength() !== 1e6) {
|
||||
<span>{{ value().length }} / {{ maxLength() }}</span>
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Renderer2,
|
||||
OnDestroy,
|
||||
input,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
|
||||
// RxJS
|
||||
@@ -45,11 +46,9 @@ export class PiBackgroundImageDirective implements AfterViewInit, OnDestroy {
|
||||
|
||||
private destroy$: Subject<void> = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private deviceConfigsFacade: DeviceConfigsFacade,
|
||||
private element: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
) {}
|
||||
private readonly deviceConfigsFacade = inject(DeviceConfigsFacade);
|
||||
private readonly element = inject(ElementRef);
|
||||
private readonly renderer = inject(Renderer2);
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.deviceConfigsFacade.device$
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostBinding,
|
||||
Inject,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {
|
||||
@@ -58,10 +58,11 @@ export class PiModalComponent {
|
||||
title: string | undefined;
|
||||
subtitle: string | undefined;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<PiModalComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: CustomModalData | DialogModalData,
|
||||
) {
|
||||
readonly dialogRef = inject<MatDialogRef<PiModalComponent>>(MatDialogRef);
|
||||
readonly data = inject<CustomModalData | DialogModalData>(MAT_DIALOG_DATA);
|
||||
|
||||
constructor() {
|
||||
const data = this.data;
|
||||
this.title = data.title;
|
||||
this.subtitle = data.subtitle;
|
||||
if (data.type === ModalType.DIALOG) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div>
|
||||
<mat-icon [svgIcon]="noteStatus?.icon!"></mat-icon>
|
||||
<mat-icon [svgIcon]="noteStatus()?.icon!"></mat-icon>
|
||||
@if (title()) {
|
||||
<h5 s>{{ title() }}</h5>
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ import {
|
||||
Component,
|
||||
Directive,
|
||||
HostBinding,
|
||||
OnInit,
|
||||
input,
|
||||
computed,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { NoteDataService } from '../infrastructure/note-data.service';
|
||||
import { NoteStatus } from '../entities/note.model';
|
||||
@@ -37,7 +38,7 @@ export class PiNoteActionContentDirective {}
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [MatIcon, PiTextSmallDirective, PiTextXSmallDirective],
|
||||
})
|
||||
export class PiNoteComponent implements OnInit {
|
||||
export class PiNoteComponent {
|
||||
@HostBinding('class') get classes(): string {
|
||||
const alignmentValue = this.alignment();
|
||||
const alignmentClass =
|
||||
@@ -97,18 +98,16 @@ export class PiNoteComponent implements OnInit {
|
||||
readonly appearance = input<'fill' | 'ghost'>('fill');
|
||||
readonly type = input<'status' | 'simple'>('status');
|
||||
|
||||
noteStatus?: NoteStatus;
|
||||
private readonly noteDataService = inject(NoteDataService);
|
||||
|
||||
constructor(private noteDataService: NoteDataService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.noteStatus = this.getNoteStatus();
|
||||
}
|
||||
|
||||
getNoteStatus(): NoteStatus | undefined {
|
||||
/**
|
||||
* Antes se calculaba en `ngOnInit`, así que no seguía a `status` si cambiaba
|
||||
* después. Como `computed`, se recalcula solo.
|
||||
*/
|
||||
readonly noteStatus = computed<NoteStatus | undefined>(() => {
|
||||
const noteStatusOptions = this.noteDataService.getNoteStatusData();
|
||||
return noteStatusOptions.find(
|
||||
(noteStatus) => this.status() === noteStatus.statusType,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
|
||||
// Material
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
@@ -10,7 +10,7 @@ import { PiSnackBarComponent } from '../snack-bar/snack-bar.component';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NotificationService {
|
||||
constructor(private _snackBar: MatSnackBar) {}
|
||||
private readonly _snackBar = inject(MatSnackBar);
|
||||
|
||||
success(message: string, description?: string): void {
|
||||
this._snackBar.openFromComponent(PiSnackBarComponent, {
|
||||
|
||||
@@ -2,10 +2,9 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostBinding,
|
||||
Inject,
|
||||
Optional,
|
||||
forwardRef,
|
||||
signal,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
MatSnackBarRef,
|
||||
@@ -73,7 +72,10 @@ export class PiSnackBarComponent {
|
||||
/** The instance of the component making up the content of the snack bar. */
|
||||
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) {
|
||||
this.message.set(data.message);
|
||||
this.icon.set(data.icon);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
OnInit,
|
||||
HostBinding,
|
||||
forwardRef,
|
||||
input,
|
||||
model,
|
||||
output,
|
||||
effect,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { PiIconButtonComponent } from '../../button/icon-button/icon-button.component';
|
||||
|
||||
@@ -23,7 +24,7 @@ import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||
forwardRef(() => PiPaginatorComponent),
|
||||
],
|
||||
})
|
||||
export class PiPaginatorComponent implements OnInit {
|
||||
export class PiPaginatorComponent {
|
||||
@HostBinding('class') className = 'pi-paginator';
|
||||
readonly currentPage = model(1);
|
||||
readonly pageSize = input.required<number>();
|
||||
@@ -36,8 +37,13 @@ export class PiPaginatorComponent implements OnInit {
|
||||
|
||||
firstPagesRange = 0;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initFirstPages();
|
||||
constructor() {
|
||||
// `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() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Angular
|
||||
import { Directive, OnInit, input } from '@angular/core';
|
||||
import { Directive, input } from '@angular/core';
|
||||
import {
|
||||
alignment,
|
||||
borderRadius,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { status } from '../../util/status';
|
||||
|
||||
@Directive()
|
||||
export abstract class PiBasePanelComponent implements OnInit {
|
||||
export abstract class PiBasePanelComponent {
|
||||
readonly alignment = input<alignment>('center');
|
||||
readonly borderRadius = input<borderRadius>('large');
|
||||
readonly color = input<styleType>('white');
|
||||
@@ -38,22 +38,6 @@ export abstract class PiBasePanelComponent implements OnInit {
|
||||
|
||||
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 {
|
||||
const width = this.width();
|
||||
return width && `pi-panel__width--${width}`;
|
||||
|
||||
@@ -2,14 +2,13 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
HostBinding,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
TemplateRef,
|
||||
input,
|
||||
model,
|
||||
output,
|
||||
contentChild,
|
||||
effect,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ComponentFamilyClass } from '../../../styles';
|
||||
@@ -50,10 +49,7 @@ import { TranslatePipe } from '@ngx-translate/core';
|
||||
TranslatePipe,
|
||||
],
|
||||
})
|
||||
export class PiExpansionPanelComponent
|
||||
extends PiBasePanelComponent
|
||||
implements OnInit, OnChanges
|
||||
{
|
||||
export class PiExpansionPanelComponent extends PiBasePanelComponent {
|
||||
@HostBinding('class') get classes(): string {
|
||||
const type = this.type();
|
||||
const typeClass = type && `pi-panel__expansion--type-${type}`;
|
||||
@@ -90,12 +86,16 @@ export class PiExpansionPanelComponent
|
||||
readonly expandableContentRef =
|
||||
contentChild<TemplateRef<unknown>>('expandableContent');
|
||||
|
||||
override ngOnInit(): void {
|
||||
if (this.status() === 'disabled') this.isCollapsed.set(true);
|
||||
}
|
||||
|
||||
ngOnChanges({ status }: SimpleChanges): void {
|
||||
if (status?.currentValue === 'disabled') this.isCollapsed.set(true);
|
||||
constructor() {
|
||||
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
|
||||
// de señal, un solo efecto cubre ambos casos.
|
||||
effect(() => {
|
||||
if (this.status() === 'disabled') {
|
||||
untracked(() => this.isCollapsed.set(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
@Directive()
|
||||
export abstract class PiBasePanelContentDirective implements OnInit {
|
||||
export abstract class PiBasePanelContentDirective {
|
||||
@HostBinding('class') get commonClasses(): string {
|
||||
return ['pi-panel-content', this.innerPaddingClass].join(' ');
|
||||
}
|
||||
|
||||
readonly innerPadding = input<padding>();
|
||||
|
||||
ngOnInit(): void {
|
||||
this.innerPaddingClass;
|
||||
}
|
||||
|
||||
protected get innerPaddingClass(): string {
|
||||
const innerPadding = this.innerPadding();
|
||||
return innerPadding
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
contentChildren,
|
||||
viewChildren,
|
||||
ChangeDetectionStrategy,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { FormValueControl, ValidationError } from '@angular/forms/signals';
|
||||
import { PiSelectOptionComponent } from './select-option/select-option.component';
|
||||
@@ -197,10 +198,8 @@ export abstract class PiBaseSelectComponent<
|
||||
read: ElementRef,
|
||||
});
|
||||
|
||||
constructor(
|
||||
protected el: ElementRef,
|
||||
protected readonly _renderer: Renderer2,
|
||||
) {}
|
||||
protected readonly el = inject(ElementRef);
|
||||
protected readonly _renderer = inject(Renderer2);
|
||||
|
||||
trackByFn(index: number): number | undefined {
|
||||
return index;
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
Renderer2,
|
||||
effect,
|
||||
input,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||
import _ from 'lodash';
|
||||
@@ -78,13 +78,11 @@ export class PiSelectCarouselComponent
|
||||
* @param el - Element reference of the component.
|
||||
* @param _renderer - Renderer to modify native elements.
|
||||
*/
|
||||
constructor(
|
||||
private readonly translateService: TranslateService,
|
||||
protected override el: ElementRef,
|
||||
protected override readonly _renderer: Renderer2,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
) {
|
||||
super(el, _renderer);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
private readonly _cdr = inject(ChangeDetectorRef);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
effect(() => {
|
||||
this.childrenOptionsComponents();
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
Component,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
Renderer2,
|
||||
effect,
|
||||
input,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||
import { PiBaseSelectComponent } from '../base-select.component';
|
||||
@@ -88,13 +88,11 @@ export class PiSelectMultipleComponent
|
||||
*/
|
||||
readonly selectAllLabel = input<string>();
|
||||
|
||||
constructor(
|
||||
private readonly translateService: TranslateService,
|
||||
protected override el: ElementRef,
|
||||
protected override readonly _renderer: Renderer2,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
) {
|
||||
super(el, _renderer);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
private readonly _cdr = inject(ChangeDetectorRef);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
/**
|
||||
* Sustituye al `merge(...).subscribe(...)` sobre `control.valueChanges` +
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Renderer2,
|
||||
effect,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||
import { PiBaseSelectComponent } from '../base-select.component';
|
||||
@@ -84,13 +84,11 @@ export class PiSelectPeriodComponent
|
||||
* @param el - Element reference of the component.
|
||||
* @param _renderer - Renderer to modify native elements.
|
||||
*/
|
||||
constructor(
|
||||
private readonly translateService: TranslateService,
|
||||
protected override el: ElementRef,
|
||||
protected override readonly _renderer: Renderer2,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
) {
|
||||
super(el, _renderer);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
private readonly _cdr = inject(ChangeDetectorRef);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
effect(() => {
|
||||
this.childrenOptionsComponents();
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Renderer2,
|
||||
effect,
|
||||
forwardRef,
|
||||
input,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||
import { PiBaseSelectComponent } from '../base-select.component';
|
||||
@@ -92,13 +92,11 @@ export class PiSelectComponent
|
||||
* @param el - Element reference of the component.
|
||||
* @param _renderer - Renderer to modify native elements.
|
||||
*/
|
||||
constructor(
|
||||
private readonly translateService: TranslateService,
|
||||
protected override el: ElementRef,
|
||||
protected override readonly _renderer: Renderer2,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
) {
|
||||
super(el, _renderer);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
private readonly _cdr = inject(ChangeDetectorRef);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
/**
|
||||
* 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]' })
|
||||
export class PiTabContentDirective {
|
||||
constructor(public templateRef: TemplateRef<any>) {}
|
||||
public readonly templateRef = inject<any>(TemplateRef);
|
||||
}
|
||||
|
||||
@Directive({ selector: '[pi-tab-element]' })
|
||||
export class PiTabElementDirective {
|
||||
constructor(public templateRef: TemplateRef<any>) {}
|
||||
readonly templateRef = inject<TemplateRef<any>>(TemplateRef);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Angular
|
||||
import { Directive, HostBinding, OnInit, input } from '@angular/core';
|
||||
import { Directive, HostBinding, input } from '@angular/core';
|
||||
|
||||
// Style definitions
|
||||
import {
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '../../styles';
|
||||
|
||||
@Directive()
|
||||
export abstract class PiBaseTextViewerComponent implements OnInit {
|
||||
export abstract class PiBaseTextViewerComponent {
|
||||
@HostBinding('class') get classes(): string {
|
||||
return [
|
||||
ComponentFamilyClass.Text,
|
||||
@@ -58,23 +58,6 @@ export abstract class PiBaseTextViewerComponent implements OnInit {
|
||||
readonly label = input<string | unknown>();
|
||||
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 {
|
||||
const alignmentValue = this.alignment();
|
||||
return alignmentValue && `pi-text-viewer__align--${alignmentValue}`;
|
||||
|
||||
@@ -3,9 +3,10 @@ import {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Renderer2,
|
||||
input,
|
||||
effect,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatTooltip, TooltipPosition } from '@angular/material/tooltip';
|
||||
|
||||
@@ -13,7 +14,7 @@ import { MatTooltip, TooltipPosition } from '@angular/material/tooltip';
|
||||
selector: '[pi-tooltip]',
|
||||
providers: [MatTooltip],
|
||||
})
|
||||
export class PiTooltipDirective implements OnInit, OnDestroy {
|
||||
export class PiTooltipDirective implements OnDestroy {
|
||||
@HostBinding('class') className = 'pi-tooltip-dispatcher';
|
||||
|
||||
readonly tooltipText = input<string>();
|
||||
@@ -25,19 +26,24 @@ export class PiTooltipDirective implements OnInit, OnDestroy {
|
||||
private mouseoutListener!: () => void;
|
||||
private clickListener!: () => void;
|
||||
|
||||
constructor(
|
||||
private tooltip: MatTooltip,
|
||||
private el: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
) {}
|
||||
private readonly tooltip = inject(MatTooltip);
|
||||
private readonly el = inject(ElementRef);
|
||||
private readonly renderer = inject(Renderer2);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.tooltip.tooltipClass = [
|
||||
'pi-tooltip',
|
||||
this.typeClass,
|
||||
this.colorClass,
|
||||
].join(' ');
|
||||
constructor() {
|
||||
// La clase depende de inputs, así que va en un efecto; los listeners no
|
||||
// dependen de nada y se registran una sola vez.
|
||||
effect(() => {
|
||||
this.tooltip.tooltipClass = [
|
||||
'pi-tooltip',
|
||||
this.typeClass,
|
||||
this.colorClass,
|
||||
].join(' ');
|
||||
});
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this.mouseoverListener = this.renderer.listen(
|
||||
this.el.nativeElement,
|
||||
'mouseover',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Angular
|
||||
import { Directive, OnInit, input } from '@angular/core';
|
||||
import { Directive, input } from '@angular/core';
|
||||
import { itemsWidth } from '../../styles';
|
||||
|
||||
import {
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '../../styles';
|
||||
|
||||
@Directive()
|
||||
export abstract class PiBaseWrapper implements OnInit {
|
||||
export abstract class PiBaseWrapper {
|
||||
readonly alignment = input<alignment>('center');
|
||||
readonly flexDirection = input<flexDirection>('row');
|
||||
readonly gap = input.required<gap>();
|
||||
@@ -23,17 +23,6 @@ export abstract class PiBaseWrapper implements OnInit {
|
||||
readonly maxWidth = input<maxWidth>('edge-to-edge');
|
||||
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 {
|
||||
const maxWidthValue = this.maxWidth();
|
||||
return maxWidthValue && `pi-wrapper__${maxWidthValue}`;
|
||||
|
||||
@@ -2,14 +2,15 @@ import {
|
||||
Directive,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
OnInit,
|
||||
Renderer2,
|
||||
input,
|
||||
afterNextRender,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { unit } from '../models';
|
||||
|
||||
@Directive()
|
||||
export abstract class PiBaseAffixDirective implements OnInit {
|
||||
export abstract class PiBaseAffixDirective {
|
||||
@HostBinding('class') get classes(): string {
|
||||
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 appearance = input<'ghost' | 'fill'>('ghost');
|
||||
|
||||
constructor(
|
||||
private renderer: Renderer2,
|
||||
private el: ElementRef,
|
||||
) {}
|
||||
private readonly renderer = inject(Renderer2);
|
||||
private readonly el = inject(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();
|
||||
if (unitValue) {
|
||||
const span = this.renderer.createElement('span');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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',
|
||||
@@ -8,7 +8,9 @@ export class AmplitudeService {
|
||||
private amp: any;
|
||||
private ampEnabled = false;
|
||||
|
||||
constructor(@Inject(PLATFORM_ID) public platformId: object) {
|
||||
readonly platformId = inject<object>(PLATFORM_ID);
|
||||
|
||||
constructor() {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
this.amp = (window as any).amplitude;
|
||||
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';
|
||||
|
||||
@Directive()
|
||||
export abstract class PiBaseColor implements OnInit {
|
||||
export abstract class PiBaseColor {
|
||||
@HostBinding('class') get commonClasses(): string {
|
||||
return [this.densityClass, this.modeClass].join(' ');
|
||||
}
|
||||
@@ -10,9 +10,6 @@ export abstract class PiBaseColor implements OnInit {
|
||||
readonly opacity = input<opacity>('100');
|
||||
readonly mode = input<mode>('dark');
|
||||
|
||||
ngOnInit(): void {
|
||||
this.densityClass;
|
||||
}
|
||||
protected get densityClass(): string {
|
||||
const densityValue = this.density();
|
||||
return !!densityValue ? `pi-color__density--${densityValue}` : '';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Angular
|
||||
import { HttpParams } from '@angular/common/http';
|
||||
import { Inject, PLATFORM_ID } from '@angular/core';
|
||||
import { Inject, PLATFORM_ID, inject } from '@angular/core';
|
||||
|
||||
// Shared
|
||||
import { Params } from '../models';
|
||||
import { timer } from 'rxjs';
|
||||
export class Commons {
|
||||
constructor(@Inject(PLATFORM_ID) public platformId: object) {}
|
||||
readonly platformId = inject<object>(PLATFORM_ID);
|
||||
|
||||
static followUrl($event: MouseEvent, item: { url: string }): void {
|
||||
Commons.followExternalUrl($event, item);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Angular
|
||||
import { Inject, PLATFORM_ID } from '@angular/core';
|
||||
import { Inject, PLATFORM_ID, inject } from '@angular/core';
|
||||
import { Sector } from '../models';
|
||||
import { SectorType, SectorUidMatchLocale } from '../models';
|
||||
|
||||
export class SectorCommons {
|
||||
constructor(@Inject(PLATFORM_ID) public platformId: object) {}
|
||||
readonly platformId = inject<object>(PLATFORM_ID);
|
||||
|
||||
static getSectorLocaleFromUid(uid: string): string {
|
||||
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';
|
||||
|
||||
export class SelectOptionsCommons {
|
||||
constructor(@Inject(PLATFORM_ID) public platformId: object) {}
|
||||
readonly platformId = inject<object>(PLATFORM_ID);
|
||||
|
||||
static convertStringstoSelectOptions(
|
||||
values: Array<any>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
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';
|
||||
|
||||
@@ -32,12 +32,12 @@ import { DeviceConfigsState } from '../state/device-configs.reducer';
|
||||
export class DeviceConfigsService {
|
||||
private htmlTag!: HTMLElement;
|
||||
|
||||
constructor(
|
||||
private store: Store<DeviceConfigsState>,
|
||||
private breakpointObserver: BreakpointObserver,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
@Inject(PLATFORM_ID) public platformId: Object,
|
||||
) {
|
||||
private readonly store = inject<Store<DeviceConfigsState>>(Store);
|
||||
private readonly breakpointObserver = inject(BreakpointObserver);
|
||||
private readonly document = inject<Document>(DOCUMENT);
|
||||
readonly platformId = inject<Object>(PLATFORM_ID);
|
||||
|
||||
constructor() {
|
||||
this.deviceConfigsHandler();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
|
||||
// Rxjs
|
||||
import { of } from 'rxjs';
|
||||
@@ -15,10 +15,8 @@ import { DeviceConfigsService } from '../services/device-configs.service';
|
||||
|
||||
@Injectable()
|
||||
export class DeviceConfigsEffects {
|
||||
constructor(
|
||||
private deviceConfigsService: DeviceConfigsService,
|
||||
private actions: Actions,
|
||||
) {}
|
||||
private readonly deviceConfigsService = inject(DeviceConfigsService);
|
||||
private readonly actions = inject(Actions);
|
||||
|
||||
getBrowser$ = createEffect(() => {
|
||||
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 { DomSanitizer } from '@angular/platform-browser';
|
||||
import { customIcons } from './icons.data';
|
||||
@@ -7,10 +7,8 @@ import { customIcons } from './icons.data';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class IconRegistryService {
|
||||
constructor(
|
||||
private matIconRegistry: MatIconRegistry,
|
||||
private domSanitizer: DomSanitizer,
|
||||
) {}
|
||||
private readonly matIconRegistry = inject(MatIconRegistry);
|
||||
private readonly domSanitizer = inject(DomSanitizer);
|
||||
|
||||
init(): void {
|
||||
this.registerIcons(customIcons, 'assets/icons/');
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Angular
|
||||
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' })
|
||||
export class LocalStorageService {
|
||||
constructor(@Inject(PLATFORM_ID) public platformId: Object) {}
|
||||
readonly platformId = inject<Object>(PLATFORM_ID);
|
||||
|
||||
getItem(tag: string) {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
|
||||
@@ -4,16 +4,15 @@ import {
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Renderer2,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export abstract class PiBaseMenuEventItem {
|
||||
@HostBinding('class') className = 'pi-menu-event-item';
|
||||
|
||||
constructor(
|
||||
protected el: ElementRef,
|
||||
protected renderer: Renderer2,
|
||||
) {}
|
||||
protected readonly el = inject(ElementRef);
|
||||
protected readonly renderer = inject(Renderer2);
|
||||
|
||||
@HostListener('click')
|
||||
onClick() {
|
||||
|
||||
@@ -4,15 +4,14 @@ import {
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Renderer2,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
|
||||
@Directive({ selector: '[pi-menu-event-children]' })
|
||||
export class PiMenuEventChildrenDirective {
|
||||
@HostBinding('class') className = 'pi-menu pi-menu__children';
|
||||
constructor(
|
||||
private el: ElementRef,
|
||||
private renderer: Renderer2,
|
||||
) {}
|
||||
private readonly el = inject(ElementRef);
|
||||
private readonly renderer = inject(Renderer2);
|
||||
|
||||
@HostListener('click') onClickMenuItem() {
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class GlobalErrorHandler implements ErrorHandler {
|
||||
constructor(private injector: Injector) {}
|
||||
private readonly injector = inject(Injector);
|
||||
handleError(error: any) {
|
||||
const router = this.injector.get(Router);
|
||||
console.error(error.stack.toString());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Angular
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Inject, Injectable, Renderer2 } from '@angular/core';
|
||||
import { Inject, Injectable, Renderer2, inject } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -8,7 +8,7 @@ import { Inject, Injectable, Renderer2 } from '@angular/core';
|
||||
export class SchemaService {
|
||||
script!: HTMLScriptElement;
|
||||
|
||||
constructor(@Inject(DOCUMENT) private doc: Document) {}
|
||||
private readonly doc = inject<Document>(DOCUMENT);
|
||||
|
||||
loadJsScript(renderer: Renderer2, data: any): HTMLScriptElement {
|
||||
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 { Observable } from 'rxjs';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Observable } from 'rxjs';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class I18nService {
|
||||
constructor(private translateService: TranslateService) {}
|
||||
private readonly translateService = inject(TranslateService);
|
||||
|
||||
setLanguage(lang: string): Observable<any> {
|
||||
return new Observable((observer) => {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Observable } from 'rxjs';
|
||||
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 { readFileSync } from 'fs';
|
||||
|
||||
export class TranslateServerLoader implements TranslateLoader {
|
||||
constructor(
|
||||
private transferState: TransferState,
|
||||
private prefix: string = 'i18n',
|
||||
private suffix: string = '.json',
|
||||
) {}
|
||||
private readonly transferState = inject(TransferState);
|
||||
private readonly prefix = inject((string = 'i18n'));
|
||||
private readonly suffix = inject((string = '.json'));
|
||||
|
||||
public getTranslation(lang: string): Observable<any> {
|
||||
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