Vai al contenuto

Componente

Transfer

Spostare voci da un elenco «disponibili» a un elenco «selezionati». Nella versione solo CSS ogni voce è un checkbox che cambia colonna quando viene selezionato; la versione classica usa due elenchi e pulsanti di invio.

Riferimento
<it-transfer>
Classi
ita-transferita-transfer-gridita-transfer-headita-transfer-formita-transfer-layoutita-transfer-boxita-transfer-movesita-transfer-countita-checkbox
Basato su (daisyUI)
checkboxfieldsetbtnbadge
Solo CSS
transfer(): un solo gruppo di checkbox su una griglia a due colonne. label:has(:checked) porta la voce a destra e grid-flow-dense compatta le colonne dall'alto: clic o Spazio spostano la voce, il form invia i valori selezionati. transferForm(): la disposizione di bootstrap-italia con le frecce, che senza JavaScript inviano il form (name="azione") e il server sposta le voci. Filtro di ricerca e spostamento lato client richiedono JavaScript: vedi sotto.
Storybook
Apri le storie di Transfer

Solo CSS

Seleziona una voce: passa nella colonna di destra. Deselezionala per riportarla a sinistra.

Uffici da cui ricevere comunicazioni

Riceverai avvisi e scadenze solo dagli uffici selezionati.

Mostra il codice HTML
<fieldset class="ita-transfer" aria-describedby="transfer-1-hint">
  <legend>Uffici da cui ricevere comunicazioni</legend>
  <p id="transfer-1-hint">Riceverai avvisi e scadenze solo dagli uffici selezionati.</p>
  <div class="ita-transfer-grid">
    <p class="ita-transfer-head" aria-hidden="true"><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4,12c0.1,0.5-0.3,0.9-0.8,1c-0.1,0-0.1,0-0.2,0c-0.6,0-1-0.4-1-1s0.4-1,1-1c0.5-0.1,0.9,0.3,1,0.8C4,11.9,4,11.9,4,12z M3,4.5C2.5,4.4,2.1,4.8,2,5.3c0,0.1,0,0.1,0,0.2C1.9,6,2.3,6.4,2.8,6.5c0.1,0,0.1,0,0.2,0c0.5,0.1,0.9-0.3,1-0.8 c0-0.1,0-0.1,0-0.2c0.1-0.5-0.3-0.9-0.8-1C3.1,4.5,3.1,4.5,3,4.5z M3,17.5c-0.6,0-1,0.4-1,1s0.4,1,1,1s1-0.4,1-1S3.6,17.5,3,17.5z M6,5v1h16V5H6z M6,12.5h16v-1H6V12.5z M6,19h16v-1H6V19z" /></svg>Disponibili</p>
    <p class="ita-transfer-head" aria-hidden="true"><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M17.1,7.9l0.7,0.7L10,16.2l-4.7-4.6L6,10.9l4,3.9L17.1,7.9z M22,12c0,5.5-4.5,10-10,10S2,17.5,2,12S6.5,2,12,2S22,6.5,22,12z M21,12c0-5-4-9-9-9s-9,4-9,9s4,9,9,9S21,17,21,12z" /></svg>Selezionati</p>
    <label><input type="checkbox" name="transfer-1" value="anagrafe" class="ita-checkbox ita-checkbox-sm"><span>Anagrafe</span></label>
    <label><input type="checkbox" name="transfer-1" value="tributi" class="ita-checkbox ita-checkbox-sm" checked><span>Tributi</span></label>
    <label><input type="checkbox" name="transfer-1" value="sociali" class="ita-checkbox ita-checkbox-sm"><span>Servizi sociali</span></label>
    <label><input type="checkbox" name="transfer-1" value="mobilita" class="ita-checkbox ita-checkbox-sm" checked><span>Mobilità</span></label>
    <label><input type="checkbox" name="transfer-1" value="scuola" class="ita-checkbox ita-checkbox-sm"><span>Scuola</span></label>
    <label><input type="checkbox" name="transfer-1" value="urbanistica" class="ita-checkbox ita-checkbox-sm" disabled><span>Urbanistica</span></label>
    <span aria-hidden="true"></span>
  </div>
</fieldset>

Con pulsanti (invio al server)

La disposizione di bootstrap-italia: le frecce inviano il form con azione=aggiungi o azione=rimuovi.

