munin/static/autocomplete.js
Thomas Adamek 466f1e40af Workspaces, Änderungshistorie, Papierkorb, Nutzungsübersicht, Hilfe
Umsetzung der Ausbaustufen 2.1, 3.6, 3.8, 3.9, 4.1, 4.2, 4.4, 4.6,
5.1–5.4 (Details in AUSBAUSTUFEN.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:44:52 +02:00

198 lines
6.7 KiB
JavaScript

// Schlägt beim Tippen von "[[" vorhandene Bezeichner vor.
//
// Bewusst kein an der Schreibmarke ausgerichtetes Einblendfenster — das
// bräuchte einen Spiegel-Trick, um die Pixel-Position im Textfeld
// nachzubauen (eine unsichtbare Kopie des Feldes, die dieselbe Schrift und
// denselben Umbruch trägt, nur um die Koordinate eines Zeichens abzulesen).
// Die Vorschläge stehen stattdessen als Liste direkt unter dem Feld, im
// selben Geist wie die Sammelleiste unter der Eintragsübersicht.
(function () {
"use strict";
var AKTIV = "aktiv";
var timer = null;
document.addEventListener("input", function (event) {
if (!istEditorfeld(event.target)) {
return;
}
clearTimeout(timer);
timer = setTimeout(function () {
aktualisiereVorschlaege(event.target);
}, 150);
});
document.addEventListener("keydown", function (event) {
if (!istEditorfeld(event.target)) {
return;
}
var liste = listeFuer(event.target);
if (!liste || liste.hidden) {
return;
}
switch (event.key) {
case "Escape":
liste.hidden = true;
break;
case "ArrowDown":
event.preventDefault();
bewegeAuswahl(liste, 1);
break;
case "ArrowUp":
event.preventDefault();
bewegeAuswahl(liste, -1);
break;
case "Enter":
case "Tab": {
var gewaehlt = liste.querySelector("li." + AKTIV) || liste.firstElementChild;
if (gewaehlt) {
event.preventDefault();
uebernehmen(event.target, liste, gewaehlt);
}
break;
}
}
});
// mousedown statt click, und mit preventDefault: Sonst verlässt die
// Textarea den Fokus, BEVOR der Klick verarbeitet wird, und die Liste
// wäre durch den Blur-Handler unten schon verschwunden.
document.addEventListener("mousedown", function (event) {
var eintrag = event.target.closest("[data-autovervollstaendigung] li");
if (!eintrag) {
return;
}
event.preventDefault();
var liste = eintrag.closest("[data-autovervollstaendigung]");
var textarea = textareaFuer(liste);
if (textarea) {
uebernehmen(textarea, liste, eintrag);
}
});
// Blur bubbelt nicht — nur über die Capture-Phase zu fangen.
document.addEventListener(
"blur",
function (event) {
if (!istEditorfeld(event.target)) {
return;
}
var liste = listeFuer(event.target);
if (liste) {
liste.hidden = true;
}
},
true,
);
function istEditorfeld(el) {
return !!(el && el.tagName === "TEXTAREA" && el.hasAttribute("data-suggest-url"));
}
function listeFuer(textarea) {
return textarea.parentElement.querySelector("[data-autovervollstaendigung]");
}
function textareaFuer(liste) {
return liste.parentElement.querySelector("textarea[data-suggest-url]");
}
// Sucht rückwärts von der Schreibmarke aus das letzte "[[", das noch
// nicht mit "]]" geschlossen wurde. Kein Treffer heißt: die Schreibmarke
// steht gerade nicht in einem offenen Verweis.
function offeneKlammer(wert, position) {
var start = wert.lastIndexOf("[[", position - 1);
if (start === -1) {
return null;
}
var dazwischen = wert.slice(start + 2, position);
if (dazwischen.indexOf("]]") !== -1 || dazwischen.indexOf("\n") !== -1 || dazwischen.length > 60) {
return null;
}
return { start: start, suche: dazwischen };
}
function aktualisiereVorschlaege(textarea) {
var liste = listeFuer(textarea);
if (!liste) {
return;
}
var klammer = offeneKlammer(textarea.value, textarea.selectionStart);
if (!klammer) {
liste.hidden = true;
return;
}
var url = textarea.getAttribute("data-suggest-url") + "?q=" + encodeURIComponent(klammer.suche);
fetch(url, { headers: { Accept: "application/json" } })
.then(function (antwort) {
return antwort.ok ? antwort.json() : [];
})
.then(function (treffer) {
// Zwischenzeitlich könnte weitergetippt oder das Feld
// verlassen worden sein — dann passt die Antwort nicht mehr
// zur aktuellen Schreibmarke.
var jetzt = offeneKlammer(textarea.value, textarea.selectionStart);
if (!jetzt || jetzt.start !== klammer.start) {
return;
}
zeigeVorschlaege(liste, treffer, klammer.start);
})
.catch(function () {
liste.hidden = true;
});
}
function zeigeVorschlaege(liste, treffer, klammerStart) {
liste.innerHTML = "";
if (!treffer.length) {
liste.hidden = true;
return;
}
treffer.forEach(function (t, i) {
var li = document.createElement("li");
li.className = i === 0 ? AKTIV : "";
li.dataset.slug = t.slug;
li.dataset.klammerStart = String(klammerStart);
var slug = document.createElement("code");
slug.textContent = t.slug;
var titel = document.createElement("span");
titel.textContent = t.title;
li.appendChild(slug);
li.appendChild(titel);
liste.appendChild(li);
});
liste.hidden = false;
}
function bewegeAuswahl(liste, richtung) {
var eintraege = Array.prototype.slice.call(liste.children);
if (!eintraege.length) {
return;
}
var index = eintraege.findIndex(function (e) {
return e.classList.contains(AKTIV);
});
eintraege.forEach(function (e) {
e.classList.remove(AKTIV);
});
var naechster = (index + richtung + eintraege.length) % eintraege.length;
eintraege[naechster].classList.add(AKTIV);
}
// Ersetzt den Text zwischen der offenen Klammer und der Schreibmarke
// durch "bezeichner]]" und setzt die Schreibmarke direkt dahinter.
function uebernehmen(textarea, liste, eintrag) {
var start = parseInt(eintrag.dataset.klammerStart, 10);
var cursor = textarea.selectionStart;
textarea.setRangeText(eintrag.dataset.slug + "]]", start + 2, cursor, "end");
liste.hidden = true;
textarea.focus();
}
})();