222 lines
7.3 KiB
TypeScript
222 lines
7.3 KiB
TypeScript
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
booleanAttribute,
|
|
computed,
|
|
Injector,
|
|
effect,
|
|
inject,
|
|
input,
|
|
model,
|
|
output,
|
|
signal,
|
|
viewChild,
|
|
} from '@angular/core';
|
|
import { SlLookupApi } from '../../core/sl/sl-lookup';
|
|
import { ValueHelp } from '../value-help/value-help';
|
|
import type { VhLookup, VhRow } from '../value-help/value-help.types';
|
|
import { TextInput } from './controls';
|
|
import { FieldBase } from './field-base';
|
|
import { SboField } from './sbo-field';
|
|
|
|
/** Nombres ya consultados de los códigos de las listas con `nameKey`. */
|
|
const NAMES = new Map<string, string>();
|
|
|
|
/**
|
|
* Campo con botón "…" que abre una lista de valores (flecha dorada de SAP).
|
|
* Escribir el código a mano también es válido; elegir de la lista lo rellena y emite `picked`.
|
|
*/
|
|
@Component({
|
|
selector: 'sbo-lookup-field',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
imports: [SboField, TextInput, ValueHelp],
|
|
template: `
|
|
<sbo-field
|
|
[label]="label()"
|
|
[dbName]="dbName()"
|
|
[required]="required()"
|
|
[error]="error() || missingText()"
|
|
action
|
|
[linkable]="linkable()"
|
|
(labelClick)="linkClick.emit()"
|
|
>
|
|
<sbo-input
|
|
#box
|
|
[width]="lookup().nameKey ? (lookup().codeWidth ?? '96px') : undefined"
|
|
[value]="shown()"
|
|
(valueChange)="typing($event)"
|
|
[readonly]="readOnly()"
|
|
[disabled]="disabled()"
|
|
[required]="required()"
|
|
(keydown.f4)="$event.preventDefault(); openList()"
|
|
(focusout)="resolveTyped()"
|
|
/>
|
|
<button
|
|
type="button"
|
|
class="sbo-btn"
|
|
title="Lista de valores (F4)"
|
|
[attr.aria-label]="'Abrir la lista de ' + label()"
|
|
[disabled]="readOnly() || disabled()"
|
|
(click)="openList()"
|
|
>
|
|
…
|
|
</button>
|
|
@if (lookup().nameKey) {
|
|
<sbo-input readonly [attr.title]="name()" [ariaLabel]="'Nombre de ' + label()" [value]="name()" />
|
|
}
|
|
</sbo-field>
|
|
<sbo-value-help
|
|
[open]="listOpen()"
|
|
[title]="lookup().title"
|
|
[resource]="lookup().resource"
|
|
[columns]="lookup().columns"
|
|
[baseFilter]="lookup().baseFilter"
|
|
[orderby]="lookup().orderby"
|
|
[extraFields]="lookup().extraFields ?? []"
|
|
[localRows]="lookup().rows"
|
|
(selected)="onPicked($event)"
|
|
(closed)="closeList()"
|
|
/>
|
|
`,
|
|
styles: `
|
|
:host { display: block; }
|
|
`,
|
|
})
|
|
export class LookupField extends FieldBase {
|
|
readonly value = model('');
|
|
readonly lookup = input.required<VhLookup>();
|
|
/** La etiqueta abre el objeto enlazado (evento `linkClick`). */
|
|
readonly linkable = input(false, { transform: booleanAttribute });
|
|
|
|
/** Al salir del campo con un código escrito a mano, lo busca en la lista (como si se hubiera elegido). */
|
|
readonly resolveOnLeave = input(false, { transform: booleanAttribute });
|
|
|
|
readonly picked = output<VhRow>();
|
|
readonly linkClick = output<void>();
|
|
|
|
protected readonly listOpen = signal(false);
|
|
/** Código que no existe; el aviso desaparece en cuanto el texto cambia. */
|
|
private readonly missing = signal<{ code: string; text: string } | null>(null);
|
|
protected readonly missingText = computed(() => {
|
|
const m = this.missing();
|
|
return m && m.code === this.value().trim() ? m.text : '';
|
|
});
|
|
private readonly injector = inject(Injector);
|
|
/** Último código ya resuelto, para no repetir la consulta al salir y volver a entrar sin cambios. */
|
|
private resolved = '';
|
|
private readonly box = viewChild.required<TextInput>('box');
|
|
|
|
/** Nombre de lo elegido, junto al código (`lookup.nameKey`). */
|
|
protected readonly name = signal('');
|
|
private nameFor = '';
|
|
|
|
/** El usuario está escribiendo: se muestra lo que escribe y no el texto con que la lista presenta el código guardado. */
|
|
private readonly editing = signal(false);
|
|
/**
|
|
* Lo que se ve en el campo: el código guardado o, en una lista que tiene texto propio para él (la cuenta con su
|
|
* código con formato en vez del interno `_SYS…`), ese texto.
|
|
*/
|
|
protected readonly shown = computed(() => {
|
|
const v = this.value();
|
|
const codes = this.lookup().codes;
|
|
return !this.editing() && codes && v !== '' ? codes.display(v) : v;
|
|
});
|
|
|
|
constructor() {
|
|
super();
|
|
effect(() => {
|
|
const v = this.value();
|
|
const codes = this.lookup().codes;
|
|
// un valor que llega de fuera (documento cargado, valor por defecto) ya está resuelto y se pide su texto; lo que se escribe, no
|
|
if (!this.editing()) {
|
|
if (codes && v !== '') codes.ensure([v]);
|
|
this.resolved = v.trim();
|
|
void this.loadName(v.trim());
|
|
}
|
|
});
|
|
}
|
|
|
|
/** El nombre del código guardado; se pregunta una vez por código (el resultado queda en `NAMES`). */
|
|
private async loadName(code: string): Promise<void> {
|
|
const lookup = this.lookup();
|
|
const nameKey = lookup.nameKey;
|
|
this.nameFor = code;
|
|
if (!nameKey || !code) return void this.name.set('');
|
|
const cacheKey = `${lookup.resource}|${lookup.baseFilter ?? ''}|${code}`;
|
|
const cached = NAMES.get(cacheKey);
|
|
if (cached !== undefined) return void this.name.set(cached);
|
|
try {
|
|
const row = await this.injector.get(SlLookupApi).find(lookup, code);
|
|
const text = row ? String(row[nameKey] ?? '') : '';
|
|
NAMES.set(cacheKey, text);
|
|
if (this.nameFor === code) this.name.set(text);
|
|
} catch {
|
|
if (this.nameFor === code) this.name.set('');
|
|
}
|
|
}
|
|
|
|
protected typing(text: string): void {
|
|
this.editing.set(true);
|
|
this.value.set(text);
|
|
}
|
|
|
|
protected openList(): void {
|
|
if (!this.readOnly() && !this.disabled()) this.listOpen.set(true);
|
|
}
|
|
|
|
protected closeList(): void {
|
|
this.listOpen.set(false);
|
|
this.box().focus();
|
|
}
|
|
|
|
protected onPicked(rows: VhRow[]): void {
|
|
const row = rows[0];
|
|
if (!row) return;
|
|
this.apply(row);
|
|
}
|
|
|
|
private apply(row: VhRow): void {
|
|
const key = this.lookup().valueKey ?? this.lookup().columns[0]?.key ?? '';
|
|
this.missing.set(null);
|
|
this.lookup().codes?.remember(row);
|
|
const nameKey = this.lookup().nameKey;
|
|
if (nameKey) {
|
|
this.nameFor = String(row[this.lookup().valueKey ?? this.lookup().columns[0]?.key ?? ''] ?? '');
|
|
this.name.set(String(row[nameKey] ?? ''));
|
|
}
|
|
this.editing.set(false);
|
|
this.value.set(String(row[key] ?? ''));
|
|
this.resolved = this.value();
|
|
this.picked.emit(row);
|
|
}
|
|
|
|
protected async resolveTyped(): Promise<void> {
|
|
const code = this.value().trim();
|
|
if (!this.editing()) return; // no se escribió nada: lo guardado ya está resuelto
|
|
if (!this.resolveOnLeave() || this.readOnly() || this.disabled() || this.listOpen()) {
|
|
this.editing.set(false);
|
|
return;
|
|
}
|
|
if (!code) {
|
|
this.missing.set(null);
|
|
this.resolved = '';
|
|
this.editing.set(false);
|
|
return;
|
|
}
|
|
if (code === this.resolved) {
|
|
this.editing.set(false);
|
|
return;
|
|
}
|
|
try {
|
|
const row = await this.injector.get(SlLookupApi).find(this.lookup(), code);
|
|
if (this.value().trim() !== code) return; // el usuario siguió escribiendo
|
|
if (row) this.apply(row);
|
|
else this.missing.set({ code, text: `No existe «${code}» en ${this.lookup().title.toLowerCase()}.` });
|
|
} catch {
|
|
this.missing.set({ code, text: 'No se pudo comprobar el código.' });
|
|
} finally {
|
|
if (this.value().trim() === code) this.editing.set(false);
|
|
}
|
|
}
|
|
}
|