Uffici da cui ricevere comunicazioni
Disponibili
Selezionati
Mostra il codice HTML
<form action="#" method="post" class="ita-transfer-form" data-transfer>
  <fieldset>
    <legend>Uffici da cui ricevere comunicazioni</legend>
    <div class="ita-transfer-layout">
    <fieldset class="ita-transfer-box">
      <legend class="sr-only">Disponibili</legend>
      <p class="ita-transfer-head" aria-hidden="true"><span>Disponibili</span><span class="ita-transfer-count">4</span></p>
      <ul>
        <li><label><input type="checkbox" name="transfer-form-2-aggiungi" value="anagrafe" class="ita-checkbox ita-checkbox-sm"><span>Anagrafe</span></label></li>
        <li><label><input type="checkbox" name="transfer-form-2-aggiungi" value="sociali" class="ita-checkbox ita-checkbox-sm"><span>Servizi sociali</span></label></li>
        <li><label><input type="checkbox" name="transfer-form-2-aggiungi" value="scuola" class="ita-checkbox ita-checkbox-sm"><span>Scuola</span></label></li>
        <li><label><input type="checkbox" name="transfer-form-2-aggiungi" value="urbanistica" class="ita-checkbox ita-checkbox-sm" disabled><span>Urbanistica</span></label></li>
      </ul>
    </fieldset>
    <div class="ita-transfer-moves">
      <button type="submit" name="azione" value="aggiungi" class="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs ita-btn-square" aria-label="Sposta nei selezionati"><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M13.9,5.4l-0.7,0.7l5.3,5.4H3v1h15.5l-5.3,5.4l0.7,0.7l6.6-6.6L13.9,5.4z" /></svg></button>
      <button type="submit" name="azione" value="rimuovi" class="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs ita-btn-square" aria-label="Rimuovi dai selezionati"><svg class="shrink-0 fill-current" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M21,11.5H5.5l5.3-5.4l-0.7-0.7L3.6,12l6.5,6.6l0.7-0.7l-5.3-5.4H21V11.5z" /></svg></button>
    </div>
    <fieldset class="ita-transfer-box">
      <legend class="sr-only">Selezionati</legend>
      <p class="ita-transfer-head" aria-hidden="true"><span>Selezionati</span><span class="ita-transfer-count">2</span></p>
      <ul>
        <li><label><input type="checkbox" name="transfer-form-2-rimuovi" value="tributi" class="ita-checkbox ita-checkbox-sm"><span>Tributi</span></label></li>
        <li><label><input type="checkbox" name="transfer-form-2-rimuovi" value="mobilita" class="ita-checkbox ita-checkbox-sm"><span>Mobilità</span></label></li>
      </ul>
    </fieldset>
    </div>
  </fieldset>
</form>

Con JavaScript (opzionale)

La ricetta è solo CSS e funziona così com'è. Questi frammenti non sono inclusi nel pacchetto: mostrano come aggiungere il comportamento che richiede uno script, in JavaScript semplice o in React.

Spostamento lato client per transferForm()js

Intercetta l'invio delle frecce e sposta le voci senza ricaricare la pagina.

// Client-side moves for transferForm(): the arrow buttons stop submitting
// and move the checked items between the two lists instead.
// Checkbox names end in -aggiungi (left list) or -rimuovi (right list).
const swap = (name) => (name.endsWith("-aggiungi") ? name.replace(/-aggiungi$/, "-rimuovi") : name.replace(/-rimuovi$/, "-aggiungi"));

document.querySelectorAll("form[data-transfer]").forEach((form) => {
  const [source, target] = form.querySelectorAll("ul");
  form.addEventListener("submit", (ev) => {
    const action = ev.submitter?.value;
    if (action !== "aggiungi" && action !== "rimuovi") return;
    ev.preventDefault();
    const [from, to] = action === "aggiungi" ? [source, target] : [target, source];
    for (const box of from.querySelectorAll("input:checked")) {
      box.checked = false;
      box.name = swap(box.name);
      to.append(box.closest("li"));
    }
    // keep the counters in the box headers in sync
    for (const ul of [source, target]) ul.parentElement.querySelector(".ita-transfer-count").textContent = ul.children.length;
  });
});

Filtro di ricercajs

// Filter box for long lists: hides the rows that don't match.
// <input type="search" class="ita-input ita-input-sm" data-filter="#my-transfer">
document.querySelectorAll("[data-filter]").forEach((box) => {
  const scope = document.querySelector(box.dataset.filter);
  box.addEventListener("input", () => {
    const q = box.value.trim().toLowerCase();
    for (const row of scope.querySelectorAll("label")) {
      row.hidden = q !== "" && !row.textContent.toLowerCase().includes(q);
    }
  });
});

Componente Reacttsx

import { useState } from "react";

type Item = { value: string; label: string };

/** Two lists with checkboxes; the selected values are posted as <name>[]. */
export function Transfer({ items, name, initial = [] }: { items: Item[]; name: string; initial?: string[] }) {
  const [selected, setSelected] = useState<Set<string>>(new Set(initial));
  const [marked, setMarked] = useState<Set<string>>(new Set());
  const toggleMark = (v: string) => setMarked((m) => new Set(m.has(v) ? [...m].filter((x) => x !== v) : [...m, v]));
  const move = (toSelected: boolean) => {
    setSelected((s) => {
      const next = new Set(s);
      for (const v of marked) toSelected ? next.add(v) : next.delete(v);
      return next;
    });
    setMarked(new Set());
  };
  const list = (title: string, inSelected: boolean) => {
    const rows = items.filter((i) => selected.has(i.value) === inSelected);
    return (
      <fieldset className="ita-transfer-box">
        <legend className="sr-only">{title}</legend>
        <p className="ita-transfer-head" aria-hidden>
          <span>{title}</span><span className="ita-transfer-count">{rows.length}</span>
        </p>
        <ul>
          {rows.map((i) => (
            <li key={i.value}>
              <label>
                <input type="checkbox" className="ita-checkbox ita-checkbox-sm"
                       checked={marked.has(i.value)} onChange={() => toggleMark(i.value)} />
                {i.label}
              </label>
            </li>
          ))}
        </ul>
      </fieldset>
    );
  };
  return (
    <div className="ita-transfer-layout">
      {list("Disponibili", false)}
      <div className="ita-transfer-moves">
        <button type="button" className="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs ita-btn-square" aria-label="Sposta nei selezionati" onClick={() => move(true)}>→</button>
        <button type="button" className="ita-btn ita-btn-primary ita-btn-outline ita-btn-xs ita-btn-square" aria-label="Rimuovi dai selezionati" onClick={() => move(false)}>←</button>
      </div>
      {list("Selezionati", true)}
      {[...selected].map((v) => <input key={v} type="hidden" name={`${name}[]`} value={v} />)}
    </div>
  );
}