/** * classify.js - Klassifizierung eines gerade abgelegten Dokuments. * * Wird unmittelbar nach dem Ablegen aufgerufen. Ohne diesen Schritt trägt * ein neues Dokument nur den Titel, und nachträglich macht man es * erfahrungsgemäß nicht mehr. */ import { kurz } from "../lib/settings.js"; import { uebersetzeSeite, ruf, fehlerAnzeigen, meldungAnzeigen } from "./gemeinsam.js"; const $ = (id) => document.getElementById(id); let dokumentId = null; let alleTags = []; // Bei mehreren abgelegten Dateien wird jede einzeln klassifiziert. // Andernfalls trüge nur die erste eine sinnvolle Zuordnung, und die // übrigen fielen später als unklassifiziert auf - wenn überhaupt. let liste = []; let stelle = 0; // Gewählte Tags als Menge. Bewusst getrennt vom angezeigten Zustand der // Kontrollkästchen: Der Filter blendet Zeilen aus, die Auswahl darf davon // nicht berührt werden. const gewaehlt = new Set(); document.addEventListener("DOMContentLoaded", start); async function start() { uebersetzeSeite(); const p = new URLSearchParams(location.search); // "ids" für mehrere, "id" für den Einzelfall - letzteres bleibt für // Aufrufe aus anderen Stellen erhalten. const roh = p.get("ids") || p.get("id") || ""; liste = roh.split(",").map((x) => Number(x)).filter(Boolean); stelle = 0; $("ueberspringen").addEventListener("click", weiter); $("abbrechenAlle").addEventListener("click", () => window.close()); $("speichern").addEventListener("click", speichern); if (!liste.length) { fehlerAnzeigen("Kein Dokument angegeben."); return; } try { const stamm = await ruf("stammdaten"); alleTags = stamm.tags; fuelleListe("korrListe", stamm.korr); fuelleListe("typListe", stamm.typen); zustand.korr = stamm.korr; zustand.typen = stamm.typen; } catch (err) { fehlerAnzeigen(err.message); return; } await anzeigen(); } const zustand = { korr: [], typen: [] }; /** Das Dokument an der aktuellen Stelle laden und darstellen. */ async function anzeigen() { dokumentId = liste[stelle]; $("speichern").disabled = false; $("ueberspringen").disabled = false; meldungAnzeigen(""); // Der Knopf zum Abbrechen aller weiteren ist nur sinnvoll, wenn noch // welche folgen. $("abbrechenAlle").hidden = liste.length < 2; try { const doc = await ruf("dokument", { id: dokumentId }); const zaehler = liste.length > 1 ? ` · ${browser.i18n.getMessage("classifyProgress", [String(stelle + 1), String(liste.length)]) || `${stelle + 1} von ${liste.length}`}` : ""; $("quelle").textContent = `#${dokumentId} · ${kurz(doc.title, 60)}` + zaehler; $("titel").value = doc.title || ""; $("erstellt").value = (doc.created || "").slice(0, 10); $("korr").value = nameVon(zustand.korr, doc.correspondent); $("typ").value = nameVon(zustand.typen, doc.document_type); zeigeTags(doc.tags || []); if ($("tagSuche")) $("tagSuche").value = ""; } catch (err) { fehlerAnzeigen(err.message); } } /** Zum nächsten Dokument, oder schliessen. */ async function weiter() { stelle += 1; if (stelle >= liste.length) { window.close(); return; } await anzeigen(); } function fuelleListe(id, eintraege) { const dl = $(id); dl.replaceChildren(); for (const e of eintraege) { const o = document.createElement("option"); o.value = e.name; dl.append(o); } } function nameVon(liste, id) { return liste.find((x) => x.id === id)?.name || ""; } function zeigeTags(gesetzt) { for (const id of gesetzt) gewaehlt.add(Number(id)); const feld = $("tagSuche"); if (feld) { feld.addEventListener("input", zeichneTags); // Eingabetaste soll den Dialog nicht abschicken, sondern beim // einzigen Treffer diesen umschalten - das ist beim Tippen der // erwartete Ablauf. feld.addEventListener("keydown", (e) => { if (e.key !== "Enter") return; e.preventDefault(); const treffer = gefilterteTags(); if (treffer.length === 1) { umschalten(treffer[0].id); feld.value = ""; zeichneTags(); } }); } zeichneTags(); } /** Tags, die zum aktuellen Filter passen und noch nicht gewählt sind. */ function gefilterteTags() { const filter = ($("tagSuche")?.value || "").trim().toLowerCase(); return alleTags.filter((t) => { if (gewaehlt.has(Number(t.id))) return false; if (!filter) return true; return String(t.name).toLowerCase().includes(filter); }); } function umschalten(id) { const n = Number(id); if (gewaehlt.has(n)) gewaehlt.delete(n); else gewaehlt.add(n); } /** * Tagliste zeichnen. * * Zwei Bereiche: oben die gewählten, darunter die übrigen. Die gewählten * bleiben immer sichtbar, auch wenn der Filter sie ausschliesst - sonst * verliert man beim Suchen den Überblick, was bereits angehakt ist. */ function zeichneTags() { const filter = ($("tagSuche")?.value || "").trim().toLowerCase(); const oben = $("tagsGewaehlt"); oben.replaceChildren(); const gewaehlteListe = alleTags .filter((t) => gewaehlt.has(Number(t.id))) .sort((a, b) => String(a.name).localeCompare(String(b.name), "de")); for (const tag of gewaehlteListe) { oben.append(tagZeile(tag, true, "")); } const unten = $("tags"); unten.replaceChildren(); const rest = gefilterteTags() .sort((a, b) => String(a.name).localeCompare(String(b.name), "de")); for (const tag of rest) { unten.append(tagZeile(tag, false, filter)); } const leer = $("tagsLeer"); if (leer) leer.style.display = (rest.length === 0 && filter) ? "" : "none"; } function tagZeile(tag, istGewaehlt, filter) { const zeile = document.createElement("div"); zeile.className = "wahl"; const box = document.createElement("input"); box.type = "checkbox"; box.id = `t${tag.id}`; box.value = String(tag.id); box.checked = istGewaehlt; box.addEventListener("change", () => { umschalten(tag.id); zeichneTags(); }); const bez = document.createElement("label"); bez.htmlFor = box.id; beschriftungSetzen(bez, String(tag.name), filter); zeile.append(box, bez); return zeile; } /** * Beschriftung mit hervorgehobenem Treffer. * * Aufbau über createElement und textContent, nicht über innerHTML - ein * Tagname stammt aus Paperless und damit mittelbar aus fremden Quellen. */ function beschriftungSetzen(el, name, filter) { el.replaceChildren(); if (!filter) { el.textContent = name; return; } const pos = name.toLowerCase().indexOf(filter); if (pos < 0) { el.textContent = name; return; } el.append(document.createTextNode(name.slice(0, pos))); const treffer = document.createElement("span"); treffer.className = "trefferHervor"; treffer.textContent = name.slice(pos, pos + filter.length); el.append(treffer); el.append(document.createTextNode(name.slice(pos + filter.length))); } async function speichern() { $("speichern").disabled = true; meldungAnzeigen(browser.i18n.getMessage("msgSaving") || "Wird gespeichert …"); try { // Aus der Menge lesen, nicht aus den Kästchen: Gefilterte Zeilen // sind gar nicht im Dokument, ihre Auswahl ginge sonst verloren. const tags = [...gewaehlt]; const payload = { title: $("titel").value.trim(), tags }; const erstellt = $("erstellt").value.trim(); if (erstellt) payload.created = erstellt; await ruf("klassifizieren", { id: dokumentId, payload, korrName: $("korr").value.trim(), typName: $("typ").value.trim() }); meldungAnzeigen(browser.i18n.getMessage("msgSaved") || "Gespeichert."); setTimeout(weiter, 600); } catch (err) { fehlerAnzeigen(err.message); $("speichern").disabled = false; } }