Filipe, 21.09.2026: "dan will ich dass die leute auch immer die art
von text aussuchen können, fettgedrückt, unterstrichen und so alles.
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."
VORHER GEMESSEN: Es gab genau EINE Auszeichnung im ganzen Haus --
fett, und nur auf den Brettern (`mitFett` in bereich.js). Im Chat
konnte niemand etwas hervorheben.
EINE QUELLE FUER DAS GANZE HAUS: workspace/assets/js/textform.js.
Chat, Bretter und Aufgaben zeichnen ihren Text jetzt durch denselben
Zerleger. Drei Fassungen derselben Regel waeren drei Fassungen, die
verschieden altern.
**fett** _kursiv_ __unterstrichen__ ~~durchgestrichen~~
Dazu eine Leiste ueber jedem Schreibfeld: F K U S, 44 px am Finger,
Strg+B/I/U, und ein zweiter Druck nimmt die Zeichen wieder weg.
DIE WORTGRENZEN SIND DER EIGENTLICHE BAU. Ein Zerleger, der alles
auszeichnet, ist schlimmer als keiner: `datei_name_hier` hiesse
plotzlich anders, als es heisst, und `@max_muster` ebenso. Deshalb
stehen bei den Unterstrichen Wortgrenzen -- bei Sternchen und Tilden
nicht, die kommen in Text nicht versehentlich paarweise vor.
GEBAUT MIT createTextNode, NIE innerHTML. Wer Text zu Auszeichnung
macht, ist einen Tippfehler von einer Luecke entfernt. Das ist heute
sicher; pruef-textform misst es, damit es das morgen auch ist.
EIN ECHTER FEHLER DABEI GEFUNDEN -- in meinem eigenen Einbau von
vorhin: Auf dem Aufgabenbrett stand `$('f-text')` in einem Block, der
`$` gar nicht kennt (die Datei hat drei getrennte Bloecke).
`ReferenceError: $ is not defined`, die Leiste kam dort nie an, und
die Seite sah dabei vollkommen normal aus. Derselbe Block beginnt
ausserdem mit `if (!feld) return;` auf ein Aufwand-Feld, das mit
Textgestaltung nichts zu tun hat -- die Leiste haette an einer
voellig fremden Bedingung gehangen. Jetzt ein eigener Block, ohne
fremden Helfer und ohne fremde Bedingung.
Gefunden hat es die Pruefung, weil sie die Leiste AM FELD sucht,
statt den Aufruf im Quelltext zu zaehlen. Sie sammelt seitdem auch
Skriptfehler ein -- ein ReferenceError ist immer ein Befund, auch
wenn man noch nicht weiss, was er anrichtet.
pruef-textform.mjs, 50 Aussagen:
- welche Seite das Modul braucht, wird ABGELEITET (beide
Richtungen: keine ohne, keine umsonst) -- 3 brauchen, 32 nicht
- 13 Regelfaelle im echten Browser an der echten Datei, davon
sechs, die NICHT gestaltet werden duerfen
- eine echte Nachricht wird zu <strong>/<em>/<u>/<s>
- Sicherheitsprobe: <b> und <img onerror> bleiben Text, nichts
wird ausgefuehrt -- und die echte Auszeichnung daneben wirkt
trotzdem (sonst waere nur bewiesen, dass gar nichts passiert)
- die Leiste legt Zeichen wirklich um und wieder ab
- Gegenproben fuer beide Richtungen
Nachbarn unveraendert gruen: pruef-chat-optik 45/0 (war 39 -- die
Antwortleiste kam dazu), pruef-aufgabenbrett 49/0, pruef-nachfrage
49/0, pruef-css-klassen 30/0, pruef-tippziele 11/0, pruef-meldungen
8/0.
269 lines
11 KiB
JavaScript
269 lines
11 KiB
JavaScript
/* =====================================================================
|
|
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,
|
|
};
|
|
})();
|