// 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(); } })();