frontend: Zeilennummern im Texteditor + Alt+Klick auch im Text
Zwei Lücken, die beim Arbeiten am Text auffallen: **Zeilennummern (D33).** Die Warnungen nennen Zeilennummern („Zeile 12: …"), das Textfeld zeigte keine — man musste abzählen. Der Streifen links vom Textfeld hebt zusätzlich die Cursor-Zeile und Zeilen mit Warnung hervor. Die Zahlen stehen auf **gemessenen** Höhen (Spiegel-`div` aus D25, ein Marker je Zeile), nicht auf „Zeilenhöhe × n": Der Text bricht weich um, eine lange Zeile belegt mehrere Bildzeilen und behält doch eine Nummer. Bei 375 px Breite brechen 134 der 143 Zeilen um — gerechnet liefe der Streifen sofort davon. Dabei fiel ein älterer Fehler im Spiegel auf: `width = src.clientWidth` bei `content-box` machte ihn um genau die 32 px Innenabstand zu breit, er brach später um als das Textfeld. Mit `box-sizing:border-box` stimmen die Umbrüche nachweislich überein (170 Bildzeilen beiderseits) — das behebt nebenbei die Zielgenauigkeit des Sprungs zu langen Zeilen (D25). **Alt+Klick im Textfeld (Nachtrag zu D25).** Die Rückrichtung lief bisher allein über die Cursor-Zeile — und die scrollt bewusst nur `nearest` und nur beim Zeilenwechsel, sonst ruckelte das Diagramm beim Tippen. Als „zeig mir das" taugt sie damit nicht. Alt+Klick im Text (Tastatur Alt+Enter) zentriert den Knoten jetzt und gibt ihm den Fokus: derselbe Modifier in beide Richtungen, und mit Fokus wird daraus ein vollständiger Hin- und Rückweg. Die Legenden-Zeile nennt beide Richtungen (kein zehnter i18n-Schlüssel), 9 Sprachen nachgezogen. SPEC §9 ergänzt, D33 angelegt, Plan-Dokument und frontend/CLAUDE.md nachgeführt. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -587,6 +587,38 @@ es nur auf einem Gerät), sowie ein eigener Umschalter dafür (weiteres
|
||||
Bedienelement in einer engen Kopfzeile plus i18n in 9 Sprachen, für ein
|
||||
Verhalten, das kaum jemand umstellen will).
|
||||
|
||||
**Nachtrag — Alt+Klick gibt es jetzt auch im Textfeld.** Gemeldet als „das
|
||||
Alt+Klick muss im Text-Editor wieder funktionieren, um den Knoten im Diagramm zu
|
||||
fokussieren". Nachgesehen: Die Geste hat es dort nie gegeben — die Rückrichtung
|
||||
lief bisher **allein** über die Cursor-Zeile. Das ist trotzdem kein
|
||||
Missverständnis, sondern ein Befund: Die Rückrichtung fühlt sich schwächer an,
|
||||
weil sie es ist.
|
||||
|
||||
Der Grund steckt in ihrer eigenen Auslegung. Die Cursor-Zeile scrollt bewusst
|
||||
**nur beim Zeilenwechsel** und **nur `nearest`** — beides notwendig, sonst
|
||||
ruckelte das Diagramm bei jedem Tastendruck. Genau das macht sie aber als *Zeig
|
||||
mir das* untauglich: Wer denselben Knoten noch einmal sucht, bekommt nichts, und
|
||||
wer ihn am Bildrand hat, bekommt ihn an den Bildrand. Es fehlte also nicht die
|
||||
Verknüpfung, sondern eine **ausdrückliche** Geste daneben.
|
||||
|
||||
Deshalb: **Alt+Klick im Textfeld** (Tastatur **Alt+Enter**) zentriert den Knoten
|
||||
der Cursor-Zeile und gibt ihm den **Tastaturfokus**. Derselbe Modifier wie in der
|
||||
Gegenrichtung — eine Geste, zwei Richtungen, nichts Neues zu lernen; die
|
||||
vorhandene Legenden-Zeile (`hint_jump`) nennt jetzt beide Richtungen, statt einen
|
||||
zehnten i18n-Schlüssel in neun Sprachen aufzumachen.
|
||||
|
||||
**Der Fokus wandert wirklich mit** (`el.focus({preventScroll:true})`, dann
|
||||
bewusst `scrollIntoView({block:'center'})`). Erwogen war, nur zu scrollen und den
|
||||
Cursor im Text zu lassen — weniger störend beim Tippen. Dagegen sprechen zwei
|
||||
Dinge: Die Gegenrichtung nimmt den Fokus ebenso mit (in den Text hinein), und ein
|
||||
Screenreader erfährt vom Scrollen nichts. Mit Fokus wird daraus ein
|
||||
vollständiger Hin- und Rückweg: Alt+Klick in den Baum, dort mit Tab/Pfeil
|
||||
weiter, Alt+Enter zurück in die Zeile.
|
||||
|
||||
`preventDefault()` beim Tastaturweg ist Pflicht, sonst bekommt der Text einen
|
||||
Umbruch. Auf einer Zeile ohne Knoten (Kommentar, Leerzeile, ausgeblendetes
|
||||
Verworfenes) geschieht nichts — dieselbe stille Regel wie bei der Cursor-Zeile.
|
||||
|
||||
## D26 — Legende scrollbar: eigener Container statt `<details>`, plus Splitter
|
||||
Die Legende („Agenda") im Editor-Panel war zu hoch für ihren Platz und wurde
|
||||
**abgeschnitten** statt scrollbar zu sein — obwohl `.hint` seit jeher
|
||||
@@ -1389,3 +1421,53 @@ Die Alternativen stehen als any-of-Gruppe im mitgelieferten Plan
|
||||
gemessenen Sackgassen als `[-]` mit dem Messergebnis im Kommentar, damit niemand
|
||||
sie erneut aufmacht. Der günstigste Pfad wählt dort von selbst die
|
||||
Rate-Limit-Anhebung — die billigste wirksame Änderung.
|
||||
|
||||
## D33 — Zeilennummern im Texteditor, gemessen statt gerechnet
|
||||
Die Warnungen nennen Zeilennummern („Zeile 12: unbekannter Statuscode", SPEC §4)
|
||||
— und das Textfeld zeigte keine. Man musste abzählen. Der Streifen links vom
|
||||
Textfeld schließt diese Lücke; sie ist umso spürbarer, je größer der Plan ist
|
||||
(der mitgelieferte hat 143 Zeilen).
|
||||
|
||||
**Ein eigener Kasten neben dem Textfeld, kein Markup im Text.** Ein `<textarea>`
|
||||
kennt keine Auszeichnung — man kann in seinen Textfluss nichts einfügen. Also
|
||||
ein zweiter Kasten, der nicht selbst scrollt, sondern gegen `src.scrollTop`
|
||||
verschoben wird (`translateY`). So kann er nicht auseinanderlaufen: Es gibt nur
|
||||
eine Scrollposition, nicht zwei.
|
||||
|
||||
**Die Zahlen stehen auf gemessenen Höhen, nicht auf „Zeilenhöhe × n".** Der Text
|
||||
bricht weich um; eine lange Zeile belegt mehrere Bildzeilen, behält aber **eine**
|
||||
Nummer. Gerechnet liefe der Streifen deshalb schon nach der ersten langen Zeile
|
||||
davon — im Test bei 375 px Breite brechen 134 der 143 Zeilen um, meist vierfach.
|
||||
Gemessen wird am **Spiegel-`div`**, das es für das Scrollen beim Sprung schon
|
||||
gibt (D25): ein Marker je Zeile, einmal schreiben, dann alle `offsetTop` in einem
|
||||
Durchgang lesen — sonst erzwingt jede einzelne Messung ein eigenes Neu-Layout.
|
||||
|
||||
**Dabei fiel ein Fehler im Spiegel auf, der schon D25 betraf.** Der Spiegel bekam
|
||||
`width = src.clientWidth`, war aber `content-box`: `clientWidth` **enthält** die
|
||||
Innenabstände, der Spiegel war also um genau 32 px breiter als das Textfeld und
|
||||
brach später um. Mit `box-sizing:border-box` stimmen die Umbrüche jetzt
|
||||
nachweislich überein — Zeilen im Spiegel und Bildzeilen im Textfeld ergeben
|
||||
dieselbe Gesamthöhe (gemessen: 170 Zeilen beide). Vorher war der Sprung zu einer
|
||||
langen Zeile um die Höhe der übersprungenen Umbrüche daneben.
|
||||
|
||||
**Zwei Zeilen heben sich ab: die Cursor-Zeile und Zeilen mit Warnung** (in
|
||||
`--warn`). Genau die beiden Fälle, für die man in den Streifen sieht. Die
|
||||
Warnungsmenge kommt aus **derselben** Liste, die im Warnungsbereich steht —
|
||||
`render()` reicht sie weiter, statt sie ein zweites Mal zu ermitteln; sonst
|
||||
liefen Text und Streifen irgendwann auseinander.
|
||||
|
||||
**Verworfen: `wrap="off"`.** Ohne weichen Umbruch wäre jede logische Zeile genau
|
||||
eine Bildzeile, die Rechnung trivial und der Spiegel überflüssig. Preis wäre
|
||||
waagerechtes Scrollen — im schmalen Spiegel neben einem eingebetteten Pad (D31)
|
||||
oder auf dem Telefon (D17) wäre der Text damit unlesbar. Der Umbruch ist die
|
||||
wichtigere Eigenschaft.
|
||||
|
||||
**Kein Umschalter.** Die Kopfzeile ist eng (D17), und ein Bedienelement kostet
|
||||
i18n in neun Sprachen für etwas, das keinen Zustand hat, den jemand pflegen
|
||||
will. Der Streifen ist so schmal wie die Ziffern es verlangen
|
||||
(`calc(<Stellen>ch + 12px)`), auf dem Telefon sind das 36 px.
|
||||
|
||||
**Stolperfalle bei der Prüfung:** Ein programmatisch gesetztes `scrollTop` löst
|
||||
`scroll` erst im nächsten Bild aus — in einem nicht gezeichneten Tab womöglich
|
||||
gar nicht. `scrollEditorToOffset()` zieht die Zahlen deshalb selbst gleich mit,
|
||||
statt sich auf das Ereignis zu verlassen.
|
||||
|
||||
@@ -386,6 +386,13 @@ verknüpft (siehe D25):
|
||||
hervorgehoben (weißer Halo + Ring in Tinte) und beim Zeilenwechsel ins Bild
|
||||
gescrollt. Zeilen ohne Knoten (Kommentar, Leerzeile, ausgeblendetes
|
||||
verworfenes Element) heben nichts hervor.
|
||||
- **Text → Diagramm, ausdrücklich:** **Alt+Klick** im Textfeld (Tastatur:
|
||||
**Alt+Enter**) holt den Knoten der Cursor-Zeile in die **Mitte** des Diagramms
|
||||
und gibt ihm den Tastaturfokus — dieselbe Geste in beide Richtungen. Sie
|
||||
ergänzt das Mitlaufen der Cursor-Zeile: Das scrollt bewusst nur so weit wie
|
||||
nötig und nur beim Zeilenwechsel (sonst ruckelte das Diagramm beim Tippen).
|
||||
Wer einen Knoten wirklich **sehen** will, sagt es mit Alt. Auf einer Zeile ohne
|
||||
Knoten geschieht nichts.
|
||||
- Der **einfache** Klick bleibt unverändert der Link (§6): Ein Knoten mit URL
|
||||
öffnet sie weiterhin im neuen Tab. Deshalb Alt und nicht der einfache Klick.
|
||||
- **Auffindbarkeit:** Solange **Alt gedrückt** ist, zeigen alle Knoten den
|
||||
@@ -396,6 +403,20 @@ verknüpft (siehe D25):
|
||||
- Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im
|
||||
Druck.
|
||||
|
||||
### Zeilennummern im Texteditor
|
||||
Links neben dem Textfeld steht ein schmaler Streifen mit den **Zeilennummern** —
|
||||
dieselben, die die Warnungen nennen („Zeile 12: …", §4). Ohne ihn muss man sie
|
||||
im Text abzählen.
|
||||
|
||||
- Gezählt werden **logische** Zeilen des Notationstexts. Eine weich umbrochene
|
||||
Zeile behält **eine** Nummer; die Nummer steht an ihrer ersten Bildzeile.
|
||||
- Die **Cursor-Zeile** und Zeilen mit einer **Warnung** heben sich ab (Warnung in
|
||||
`--warn`, `#B45309`) — genau die beiden Fälle, für die man hinsieht.
|
||||
- Reine Lesehilfe: kein Bestandteil des Notationstexts, nicht im Grafikexport
|
||||
und nicht im Druck (dort ist ohnehin nur das Diagramm zu sehen).
|
||||
|
||||
Siehe D33.
|
||||
|
||||
### Fokusmarke im Diagramm (`!!!`, §1)
|
||||
Ein mit `!!!` markierter Knoten trägt einen **Strahlenkranz in Petrol** —
|
||||
kräftiger Ring in `--or` (`#0F766E`) plus leuchtender Schein in hellem Teal
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
- [^] Switcher in the editor title bar (S)
|
||||
- [^] Load a document from ?sourceUrl= (S)
|
||||
- [^] Jump between diagram and text (S)
|
||||
- [x] Line numbers in the text editor (XS) %% the warnings name them
|
||||
- [ ] Open and save .werkbaum files (S)
|
||||
+ [^] Show what is new since your last visit (S)
|
||||
+ [?] A pastel colour per person (S)
|
||||
|
||||
@@ -208,6 +208,31 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
||||
die Klasse `alt` an `#out` (Cursor + Ring am Knoten unter dem Zeiger) — der
|
||||
`blur`-Handler ist Pflicht, sonst bleibt der Modus nach Alt+Tab hängen; dazu
|
||||
`jumpHint` im Knoten-Tooltip und `hint_jump` als letzte Zeile der Legende.
|
||||
- Alt+Klick **im Textfeld** (D25, Nachtrag): `focusNodeOfCaret()` zentriert den
|
||||
Knoten der Cursor-Zeile und gibt ihm den Fokus (`focus({preventScroll:true})`
|
||||
**vor** `scrollIntoView({block:'center'})` — sonst scrollt der Browser zweimal).
|
||||
Das ist nicht dasselbe wie das Mitlaufen der Cursor-Zeile: `syncCaret()` scrollt
|
||||
absichtlich nur `nearest` und nur beim Zeilenwechsel. Beim Tastaturweg
|
||||
(Alt+Enter) ist `preventDefault()` Pflicht, sonst bekommt der Text einen
|
||||
Umbruch. Die Legenden-Zeile `hint_jump` nennt **beide** Richtungen — kein
|
||||
eigener i18n-Schlüssel.
|
||||
- Zeilennummern (D33): `renderLineNos()` misst die Zeilenoberkanten am
|
||||
Spiegel-`div` (`lineTops()`) — **nicht** `Zeilenhöhe × n`, das läuft beim
|
||||
ersten weichen Umbruch davon. Der Streifen scrollt nicht selbst, sondern wird
|
||||
per `translateY(-src.scrollTop)` mitgezogen (`syncLineNoScroll()`); wer
|
||||
`src.scrollTop` **selbst** setzt, muss ihn gleich mitziehen — das
|
||||
`scroll`-Ereignis kommt erst im nächsten Bild (`scrollEditorToOffset()` tut es).
|
||||
Der Spiegel braucht `box-sizing:border-box`: `src.clientWidth` enthält die
|
||||
Innenabstände, ohne das ist er 32 px zu breit und bricht später um (betraf
|
||||
vorher auch schon das Scrollen beim Sprung, D25). Der Marker ist ein
|
||||
Inline-Kasten und sitzt in seiner Zeilenbox mittig — die Zahlen müssen um
|
||||
diesen Versatz (`drop`) nach oben, sonst stehen sie durchgehend zu tief.
|
||||
Aufgerufen wird aus `render()` (dort kommt auch die Warnungsmenge her) und aus
|
||||
einem `ResizeObserver` auf `#src` (Umbruch hängt an der Breite). Markup:
|
||||
`#srcWrap` umschließt Streifen + Textfeld; die Pad-Regeln (D31) teilen gegen
|
||||
**diese Spalte** (`.src-area.pv-both>.src-wrap`), nicht gegen das nackte
|
||||
Textfeld. `caretLine` steht deshalb **oben** bei `padView` — der Streifen liest
|
||||
sie und hängt an `render()` (temporale Todeszone, siehe D31).
|
||||
- Touch-Langdruck (D25): Der Timer (500 ms) setzt **nur** die Klasse `armed`
|
||||
(Petrol-Ring); `jumpToLine()` läuft im `touchend`-Handler. Nicht zurück in den
|
||||
Timer verlegen — **`focus()` aus einem Timer gilt in mobilen Browsern nicht als
|
||||
|
||||
@@ -87,8 +87,13 @@
|
||||
<div class="src-area pv-text" id="srcArea">
|
||||
<iframe class="padframe" id="padFrame" title="Etherpad" hidden></iframe>
|
||||
<div class="pad-gutter" id="padGutter" role="separator" data-i18n-title="gutterTooltip" data-i18n-aria="padGutterAria" title="Ziehen zum Verschieben, Doppelklick setzt zurück" aria-label="Pad und Texteditor größenverändern" hidden></div>
|
||||
<!-- Zeilennummern + Textfeld sind eine Spalte: der Splitter oben teilt den
|
||||
Rahmen gegen diese Spalte, nicht gegen das nackte Textfeld. -->
|
||||
<div class="src-wrap" id="srcWrap">
|
||||
<div class="lineno" id="lineNos" aria-hidden="true"><div class="lineno-inner"></div></div>
|
||||
<textarea id="src" spellcheck="false" data-i18n-aria="editorTitle" aria-label="Struktur (Text)"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint-gutter" id="hintGutter" role="separator" data-i18n-title="gutterTooltip" data-i18n-aria="hintGutterAria" title="Ziehen zum Verschieben, Doppelklick setzt zurück" aria-label="Editor und Legende größenverändern" hidden></div>
|
||||
<div class="agenda" id="agenda">
|
||||
<button type="button" class="agenda-summary" id="agendaToggle" aria-expanded="false" aria-controls="hint" data-i18n="agenda">Agenda</button>
|
||||
|
||||
+148
-19
@@ -34,6 +34,10 @@ const INITIAL = `%% Project structure – Sprint 14
|
||||
const src = document.getElementById('src');
|
||||
const out = document.getElementById('out');
|
||||
const warnBox = document.getElementById('warn');
|
||||
/* Zeilennummern-Streifen (D33) — hier oben geholt, weil render() ihn füllt. */
|
||||
const srcWrap = document.getElementById('srcWrap');
|
||||
const lineNoBox = document.getElementById('lineNos');
|
||||
const lineNoInner = lineNoBox.firstElementChild;
|
||||
|
||||
/* Baum-/Kostenlogik (gateOf, needsBreakdown, visibleChildren, günstigster
|
||||
Pfad) lebt headless in model.js, das HTML-Erzeugen in render.js. Hier bleibt
|
||||
@@ -44,6 +48,11 @@ let cheapPathOn = true;
|
||||
kann — weiter unten stünde sie dann noch in der temporalen Todeszone. */
|
||||
const PAD_VIEWS = ['both', 'pad', 'text'];
|
||||
let padView = 'both';
|
||||
/* Cursor-Zeile (D25) — bleibt `null`, bis der Cursor das erste Mal bewegt wurde,
|
||||
sonst wäre nach dem Laden ungefragt die Wurzel markiert. Hier oben deklariert
|
||||
wie `padView`: der Zeilennummern-Streifen (D33) liest sie, und der hängt an
|
||||
render(). */
|
||||
let caretLine = null, currentNodeEl = null;
|
||||
/* Warnung des ?sourceUrl-Ladens (D23) — zeilenlos und persistent, siehe render(). */
|
||||
let sourceWarning = null;
|
||||
/* „Was ist neu?" (D28): Knoten, die gegenüber der zuletzt GESEHENEN Fassung neu
|
||||
@@ -90,6 +99,10 @@ function render(){
|
||||
|
||||
warnings = warnings.slice().sort((a, b) => (a.line || 0) - (b.line || 0));
|
||||
warnBox.innerHTML = warnings.map(w => `<div>⚠ ${formatWarning(w, t)}</div>`).join('');
|
||||
/* Der Zeilennummern-Streifen zeigt genau die Zeilen an, die hier genannt
|
||||
werden — deshalb hängt er an derselben Warnungsliste (D33). */
|
||||
lineNoWarn = new Set(warnings.map(w => w.line).filter(Boolean));
|
||||
renderLineNos();
|
||||
applyOptStairs(); /* muss vor dem Messen laufen — es verschiebt Knoten */
|
||||
alignStems();
|
||||
drawCheapPath();
|
||||
@@ -488,11 +501,17 @@ function lineRange(line){
|
||||
an weichen Umbrüchen (lange Zeilen belegen mehrere Bildzeilen), deshalb ein
|
||||
unsichtbarer Spiegel mit gleicher Typografie und Breite plus Marker-Span. */
|
||||
let mirrorEl = null;
|
||||
function offsetTopInEditor(offset){
|
||||
function syncMirror(){
|
||||
if(!mirrorEl){
|
||||
mirrorEl = document.createElement('div');
|
||||
mirrorEl.setAttribute('aria-hidden', 'true');
|
||||
/* `box-sizing:border-box` ist Pflicht: `src.clientWidth` **enthält** die
|
||||
Innenabstände. Ohne das ist der Spiegel um genau diese 32 px breiter als
|
||||
das Textfeld und bricht später um — lange Zeilen landeten dadurch zu weit
|
||||
oben (fiel beim Bau der Zeilennummern auf, D33; betraf auch schon das
|
||||
Scrollen beim Sprung, D25). */
|
||||
mirrorEl.style.cssText = 'position:absolute;visibility:hidden;top:0;left:-9999px;' +
|
||||
'box-sizing:border-box;' +
|
||||
'white-space:pre-wrap;overflow-wrap:break-word;';
|
||||
document.body.appendChild(mirrorEl);
|
||||
}
|
||||
@@ -502,20 +521,104 @@ function offsetTopInEditor(offset){
|
||||
mirrorEl.style[p] = cs[p];
|
||||
}
|
||||
mirrorEl.style.width = src.clientWidth + 'px';
|
||||
mirrorEl.textContent = src.value.slice(0, offset);
|
||||
return mirrorEl;
|
||||
}
|
||||
const ZWSP = '';
|
||||
function offsetTopInEditor(offset){
|
||||
const m = syncMirror();
|
||||
m.textContent = src.value.slice(0, offset);
|
||||
const marker = document.createElement('span');
|
||||
marker.textContent = '';
|
||||
mirrorEl.appendChild(marker);
|
||||
marker.textContent = ZWSP;
|
||||
m.appendChild(marker);
|
||||
const top = marker.offsetTop;
|
||||
mirrorEl.textContent = '';
|
||||
m.textContent = '';
|
||||
return top;
|
||||
}
|
||||
|
||||
/* Oberkante jeder **logischen** Zeile, im selben Koordinatensystem wie
|
||||
`src.scrollTop` (die Innenabstände stecken schon drin). Ein Marker je Zeile
|
||||
im selben Spiegel: einmal schreiben, dann alle `offsetTop` in einem Durchgang
|
||||
lesen — sonst erzwingt jede Messung ein eigenes Neu-Layout. */
|
||||
function lineTops(){
|
||||
const m = syncMirror();
|
||||
m.textContent = '';
|
||||
const marks = [];
|
||||
for(const line of src.value.split('\n')){
|
||||
const s = document.createElement('span');
|
||||
s.textContent = ZWSP;
|
||||
m.appendChild(s);
|
||||
marks.push(s);
|
||||
m.appendChild(document.createTextNode(line + '\n'));
|
||||
}
|
||||
const tops = marks.map(s => s.offsetTop);
|
||||
m.textContent = '';
|
||||
return tops;
|
||||
}
|
||||
|
||||
/* ---------- Zeilennummern (D33) ----------
|
||||
Die Warnungen nennen Zeilennummern (SPEC §4); ohne Streifen muss man sie im
|
||||
Textfeld abzählen. Der Streifen ist ein eigener Kasten neben dem Textfeld —
|
||||
in den Textfluss lässt sich nichts einfügen, ein `<textarea>` kennt kein
|
||||
Markup. Gescrollt wird er nicht selbst, sondern gegen `src.scrollTop`
|
||||
verschoben; so kann er nie auseinanderlaufen. */
|
||||
let lineNoWarn = new Set();
|
||||
function renderLineNos(){
|
||||
const tops = lineTops();
|
||||
/* Der Marker ist ein Inline-Kasten und steht in seiner Zeilenbox mittig — sein
|
||||
`offsetTop` liegt also ein paar Pixel unter der Zeilen**oberkante**. Die Zahl
|
||||
bekommt eine eigene Zeilenbox gleicher Höhe und muss deshalb an der
|
||||
Oberkante ansetzen, sonst stünde sie durchgehend zu tief. Der Versatz ist an
|
||||
der ersten Zeile ablesbar: dort ist die Oberkante der obere Innenabstand. */
|
||||
const drop = tops.length ? tops[0] - (parseFloat(getComputedStyle(src).paddingTop) || 0) : 0;
|
||||
/* Breite nur schreiben, wenn sie sich ändert: sie ändert die Breite des
|
||||
Textfelds, das weckt den ResizeObserver — und der ruft wieder hierher. */
|
||||
const w = 'calc(' + String(tops.length).length + 'ch + 12px)';
|
||||
if(lineNoBox.style.width !== w) lineNoBox.style.width = w;
|
||||
/* Vorhandene Zahlen weiterverwenden: bei jedem Tastendruck N Elemente neu zu
|
||||
bauen wäre Müll für nichts — die Zahlen ändern sich fast nie, nur ihre Höhe. */
|
||||
const have = lineNoInner.children.length;
|
||||
for(let i = have; i < tops.length; i++){
|
||||
lineNoInner.appendChild(document.createElement('span'));
|
||||
}
|
||||
for(let i = have - 1; i >= tops.length; i--) lineNoInner.children[i].remove();
|
||||
for(let i = 0; i < tops.length; i++){
|
||||
const s = lineNoInner.children[i], n = i + 1;
|
||||
if(s.textContent !== String(n)) s.textContent = n;
|
||||
s.style.top = (tops[i] - drop) + 'px';
|
||||
s.classList.toggle('warn', lineNoWarn.has(n));
|
||||
}
|
||||
markCurrentLineNo();
|
||||
syncLineNoScroll();
|
||||
}
|
||||
function syncLineNoScroll(){
|
||||
lineNoInner.style.transform = 'translateY(' + (-src.scrollTop) + 'px)';
|
||||
}
|
||||
function markCurrentLineNo(){
|
||||
const cur = lineNoInner.querySelector('span.cur');
|
||||
if(cur) cur.classList.remove('cur');
|
||||
const s = caretLine == null ? null : lineNoInner.children[caretLine - 1];
|
||||
if(s) s.classList.add('cur');
|
||||
}
|
||||
src.addEventListener('scroll', syncLineNoScroll);
|
||||
/* Der Umbruch hängt an der Breite: Splitter, Fenster, Drehung, Tastatur. Beim
|
||||
Ziehen am Splitter kämen sonst je Bild mehrere Messungen — einmal je Bild
|
||||
genügt. */
|
||||
let lineNoPending = false;
|
||||
if(window.ResizeObserver) new ResizeObserver(() => {
|
||||
if(lineNoPending) return;
|
||||
lineNoPending = true;
|
||||
requestAnimationFrame(() => { lineNoPending = false; renderLineNos(); });
|
||||
}).observe(src);
|
||||
|
||||
/* Nur scrollen, wenn die Zeile nicht ohnehin bequem sichtbar ist. */
|
||||
function scrollEditorToOffset(offset){
|
||||
const top = offsetTopInEditor(offset), h = src.clientHeight;
|
||||
if(top < src.scrollTop + 8 || top > src.scrollTop + h - 28){
|
||||
src.scrollTop = Math.max(0, top - h / 2);
|
||||
/* Das `scroll`-Ereignis kommt erst im nächsten Bild. Wer selbst scrollt,
|
||||
zieht die Zeilennummern deshalb gleich mit — sonst stünden sie bis dahin
|
||||
um eine ganze Bildhöhe daneben. */
|
||||
syncLineNoScroll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,11 +726,9 @@ out.addEventListener('touchend', e => {
|
||||
out.addEventListener('touchcancel', disarmPress);
|
||||
out.addEventListener('contextmenu', e => { if(pressTimer || armedEl) e.preventDefault(); });
|
||||
|
||||
/* Text -> Diagramm: Knoten der Cursor-Zeile hervorheben. `caretLine` bleibt
|
||||
null, bis der Cursor das erste Mal bewegt wurde — sonst wäre nach dem Laden
|
||||
ohne Zutun die Wurzel markiert. */
|
||||
let caretLine = null, currentNodeEl = null;
|
||||
/* Text -> Diagramm: Knoten der Cursor-Zeile hervorheben (`caretLine` steht oben). */
|
||||
function highlightCurrentNode(scroll){
|
||||
markCurrentLineNo(); /* die eine Stelle, an der die Cursor-Zeile neu gesetzt wird */
|
||||
if(currentNodeEl) currentNodeEl.classList.remove('current');
|
||||
currentNodeEl = caretLine == null
|
||||
? null
|
||||
@@ -637,14 +738,41 @@ function highlightCurrentNode(scroll){
|
||||
/* Nur beim Zeilenwechsel scrollen, sonst ruckelte das Diagramm beim Tippen. */
|
||||
if(scroll) currentNodeEl.scrollIntoView({block:'nearest', inline:'nearest', behavior:'smooth'});
|
||||
}
|
||||
function caretLineOf(){
|
||||
return src.value.slice(0, src.selectionStart).split('\n').length;
|
||||
}
|
||||
function syncCaret(){
|
||||
const line = src.value.slice(0, src.selectionStart).split('\n').length;
|
||||
const line = caretLineOf();
|
||||
const moved = line !== caretLine;
|
||||
caretLine = line;
|
||||
highlightCurrentNode(moved);
|
||||
}
|
||||
for(const ev of ['click','keyup','input','focus']) src.addEventListener(ev, syncCaret);
|
||||
|
||||
/* Gegenstück zum Alt+Klick am Knoten (D25, Nachtrag): Alt+Klick im Textfeld —
|
||||
Tastatur Alt+Enter — holt den Knoten der Cursor-Zeile in die **Mitte** des
|
||||
Diagramms und gibt ihm den Fokus. Der gewöhnliche Klick markiert ihn zwar
|
||||
ohnehin, scrollt aber absichtlich nur `nearest` und nur beim Zeilenwechsel
|
||||
(sonst ruckelte das Diagramm beim Tippen) — genau das reicht nicht, wenn man
|
||||
den Knoten wirklich **sehen** will. Derselbe Modifier in beide Richtungen:
|
||||
eine Geste, zwei Richtungen. */
|
||||
function focusNodeOfCaret(){
|
||||
const line = caretLineOf();
|
||||
const el = out.querySelector('.node[data-line="' + line + '"]');
|
||||
if(!el) return; /* Kommentar, Leerzeile, ausgeblendet Verworfenes */
|
||||
caretLine = line;
|
||||
highlightCurrentNode(false);
|
||||
/* Erst den Fokus (ohne eigenes Scrollen), dann bewusst zentrieren. */
|
||||
el.focus({preventScroll: true});
|
||||
el.scrollIntoView({block:'center', inline:'center', behavior:'smooth'});
|
||||
}
|
||||
src.addEventListener('click', e => { if(e.altKey) focusNodeOfCaret(); });
|
||||
src.addEventListener('keydown', e => {
|
||||
if(e.key !== 'Enter' || !e.altKey) return;
|
||||
e.preventDefault(); /* sonst bekäme der Text einen Umbruch */
|
||||
focusNodeOfCaret();
|
||||
});
|
||||
|
||||
/* Alt-Modus sichtbar machen: solange Alt gedrückt ist, zeigt jeder Knoten den
|
||||
Sprung-Cursor und der Knoten unter dem Zeiger einen Petrol-Ring. Das ist die
|
||||
einzige Rückmeldung, die auch auf verlinkten Knoten funktioniert (dort gehört
|
||||
@@ -976,7 +1104,7 @@ const I18N = {
|
||||
hint_break:"Ab (M) gilt: weiter untergliedern — fehlt die Untergliederung, erscheint ein Platzhalter im Diagramm.",
|
||||
hint_comment:"Kommentare mit %% — als ganze Zeile oder am Zeilenende.",
|
||||
hint_people:"Personen mit @name — erscheinen unten rechts am Knoten.",
|
||||
hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile."
|
||||
hint_jump:"Alt+Klick auf einen Knoten (mobil: langer Druck) springt zur zugehörigen Textzeile; Alt+Klick im Text holt den Knoten ins Bild."
|
||||
},
|
||||
en: {
|
||||
subtitle:"Werkbaum – Work Breakdown Structure / Lean Pathfinding · Project structure editor (also feature-tree & requirements)",
|
||||
@@ -1040,7 +1168,7 @@ const I18N = {
|
||||
hint_break:"From (M) on: break it down further — if the breakdown is missing, a placeholder appears in the diagram.",
|
||||
hint_comment:"Comments with %% — whole line or at the end of a line.",
|
||||
hint_people:"People with @name — shown at the bottom-right of the node.",
|
||||
hint_jump:"Alt+click a node (long press on touch) jumps to its line in the text."
|
||||
hint_jump:"Alt+click a node (long press on touch) jumps to its line in the text; Alt+click in the text brings the node into view."
|
||||
},
|
||||
es: {
|
||||
subtitle:"Werkbaum – EDT / Lean Pathfinding · Editor de estructura de proyectos (también árboles de características y requisitos)",
|
||||
@@ -1104,7 +1232,7 @@ const I18N = {
|
||||
hint_break:"A partir de (M): sigue desglosando — si falta el desglose, aparece un marcador de posición en el diagrama.",
|
||||
hint_comment:"Comentarios con %% — línea completa o al final de la línea.",
|
||||
hint_people:"Personas con @nombre — aparecen abajo a la derecha del nodo.",
|
||||
hint_jump:"Alt+clic en un nodo (pulsación larga en táctil) salta a su línea en el texto."
|
||||
hint_jump:"Alt+clic en un nodo (pulsación larga en táctil) salta a su línea en el texto; Alt+clic en el texto trae el nodo a la vista."
|
||||
},
|
||||
fr: {
|
||||
subtitle:"Werkbaum – WBS / Lean Pathfinding · Éditeur de structure de projet (aussi pour arbres de fonctionnalités et d'exigences)",
|
||||
@@ -1168,7 +1296,7 @@ const I18N = {
|
||||
hint_break:"À partir de (M) : décomposer davantage — si la décomposition manque, un espace réservé apparaît dans le diagramme.",
|
||||
hint_comment:"Commentaires avec %% — ligne entière ou en fin de ligne.",
|
||||
hint_people:"Personnes avec @nom — affichées en bas à droite du nœud.",
|
||||
hint_jump:"Alt+clic sur un nœud (appui long sur tactile) saute à sa ligne dans le texte."
|
||||
hint_jump:"Alt+clic sur un nœud (appui long sur tactile) saute à sa ligne dans le texte ; Alt+clic dans le texte amène le nœud à l’écran."
|
||||
},
|
||||
pl: {
|
||||
subtitle:"Werkbaum – WBS / Lean Pathfinding · Edytor struktury projektów (również dla drzew funkcji i wymagań)",
|
||||
@@ -1232,7 +1360,7 @@ const I18N = {
|
||||
hint_break:"Od (M): dziel dalej — gdy brakuje podziału, w diagramie pojawia się symbol zastępczy.",
|
||||
hint_comment:"Komentarze z %% — cały wiersz lub na końcu wiersza.",
|
||||
hint_people:"Osoby z @nazwa — pokazywane w prawym dolnym rogu węzła.",
|
||||
hint_jump:"Alt+kliknięcie węzła (długie naciśnięcie na dotyku) przechodzi do jego wiersza w tekście."
|
||||
hint_jump:"Alt+kliknięcie węzła (długie naciśnięcie na dotyku) przechodzi do jego wiersza w tekście; Alt+kliknięcie w tekście pokazuje węzeł na diagramie."
|
||||
},
|
||||
ru: {
|
||||
subtitle:"Werkbaum – СДР / Lean Pathfinding · Редактор структуры проектов (также для деревьев функций и требований)",
|
||||
@@ -1296,7 +1424,7 @@ const I18N = {
|
||||
hint_break:"С (M): дробите дальше — если декомпозиции нет, в диаграмме появляется заполнитель.",
|
||||
hint_comment:"Комментарии через %% — вся строка или в конце строки.",
|
||||
hint_people:"Люди через @имя — показываются справа внизу узла.",
|
||||
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте."
|
||||
hint_jump:"Alt+клик по узлу (долгое нажатие на сенсоре) переходит к его строке в тексте; Alt+клик в тексте показывает узел на диаграмме."
|
||||
},
|
||||
hi: {
|
||||
subtitle:"Werkbaum – WBS / Lean Pathfinding · परियोजना संरचना संपादक (फ़ीचर और रिक्वायरमेंट ट्री के लिए भी)",
|
||||
@@ -1360,7 +1488,7 @@ const I18N = {
|
||||
hint_break:"(M) से आगे: और विभाजित करें — विभाजन न होने पर आरेख में प्लेसहोल्डर दिखता है।",
|
||||
hint_comment:"%% से टिप्पणियाँ — पूरी पंक्ति या पंक्ति के अंत में।",
|
||||
hint_people:"@नाम से व्यक्ति — नोड के नीचे-दाएँ दिखते हैं।",
|
||||
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है।"
|
||||
hint_jump:"किसी नोड पर Alt+क्लिक (टच पर लंबा दबाव) टेक्स्ट में उसकी पंक्ति पर ले जाता है; टेक्स्ट में Alt+क्लिक उस नोड को आरेख में दिखाता है।"
|
||||
},
|
||||
zh: {
|
||||
subtitle:"Werkbaum – WBS / Lean Pathfinding · 项目结构编辑器(也支持功能树和需求树)",
|
||||
@@ -1424,7 +1552,7 @@ const I18N = {
|
||||
hint_break:"从 (M) 起:继续细分——若缺少细分,图表中会出现占位符。",
|
||||
hint_comment:"用 %% 注释——整行或行尾。",
|
||||
hint_people:"用 @姓名 表示人员——显示在节点右下角。",
|
||||
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行。"
|
||||
hint_jump:"Alt+点击节点(触摸屏为长按)可跳转到文本中对应的行;在文本中 Alt+点击则把该节点带入视野。"
|
||||
},
|
||||
ja: {
|
||||
subtitle:"Werkbaum – WBS / Lean Pathfinding · プロジェクト構造エディター(フィーチャーツリーと要件ツリーにも対応)",
|
||||
@@ -1488,7 +1616,7 @@ const I18N = {
|
||||
hint_break:"(M) 以上:さらに分解 — 分解がないと図にプレースホルダーが表示されます。",
|
||||
hint_comment:"%% でコメント — 行全体または行末。",
|
||||
hint_people:"@名前 で担当者 — ノードの右下に表示されます。",
|
||||
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。"
|
||||
hint_jump:"ノードを Alt+クリック(タッチでは長押し)すると、テキストの該当行へ移動します。テキスト内で Alt+クリックすると、そのノードが図の中央に表示されます。"
|
||||
}
|
||||
};
|
||||
let lang = 'de';
|
||||
@@ -2203,6 +2331,7 @@ function updatePadLink(){
|
||||
const isPad = !!(d && padSource && d.id === padSource.id);
|
||||
src.readOnly = isPad;
|
||||
src.classList.toggle('readonly', isPad);
|
||||
if(srcWrap) srcWrap.classList.toggle('readonly', isPad); /* Streifen mit abtönen */
|
||||
if(isPad) src.title = t('padReadonly'); else src.removeAttribute('title');
|
||||
if(padLink){
|
||||
padLink.hidden = !isPad;
|
||||
|
||||
+29
-3
@@ -217,13 +217,17 @@
|
||||
.src-area{display:flex;align-items:stretch;flex:1 1 auto;min-width:0;min-height:0}
|
||||
.padframe{flex:1 1 auto;min-width:0;min-height:0;border:0;background:var(--card)}
|
||||
.padframe[hidden]{display:none}
|
||||
.src-area.pv-both>textarea{
|
||||
/* Zeilennummern + Textfeld sind eine Spalte (`.src-wrap`). Der Pad-Splitter
|
||||
teilt gegen diese Spalte — nicht gegen das nackte Textfeld, sonst stünde der
|
||||
Zahlenstreifen außerhalb der geteilten Breite. */
|
||||
.src-wrap{display:flex;align-items:stretch;flex:1 1 auto;min-width:0;min-height:0}
|
||||
.src-area.pv-both>.src-wrap{
|
||||
flex:0 0 var(--pcol,300px);max-width:85%;
|
||||
border-left:1px dashed rgba(36,52,71,.18);
|
||||
}
|
||||
.src-area.pv-pad>textarea{display:none}
|
||||
.src-area.pv-pad>.src-wrap{display:none}
|
||||
.side .src-area,body.mobile .src-area{flex-direction:column}
|
||||
.side .src-area.pv-both>textarea,body.mobile .src-area.pv-both>textarea{
|
||||
.side .src-area.pv-both>.src-wrap,body.mobile .src-area.pv-both>.src-wrap{
|
||||
flex:0 0 var(--prow,180px);max-width:none;max-height:85%;
|
||||
border-left:0;border-top:1px dashed rgba(36,52,71,.18);
|
||||
}
|
||||
@@ -459,6 +463,28 @@
|
||||
erscheint — die Schrift bleibt aber Tinte: hier wird gelesen, der Plantext
|
||||
ist der Hauptinhalt und kein ausgegrautes Formularfeld. */
|
||||
textarea.readonly{background:#F1F4F7;cursor:default}
|
||||
/* Zeilennummern (D33). Die Warnungen nennen Zeilennummern — ohne Streifen
|
||||
muss man sie im Textfeld abzählen. Weich umbrochene Zeilen belegen mehrere
|
||||
Bildzeilen, deshalb steht jede Zahl auf einer **gemessenen** Höhe
|
||||
(`renderLineNos()` in app.js), nicht auf „Zeilenhöhe × n". Typografie muss
|
||||
mit dem Textfeld übereinstimmen, sonst wandern die Zahlen weg. */
|
||||
.lineno{
|
||||
position:relative;overflow:hidden;flex:0 0 auto;
|
||||
background:var(--card);border-right:1px solid rgba(36,52,71,.10);
|
||||
font-family:'IBM Plex Mono',monospace;font-size:.85rem;line-height:1.7;
|
||||
color:var(--muted);
|
||||
-webkit-user-select:none;user-select:none;
|
||||
}
|
||||
.src-wrap.readonly .lineno{background:#F1F4F7}
|
||||
.lineno-inner{position:absolute;top:0;left:0;right:0}
|
||||
.lineno-inner span{
|
||||
position:absolute;right:6px;
|
||||
opacity:.5;font-variant-numeric:tabular-nums;
|
||||
}
|
||||
/* Cursor-Zeile und Zeilen mit Warnung heben sich ab — genau die beiden Fälle,
|
||||
für die man im Streifen überhaupt nachsieht. */
|
||||
.lineno-inner span.cur{opacity:1;color:var(--ink);font-weight:600}
|
||||
.lineno-inner span.warn{opacity:1;color:var(--warn);font-weight:600}
|
||||
.hint{
|
||||
padding:12px 16px;border-top:1px dashed rgba(36,52,71,.18);
|
||||
font-size:.8rem;color:var(--muted);line-height:1.7;
|
||||
|
||||
Reference in New Issue
Block a user