import './bootstrap'; import { proponi, normalizza } from './scelta'; // ─── Campo di scelta con ricerca () ─────────────────────────────── // // 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 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=: // 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); });