fix: Tab rückt Zeilen ein und zerstört kein Undo mehr (D53)
Zwei gemeldete Fehler, eine Zeile:
src.value = value.slice(0, s) + ' ' + value.slice(eEnd);
1. Sie schneidet den markierten Bereich heraus — mehrere markierte
Zeilen wurden also durch zwei Leerzeichen ERSETZT. Ohne Auswahl fügt
derselbe Ausdruck nur ein, deshalb fiel es nie auf.
2. `src.value =` löscht die Undo-Historie des Textfelds vollständig
(D38-Nachtrag 2 hatte das gemessen, aber nur beim Zurückschreiben der
Faltung beherzigt). Betroffen war damit alles davor Getippte. Erneut
gemessen, alte Zeile nachgestellt: erstes `undo` ändert nichts,
zweites liefert `false` — Stapel leer. Das ist die Antwort auf „wann
geht Undo kaputt": bei jedem Tab-Druck. Die übrigen drei
`src.value =` laden ein anderes Dokument, dorthin gibt es nichts
zurückzunehmen.
Neue Regel: ohne Auswahl zwei Leerzeichen an der Marke (Shift+Tab nimmt
sie zurück und zieht die Marke mit), mit Auswahl wird jede berührte
ZEILE ein-/ausgerückt. Verworfen war die VS-Code-Feinregel „nur bei
mehrzeiliger Auswahl einrücken": Wer eine ganze Zeile markiert, meint
auch dann Einrücken, und die einfache Regel kann nie Text löschen.
Geschrieben wird über execCommand('insertText') — ein Undo-Schritt.
Nebenbefund mitbehoben: Tab war eine Tastenfalle (WCAG 2.1.2). Esc hebt
sie für den nächsten Tastendruck auf.
Nachgemessen mit echten Tastendrücken: drei markierte Zeilen, Tab -> alle
drei von 2 auf 4 Leerzeichen, nichts gelöscht, Block bleibt ausgewählt;
Shift+Tab zurück; tippen + Tab + zweimal undo -> erst der Einzug, dann
das Getippte. Werkzeuggrenze notiert: synthetisches ctrl+z löst kein
natives Undo aus, geprüft wird mit execCommand('undo').
240 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7904c82481
commit
6935b4882d
+73
-9
@@ -673,16 +673,80 @@ async function downloadDiagramPng(){
|
||||
if(png && png.blob) saveBlob(png.blob, 'werkbaum-diagramm.png');
|
||||
}
|
||||
|
||||
/* Tab-Taste rückt ein statt den Fokus zu wechseln */
|
||||
src.addEventListener('keydown', e => {
|
||||
if(e.key === 'Tab'){
|
||||
e.preventDefault();
|
||||
const {selectionStart:s, selectionEnd:eEnd, value} = src;
|
||||
src.value = value.slice(0, s) + ' ' + value.slice(eEnd);
|
||||
src.selectionStart = src.selectionEnd = s + 2;
|
||||
render();
|
||||
saveSrc();
|
||||
/* ---------- Tab rückt ein (D53) ----------
|
||||
Einrückung IST hier die Hierarchie (SPEC §2), Tab also die häufigste Taste
|
||||
nach den Buchstaben. Zwei Regeln:
|
||||
|
||||
- **Ohne Auswahl** zwei Leerzeichen an der Schreibmarke (Tab zählt in dieser
|
||||
Notation als zwei, SPEC §2); Shift+Tab nimmt sie der Zeile wieder weg.
|
||||
- **Mit Auswahl** wird jede berührte ZEILE ein-/ausgerückt, nie die Auswahl
|
||||
ersetzt. In einem Notationstext ist Einrücken praktisch immer gemeint, und
|
||||
diese Regel kann nichts löschen. Danach ist der ganze Zeilenblock
|
||||
ausgewählt, sodass wiederholtes Tab weiter einrückt.
|
||||
|
||||
Geschrieben wird über `execCommand('insertText')` — die einzige Art, ein
|
||||
Textfeld zu ändern, ohne die Rückgängig-Historie zu zerstören (D38-Nachtrag
|
||||
2, hier erneut gemessen). Das alte `src.value = …` hat sie bei JEDEM
|
||||
Tastendruck gelöscht, also auch das davor Getippte. */
|
||||
const IND = ' ';
|
||||
function outdentLine(l){
|
||||
if(l.startsWith(IND)) return l.slice(IND.length);
|
||||
if(l.startsWith('\t') || l.startsWith(' ')) return l.slice(1);
|
||||
return l;
|
||||
}
|
||||
function indentSelection(out){
|
||||
const v = src.value, s = src.selectionStart, e = src.selectionEnd;
|
||||
const von = v.lastIndexOf('\n', s - 1) + 1;
|
||||
if(s === e){
|
||||
if(!out) return writeAt(s, e, IND, s + IND.length, s + IND.length);
|
||||
/* Ausrücken ohne Auswahl: der Zeile den Einzug nehmen, die Schreibmarke um
|
||||
dasselbe Stück mitziehen — sie soll am selben Zeichen stehen bleiben. */
|
||||
let bisZ = v.indexOf('\n', s);
|
||||
if(bisZ === -1) bisZ = v.length;
|
||||
const zeile = v.slice(von, bisZ), kurz = outdentLine(zeile);
|
||||
if(kurz === zeile) return false;
|
||||
const weg = zeile.length - kurz.length;
|
||||
const p = Math.max(von, s - weg);
|
||||
return writeAt(von, bisZ, kurz, p, p);
|
||||
}
|
||||
/* Endet die Auswahl genau auf einem Zeilenanfang, gehört diese Zeile nicht
|
||||
mehr dazu — sonst rückte ein Zug bis zum nächsten Zeilenbeginn eine Zeile
|
||||
zu viel ein. */
|
||||
const eAdj = e > s && e > von && v[e-1] === '\n' ? e - 1 : e;
|
||||
let bis = v.indexOf('\n', eAdj);
|
||||
if(bis === -1) bis = v.length;
|
||||
const alt = v.slice(von, bis);
|
||||
const neu = alt.split('\n')
|
||||
.map(l => out ? outdentLine(l) : (l ? IND + l : l)) /* Leerzeilen bleiben leer */
|
||||
.join('\n');
|
||||
if(neu === alt) return false;
|
||||
return writeAt(von, bis, neu, von, von + neu.length);
|
||||
}
|
||||
/* Ersetzt [von,bis) durch `ein` und setzt danach die Auswahl — undo-fähig.
|
||||
`input` feuert dabei von selbst, render() und saveSrc() hängen daran. */
|
||||
function writeAt(von, bis, ein, selA, selB){
|
||||
src.setSelectionRange(von, bis);
|
||||
let ok = false;
|
||||
try{ ok = document.execCommand('insertText', false, ein); }catch(_){}
|
||||
if(!ok){
|
||||
/* Rückfall: der richtige Text geht vor der Historie (wie D38-Nachtrag 2). */
|
||||
src.value = src.value.slice(0, von) + ein + src.value.slice(bis);
|
||||
src.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
}
|
||||
src.setSelectionRange(selA, selB);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Tab im Textfeld ist eine Tastenfalle (WCAG 2.1.2): Wer nur die Tastatur
|
||||
benutzt, käme sonst nicht mehr heraus. Esc hebt sie für den NÄCHSTEN
|
||||
Tastendruck auf — der übliche Ausweg. */
|
||||
let tabEscapes = false;
|
||||
src.addEventListener('keydown', e => {
|
||||
if(e.key === 'Escape'){ tabEscapes = true; return; }
|
||||
if(e.key !== 'Tab'){ tabEscapes = false; return; }
|
||||
if(tabEscapes){ tabEscapes = false; return; } /* Fokus darf weiterwandern */
|
||||
e.preventDefault();
|
||||
indentSelection(e.shiftKey);
|
||||
});
|
||||
|
||||
src.addEventListener('input', render);
|
||||
|
||||
Reference in New Issue
Block a user