171 lines
6.9 KiB
JavaScript
171 lines
6.9 KiB
JavaScript
import './bootstrap';
|
|
import { proponi, normalizza } from './scelta';
|
|
|
|
// ─── Campo di scelta con ricerca (<x-scelta>) ───────────────────────────────
|
|
//
|
|
// Al posto della tendina: si scrive e sotto compaiono le proposte più vicine
|
|
// (logica in ./scelta.js). Campo vuoto o appena aperto: la tendina completa.
|
|
// Il valore scelto arriva a Livewire con x-modelable + wire:model.
|
|
|
|
document.addEventListener('alpine:init', () => {
|
|
// Elenco filtrato nel browser: ogni riga ha data-cerca (testo già senza
|
|
// accenti, minuscolo); valgono tutte le parole scritte, in qualsiasi
|
|
// ordine. Nessuna richiesta al server mentre si scrive.
|
|
window.Alpine.data('elencoCercabile', () => ({
|
|
q: '',
|
|
corrisponde(testo) {
|
|
const parole = normalizza(this.q).split(/\s+/).filter(Boolean);
|
|
return parole.every((p) => (testo ?? '').includes(p));
|
|
},
|
|
get nessuno() {
|
|
if (normalizza(this.q) === '') return false;
|
|
return ![...this.$root.querySelectorAll('[data-cerca]')].some((riga) => this.corrisponde(riga.dataset.cerca));
|
|
},
|
|
// Copia il link della riga (il <code> accanto al pulsante); il pulsante
|
|
// mostra la spunta per due secondi (classe tm-copiato). Un metodo solo
|
|
// invece dello stesso codice ripetuto in ogni riga.
|
|
copia(pulsante) {
|
|
const testo = pulsante.closest('[data-link]')?.querySelector('code')?.textContent.trim() ?? '';
|
|
navigator.clipboard?.writeText(testo).then(() => {
|
|
pulsante.classList.add('tm-copiato');
|
|
setTimeout(() => pulsante.classList.remove('tm-copiato'), 2000);
|
|
}).catch(() => {});
|
|
},
|
|
}));
|
|
|
|
// vuoto: il valore di "nessuna scelta" per la proprietà Livewire collegata
|
|
// ('' per le stringhe, null per i ?int: '' su un ?int sarebbe un errore).
|
|
window.Alpine.data('sceltaRicerca', (opzioni, vuoto = '') => ({
|
|
opzioni,
|
|
vuoto,
|
|
valore: null,
|
|
testo: '',
|
|
aperto: false,
|
|
evidenziato: 0,
|
|
|
|
init() {
|
|
this.testo = this.etichettaDi(this.valore);
|
|
// Valore cambiato da Livewire (reset del form, scelta da codice).
|
|
this.$watch('valore', (v) => { if (!this.aperto) this.testo = this.etichettaDi(v); });
|
|
},
|
|
|
|
scelta() {
|
|
return this.opzioni.find((o) => String(o.valore) === String(this.valore ?? '')) ?? null;
|
|
},
|
|
|
|
etichettaDi(v) {
|
|
if (v === null || v === undefined || v === '') {
|
|
const vuota = this.opzioni.find((o) => o.speciale && o.valore === '');
|
|
return vuota ? vuota.etichetta : '';
|
|
}
|
|
const o = this.opzioni.find((x) => String(x.valore) === String(v));
|
|
return o ? o.etichetta : '';
|
|
},
|
|
|
|
// Finché nel campo c'è l'etichetta della scelta attuale, si vede la
|
|
// tendina completa: si cerca solo da quando si comincia a scrivere.
|
|
get ricerca() {
|
|
return this.testo === this.etichettaDi(this.valore) ? '' : this.testo;
|
|
},
|
|
|
|
get risultati() {
|
|
return proponi(this.opzioni, this.ricerca);
|
|
},
|
|
|
|
apri() {
|
|
if (this.aperto) return;
|
|
this.aperto = true;
|
|
const i = this.risultati.findIndex((o) => String(o.valore) === String(this.valore ?? ''));
|
|
this.evidenziato = Math.max(0, i);
|
|
this.$nextTick(() => this.mostraEvidenziato());
|
|
},
|
|
|
|
chiudi() {
|
|
this.aperto = false;
|
|
this.testo = this.etichettaDi(this.valore);
|
|
},
|
|
|
|
alterna() {
|
|
if (this.aperto) { this.chiudi(); return; }
|
|
this.$refs.campo.focus();
|
|
this.apri();
|
|
},
|
|
|
|
scrivendo() {
|
|
this.aperto = true;
|
|
this.evidenziato = 0;
|
|
},
|
|
|
|
sposta(passo) {
|
|
if (!this.aperto) { this.apri(); return; }
|
|
const n = this.risultati.length;
|
|
if (n === 0) return;
|
|
this.evidenziato = (this.evidenziato + passo + n) % n;
|
|
this.$nextTick(() => this.mostraEvidenziato());
|
|
},
|
|
|
|
mostraEvidenziato() {
|
|
this.$refs.elenco?.querySelector('[data-evidenziato="1"]')?.scrollIntoView({ block: 'nearest' });
|
|
},
|
|
|
|
// daTocco: scelta col dito o col mouse. Si toglie il focus, così sul
|
|
// telefono si chiude la tastiera; con frecce e Invio il focus resta.
|
|
scegli(opzione, daTocco = false) {
|
|
if (!opzione || opzione.motivo) return;
|
|
this.valore = opzione.valore;
|
|
this.aperto = false;
|
|
this.testo = opzione.etichetta;
|
|
// Per chi vuole reagire alla scelta senza passare dal server
|
|
// (l'anteprima della miniatura in Assegna).
|
|
this.$dispatch('scelta', { valore: opzione.valore, opzione });
|
|
if (daTocco) this.$refs.campo.blur();
|
|
},
|
|
|
|
scegliEvidenziato() {
|
|
if (this.aperto) this.scegli(this.risultati[this.evidenziato]);
|
|
},
|
|
|
|
svuota() {
|
|
this.valore = this.vuoto;
|
|
this.testo = '';
|
|
this.$dispatch('scelta', { valore: this.vuoto, opzione: null });
|
|
this.$refs.campo.focus();
|
|
this.aperto = true;
|
|
this.evidenziato = 0;
|
|
},
|
|
}));
|
|
});
|
|
|
|
// ─── Ritorno alla pagina di partenza ────────────────────────────────────────
|
|
//
|
|
// I link con data-ritorno (schede e operazioni) portano con sé ?da=<percorso>:
|
|
// la pagina di partenza con i suoi filtri, oppure il `da` che la pagina
|
|
// corrente aveva già ricevuto, così si risale sempre all'origine del percorso
|
|
// (elenco → scheda → Rientra → torna all'elenco). Il server lo valida e lo usa
|
|
// dopo conferma o annulla: App\Support\Ritorno, trait RicordaRitorno.
|
|
//
|
|
// Si calcola al momento dell'uso e non al caricamento: filtri e pagina degli
|
|
// elenchi cambiano l'indirizzo (#[Url] di Livewire) senza ricaricare.
|
|
|
|
function ritornoCorrente() {
|
|
const qui = new URL(window.location.href);
|
|
return qui.searchParams.get('da') || (qui.pathname + qui.search);
|
|
}
|
|
|
|
function aggiornaLink(link) {
|
|
const destinazione = new URL(link.getAttribute('href'), window.location.origin);
|
|
if (destinazione.origin !== window.location.origin) return;
|
|
destinazione.searchParams.set('da', ritornoCorrente());
|
|
link.setAttribute('href', destinazione.pathname + destinazione.search + destinazione.hash);
|
|
}
|
|
|
|
// Fase di cattura: si arriva prima di wire:navigate, che legge l'href al
|
|
// pointerdown (precaricamento) e al click. contextmenu e focusin coprono
|
|
// "apri in una nuova scheda" e la tastiera.
|
|
['pointerdown', 'click', 'contextmenu', 'focusin'].forEach((evento) => {
|
|
document.addEventListener(evento, (e) => {
|
|
const link = e.target instanceof Element ? e.target.closest('a[data-ritorno][href]') : null;
|
|
if (link) aggiornaLink(link);
|
|
}, true);
|
|
});
|