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>
198 lines
6.7 KiB
JavaScript
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();
|
|
}
|
|
})();
|