/* ===================================================================== textform.js — Textgestaltung, einmal fuer das ganze Haus (21.09.2026) Filipe: "dan will ich dass die leute auch immer die art von text aussuchen können, fettgedrückt, unterstrichen und so alles. perfektionier das bitte, auch sachen die ich gerade vergesse. das überall sei es im chat jeder für sich oder sei es wenn die modis oder rechte hand oder dogfather eine aufgabe oder sachen posten." --------------------------------------------------------------------- WAS ES VORHER GAB Genau eine Sache: `**fett**`, und zwar nur auf den Brettern (`mitFett` in bereich.js). Im Chat gar nichts, bei den Aufgaben nichts. Wer dort Sternchen tippte, sah Sternchen. --------------------------------------------------------------------- WARUM ZEICHEN IM TEXT UND KEIN ECHTER EDITOR Die naheliegende Loesung waere ein Schreibfeld, in dem man direkt fett schreibt (`contenteditable`). Sie ist aus drei Gruenden falsch fuer dieses Haus: 1. Dann steht HTML in der Datenbank. Alles, was von aussen kommt, muesste vor der Anzeige geputzt werden -- und ein Fehler dabei ist kein Schoenheitsfehler, sondern eine Luecke. Hier bleibt in der Datenbank REINER TEXT; gezeichnet wird er erst beim Anzeigen, aus Bausteinen, die dieses Haus selbst baut. 2. Ein solcher Editor verhaelt sich in jedem Browser anders -- besonders auf dem Handy, wo die Haelfte der Leute schreibt. 3. Alles, was schon geschrieben wurde, bliebe lesbar. `**fett**` gibt es seit Wochen auf den Brettern; es funktioniert weiter. --------------------------------------------------------------------- DIE VIER ZEICHEN -- UND WARUM GERADE DIESE **fett** wie bisher im Haus, und wie fast ueberall _kursiv_ ein Unterstrich, wie in Messengern gewohnt __unterstrichen__ zwei davon ~~durchgestrichen~~ wie fast ueberall NIEMAND MUSS SIE SICH MERKEN. Ueber jedem Schreibfeld steht eine kleine Leiste mit vier Knoepfen; wer Text markiert und darauf tippt, bekommt die Zeichen gesetzt. Die Zeichen sind der SPEICHER, nicht die Bedienung. EIN UNTERSTRICH MITTEN IM WORT GILT NICHT. `datei_name_hier` soll nicht kursiv werden, und `@max_muster` schon gar nicht. Deshalb zaehlt ein Unterstrich nur, wenn links davon kein Buchstabe steht und rechts vom schliessenden auch nicht. Bei `**` und `~~` braucht es das nicht -- die tippt niemand versehentlich doppelt. --------------------------------------------------------------------- WAS DIESE DATEI NICHT TUT Sie kennt keine Ueberschriften, keine Listen, keine Links. Links findet der Chat bereits selbst, und Ueberschriften in einer Chatnachricht waeren eine Einladung, laut zu schreien. Wer mehr braucht, bekommt es -- aber dann als Entscheidung, nicht als Nebenwirkung. ===================================================================== */ (function () { 'use strict'; /* Die Reihenfolge ist Bedeutung, nicht Geschmack: `__` muss VOR `_` versucht werden, sonst wuerde aus „__x__“ ein kursives „_x_“. */ var REGELN = [ { name: 'fett', zeichen: '**', muster: /\*\*([\s\S]+?)\*\*/ }, { name: 'durch', zeichen: '~~', muster: /~~([\s\S]+?)~~/ }, /* Die Wortgrenzen stehen nur bei den Unterstrichen -- siehe Kopf. */ { name: 'unter', zeichen: '__', muster: /(^|[^\w])__([^\n_]+?)__(?![\w])/, gruppe: 2, vorne: 1 }, { name: 'kursiv', zeichen: '_', muster: /(^|[^\w])_([^\n_]+?)_(?![\w])/, gruppe: 2, vorne: 1 }, ]; var ELEMENT = { fett: 'strong', kursiv: 'em', unter: 'u', durch: 's' }; /** Zerlegt einen Text in Stuecke mit ihren Stilen. * * Rueckgabe: [{ text: "…", stile: { fett: true, … } }, …] * * ARBEITET REKURSIV, damit Verschachtelung geht ("**fett und * _auch kursiv_**"). Ein Stil, der schon gilt, wird nicht noch * einmal gesucht -- sonst frisst sich das an "****" fest. */ function teile(text, stile) { stile = stile || {}; var roh = String(text == null ? '' : text); for (var i = 0; i < REGELN.length; i++) { var r = REGELN[i]; if (stile[r.name]) continue; var t = roh.match(r.muster); if (!t) continue; var gruppe = r.gruppe || 1; var vorneLang = r.vorne ? (t[r.vorne] || '').length : 0; var anfang = t.index + vorneLang; var vor = roh.slice(0, anfang); var nach = roh.slice(t.index + t[0].length); var innen = {}; for (var k in stile) if (Object.prototype.hasOwnProperty.call(stile, k)) innen[k] = stile[k]; innen[r.name] = true; return [] .concat(vor ? teile(vor, stile) : []) .concat(teile(t[gruppe], innen)) .concat(nach ? teile(nach, stile) : []); } return roh ? [{ text: roh, stile: stile }] : []; } /** Legt die Stile eines Stuecks um einen fertigen Knoten. * * So koennen andere Dateien ihren eigenen Inhalt einsetzen -- der * Chat zum Beispiel seine Links und Erwaehnungen. Die Gestaltung * liegt dann AUSSEN herum, und beides geht zusammen. */ function huelle(knoten, stile) { var innen = knoten; for (var name in ELEMENT) { if (!Object.prototype.hasOwnProperty.call(ELEMENT, name)) continue; if (!stile || !stile[name]) continue; var e = document.createElement(ELEMENT[name]); e.appendChild(innen); innen = e; } return innen; } /** Der einfache Weg: fertiger Textbaustein mit Gestaltung. * * `textContent` und nie `innerHTML` -- was hier hineingeht, ist * Eingabe von Menschen. */ function zeichne(text) { var stueck = document.createDocumentFragment(); var teilchen = teile(text); for (var i = 0; i < teilchen.length; i++) { var t = teilchen[i]; stueck.appendChild(huelle(document.createTextNode(t.text), t.stile)); } return stueck; } /** Nur der Text, ohne Zeichen. * * Fuer Stellen, an denen keine Gestaltung moeglich ist: die * Antwortleiste, eine Meldung auf dem Sperrbildschirm, die * Vorschau in einer Liste. Dort sollen Sternchen nicht als * Sternchen stehen -- das sieht nach kaputt aus. */ function nurText(text) { var teilchen = teile(text); var aus = ''; for (var i = 0; i < teilchen.length; i++) aus += teilchen[i].text; return aus; } /* ===================================================================== DIE LEISTE UEBER DEM SCHREIBFELD VIER KNOEPFE, NICHT ZEHN. Was man nicht findet, benutzt man nicht; was zu viel ist, uebersieht man. Fett, kursiv, unterstrichen, durchgestrichen -- das ist, was Menschen in einem Chat wirklich brauchen. SIE ARBEITET MIT DER MARKIERUNG: Wer Text markiert und auf "F" tippt, bekommt ihn fett. Wer nichts markiert, bekommt die Zeichen gesetzt und steht mit dem Schreibzeiger dazwischen -- man tippt einfach weiter. UND SIE NIMMT ES AUCH WIEDER WEG. Zweimal "F" auf demselben Text entfernt die Sternchen wieder. Ohne das waere der Knopf eine Einbahnstrasse, und die einzige Rueckfahrt waere Loeschen. ===================================================================== */ var KNOEPFE = [ { name: 'fett', zeichen: '**', schild: 'F', titel: 'Fett (Strg+B)', taste: 'b' }, { name: 'kursiv', zeichen: '_', schild: 'K', titel: 'Kursiv (Strg+I)', taste: 'i' }, { name: 'unter', zeichen: '__', schild: 'U', titel: 'Unterstrichen (Strg+U)', taste: 'u' }, { name: 'durch', zeichen: '~~', schild: 'S', titel: 'Durchgestrichen', taste: null }, ]; function umlegen(feld, zeichen) { var wert = feld.value; var a = feld.selectionStart; var b = feld.selectionEnd; var mitte = wert.slice(a, b); var vor = wert.slice(0, a); var nach = wert.slice(b); var n = zeichen.length; /* SCHON GESETZT? Dann weg damit -- einmal von innen (der markierte Text traegt die Zeichen selbst) und einmal von aussen (sie stehen links und rechts der Markierung). Beides kommt vor, je nachdem wie jemand markiert hat. */ if (mitte.length >= 2 * n && mitte.slice(0, n) === zeichen && mitte.slice(-n) === zeichen) { var ohne = mitte.slice(n, -n); feld.value = vor + ohne + nach; feld.setSelectionRange(a, a + ohne.length); } else if (vor.slice(-n) === zeichen && nach.slice(0, n) === zeichen) { feld.value = vor.slice(0, -n) + mitte + nach.slice(n); feld.setSelectionRange(a - n, a - n + mitte.length); } else { feld.value = vor + zeichen + mitte + zeichen + nach; /* Ohne Markierung steht der Schreibzeiger ZWISCHEN den Zeichen -- man tippt einfach weiter. Mit Markierung bleibt sie stehen. */ if (a === b) feld.setSelectionRange(a + n, a + n); else feld.setSelectionRange(a + n, b + n); } feld.focus(); /* Damit die Seite mitbekommt, dass sich etwas geaendert hat -- daran haengen die Hoehe des Feldes und der Senden-Knopf. */ feld.dispatchEvent(new Event('input', { bubbles: true })); } /** Haengt die Leiste ueber ein Schreibfeld. * * Zweimal aufrufen schadet nicht -- gebaut wird nur einmal. Das ist * wichtig, weil manche Seiten ihre Formulare neu aufbauen. */ function leiste(feld, wohin) { if (!feld || feld.dataset.textform === 'ja') return null; feld.dataset.textform = 'ja'; var kasten = document.createElement('div'); kasten.className = 'textform'; kasten.setAttribute('role', 'group'); kasten.setAttribute('aria-label', 'Textgestaltung'); KNOEPFE.forEach(function (k) { var b = document.createElement('button'); b.type = 'button'; b.className = 'textform__knopf textform__knopf--' + k.name; b.textContent = k.schild; b.title = k.titel; b.setAttribute('aria-label', k.titel); b.addEventListener('mousedown', function (e) { /* NICHT DEN FOKUS STEHLEN. Ohne das verliert das Schreibfeld beim Antippen die Markierung -- und der Knopf wuerde die Zeichen ans falsche Ende setzen. */ e.preventDefault(); }); b.addEventListener('click', function () { umlegen(feld, k.zeichen); }); kasten.appendChild(b); }); /* Die Tastenkuerzel, die jeder aus anderen Programmen kennt. */ feld.addEventListener('keydown', function (e) { if (!(e.ctrlKey || e.metaKey) || e.altKey) return; var taste = String(e.key || '').toLowerCase(); for (var i = 0; i < KNOEPFE.length; i++) { if (KNOEPFE[i].taste && KNOEPFE[i].taste === taste) { e.preventDefault(); umlegen(feld, KNOEPFE[i].zeichen); return; } } }); var ziel = wohin || feld.parentNode; if (ziel === feld.parentNode) ziel.insertBefore(kasten, feld); else ziel.appendChild(kasten); return kasten; } window.Textform = { teile: teile, huelle: huelle, zeichne: zeichne, nurText: nurText, leiste: leiste, ZEICHEN: KNOEPFE, }; })();