frontend: Sprung zwischen Diagramm und Text (beide Richtungen)
Knoten und Notationszeile sind nun verknüpft. Der Parser hängte die
Zeilennummer ohnehin schon an jeden Knoten — sie wandert jetzt als data-line
ins Markup.
- Diagramm -> Text: Alt+Klick markiert die ganze Zeile im Editor (Fokus,
in Sicht gescrollt); Tastatur Alt+Enter, Touch langer Druck (500 ms,
Wischen bricht ab). Ein zugeklapptes Editor-Panel wird zuerst geöffnet.
- Text -> Diagramm: der Knoten der Cursor-Zeile bekommt weißen Halo +
Tinte-Ring und wird beim Zeilenwechsel ins Bild gescrollt.
- Alt statt einfachem Klick, weil ein Knoten mit URL als <a> den ganzen
Kasten belegt (SPEC §6 bleibt unverändert). Der Handler ruft
preventDefault() — sonst lädt Alt+Klick auf einen Link das Ziel herunter.
Neuer Tooltip-Hinweis an jedem Knoten (i18n `jumpHint`, 9 Sprachen).
- Gescrollt wird über einen Spiegel-div, nicht über Zeilenhöhe x n: lange
Zeilen brechen weich um, die naive Rechnung lag im Test bei 60 Zeilen um
bis zu 525 px daneben.
Beim Verifizieren gefunden und behoben: `ul.or .node{box-shadow:none}` ist
spezifischer als `.node.current` und schluckte den Ring überall unterhalb
einer any-of-Gruppe — die Regel braucht den #out-Präfix.
Verifiziert im Browser (Alt+Klick auf <a>- und div-Knoten, Klick aufs
Größen-Badge, Klick/Enter ohne Alt unverändert, Alt+Enter, kurzer/langer
Druck und Wischen auf Mobil, Panel-Aufklappen aus dem minimierten Zustand,
Zeile nach Sprung immer im sichtbaren Band, Kommentar-/verworfene Zeilen
heben nichts hervor). Vitest 37/37 (3 neue data-line-Tests, Snapshots
aktualisiert). Dokumentiert als D25 + SPEC §9 + frontend/CLAUDE.md + README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
cdf843e2e3
commit
1f6988b047
@@ -38,6 +38,18 @@ Den [gehosteten Editor](https://werkbaum.javagil.de) öffnen — links
|
|||||||
Text bearbeiten, rechts entsteht das Diagramm live. Toggles: transponierte
|
Text bearbeiten, rechts entsteht das Diagramm live. Toggles: transponierte
|
||||||
(schmale) Darstellung, verworfene Elemente einblenden.
|
(schmale) Darstellung, verworfene Elemente einblenden.
|
||||||
|
|
||||||
|
### Zwischen Diagramm und Text springen
|
||||||
|
|
||||||
|
Diagramm und Editor sind in beide Richtungen verknüpft:
|
||||||
|
|
||||||
|
- **Alt+Klick auf einen Knoten** (Tastatur: **Alt+Enter**, Touch: **langer
|
||||||
|
Druck**) markiert seine Zeile im Texteditor — ein zugeklapptes Editor-Panel
|
||||||
|
öffnet sich dabei zuerst.
|
||||||
|
- **Cursor im Text bewegen** hebt den zugehörigen Knoten im Diagramm hervor.
|
||||||
|
|
||||||
|
Der einfache Klick behält seine bisherige Bedeutung: Ein Knoten mit URL öffnet
|
||||||
|
sie weiterhin.
|
||||||
|
|
||||||
### Diagramm aus einer URL laden
|
### Diagramm aus einer URL laden
|
||||||
|
|
||||||
Der Editor kann seinen Notationstext über den Query-Parameter `sourceUrl` aus
|
Der Editor kann seinen Notationstext über den Query-Parameter `sourceUrl` aus
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ Open the [hosted editor](https://werkbaum.javagil.de) — edit text on
|
|||||||
the left, the diagram is built live on the right. Toggles: transposed (narrow)
|
the left, the diagram is built live on the right. Toggles: transposed (narrow)
|
||||||
layout, show discarded elements.
|
layout, show discarded elements.
|
||||||
|
|
||||||
|
### Jumping between diagram and text
|
||||||
|
|
||||||
|
Diagram and editor are linked in both directions:
|
||||||
|
|
||||||
|
- **Alt+click a node** (keyboard: **Alt+Enter**, touch: **long press**) selects
|
||||||
|
its line in the text editor — collapsed editor panel opens first.
|
||||||
|
- **Moving the cursor in the text** highlights the matching node in the diagram.
|
||||||
|
|
||||||
|
A plain click keeps its old meaning: a node carrying a URL still opens it.
|
||||||
|
|
||||||
### Loading a diagram from a URL
|
### Loading a diagram from a URL
|
||||||
|
|
||||||
The editor can pull its notation text from an external text file via the
|
The editor can pull its notation text from an external text file via the
|
||||||
|
|||||||
@@ -465,3 +465,58 @@ Dokumenten zeigen lässt: jeder `?sourceUrl=`-Link legt ein eigenes Dokument an
|
|||||||
`example-werkbaum.werkbaum` beschreibt Werkbaum selbst (Bestand + mögliche
|
`example-werkbaum.werkbaum` beschreibt Werkbaum selbst (Bestand + mögliche
|
||||||
Weiterentwicklung, destilliert aus ROADMAP/TASKS/DECISIONS) — zugleich
|
Weiterentwicklung, destilliert aus ROADMAP/TASKS/DECISIONS) — zugleich
|
||||||
Beispiel und lebende Projektübersicht; bei größeren Änderungen mitpflegen.
|
Beispiel und lebende Projektübersicht; bei größeren Änderungen mitpflegen.
|
||||||
|
|
||||||
|
## D25 — Sprung Diagramm ↔ Text per Alt+Klick, Gegenrichtung per Cursor-Zeile
|
||||||
|
Ein Knoten im Diagramm und „seine" Zeile im Texteditor sind nun verknüpft. Der
|
||||||
|
Parser hängte die Zeilennummer ohnehin schon an jeden Knoten (`{…, line}`,
|
||||||
|
bislang nur für Warnungen genutzt); der Renderer gibt sie als `data-line` aus.
|
||||||
|
|
||||||
|
**Alt+Klick statt einfachem Klick.** Ein Knoten mit URL ist als `<a>` gerendert
|
||||||
|
und belegt mit dem einfachen Klick bereits den ganzen Kasten (SPEC §6, D6).
|
||||||
|
Erwogen und verworfen:
|
||||||
|
|
||||||
|
- **↗ wird der Link, Knotenfläche springt** — klarste Regel, alles per
|
||||||
|
einfachem Klick; ändert aber SPEC §6 und schrumpft die Link-Trefferfläche auf
|
||||||
|
ein Symbol.
|
||||||
|
- **Klick springt, Strg-Klick öffnet** — behält die große Trefferfläche, macht
|
||||||
|
das Öffnen aber zur unsichtbaren Geste und ändert §6 ebenfalls.
|
||||||
|
- **Gewählt: Alt+Klick springt** — das Verlinkungs-Verhalten bleibt exakt wie
|
||||||
|
bisher, SPEC §6 unverändert. Preis ist die geringe Auffindbarkeit; dagegen
|
||||||
|
steht der **Tooltip an jedem Knoten** („Alt+Klick: zur Zeile im Text",
|
||||||
|
i18n-Key `jumpHint`, in allen 9 Sprachen), der bisher nur den Statusnamen
|
||||||
|
zeigte.
|
||||||
|
|
||||||
|
Wichtig: Der Klick-Handler **muss `preventDefault()`** rufen — Alt+Klick auf
|
||||||
|
einen Link lädt sonst in Chrome/Firefox das Ziel herunter. Tastatur-Pendant ist
|
||||||
|
**Alt+Enter** am fokussierten Knoten (Enter allein bleibt dem Link). Auf Touch
|
||||||
|
gibt es kein Alt: dort **langer Druck** (500 ms), Wischen bricht ab; der
|
||||||
|
folgende Klick und das Kontextmenü werden unterdrückt (`-webkit-touch-callout`
|
||||||
|
aus), sonst öffnete ein Link-Knoten zusätzlich seine URL.
|
||||||
|
|
||||||
|
**Ganze Zeile markieren statt nur Cursor setzen.** Ein `<textarea>` kennt keine
|
||||||
|
Zeilen-Hervorhebung (kein Rich-Text-Markup); die native Auswahl ist die einzige
|
||||||
|
Betonung, die es gibt — und sie verschwindet beim ersten Tippen von selbst.
|
||||||
|
|
||||||
|
**Scrollen über einen Spiegel-`div`, nicht über Zeilenhöhe × n.** Lange Zeilen
|
||||||
|
brechen weich um und belegen mehrere Bildzeilen; die naive Rechnung lag im Test
|
||||||
|
bei 60 Zeilen um bis zu 525 px daneben. Gemessen wird deshalb an einem
|
||||||
|
unsichtbaren `div` mit gleicher Typografie und Breite plus Marker-Span
|
||||||
|
(`offsetTop`). Gescrollt wird nur, wenn die Zeile nicht ohnehin sichtbar ist.
|
||||||
|
|
||||||
|
**Gegenrichtung (Cursor-Zeile → Knoten)** ist bei großen Bäumen die nützlichere
|
||||||
|
Hälfte: `example-werkbaum.werkbaum` hat 75 sichtbare Knoten, ohne Markierung
|
||||||
|
verliert man beim Tippen die Orientierung. Der Knoten der Cursor-Zeile bekommt
|
||||||
|
die Klasse `current`; ins Bild gescrollt wird **nur beim Zeilenwechsel**, sonst
|
||||||
|
ruckelte das Diagramm bei jedem Tastendruck. Vor der ersten Cursor-Bewegung ist
|
||||||
|
`caretLine === null` — sonst wäre direkt nach dem Laden ungefragt die Wurzel
|
||||||
|
markiert.
|
||||||
|
|
||||||
|
**Darstellung: weißer Halo + Ring in Tinte** (`box-shadow`, kein Rahmen). Hebt
|
||||||
|
sich von allen Pastell-Status *und* vom dunklen Wurzelknoten ab, ist vom
|
||||||
|
petrolfarbenen Fokusring (`:focus-visible`) unterscheidbar und rührt die
|
||||||
|
Knoten-Ecken nicht an (die sind laut D18 schon dicht). Die Regel braucht den
|
||||||
|
`#out`-Präfix: `ul.or .node{box-shadow:none}` ist spezifischer als
|
||||||
|
`.node.current` und schluckte den Ring sonst überall unterhalb einer
|
||||||
|
any-of-Gruppe (im Test zuerst passiert). Rein visuelle Editierhilfe — im Druck
|
||||||
|
abgeschaltet; im Grafikexport erscheint sie ohnehin nicht, weil `diagramToSvg()`
|
||||||
|
nur Hintergrund, Rahmen, Farbe und Textdekoration ausliest, nie `box-shadow`.
|
||||||
|
|||||||
@@ -225,6 +225,24 @@ Scheitert das Laden (häufigster Fall: das Ziel sendet keinen
|
|||||||
`Access-Control-Allow-Origin`-Header, außerdem 404/Netzfehler), bleibt der
|
`Access-Control-Allow-Origin`-Header, außerdem 404/Netzfehler), bleibt der
|
||||||
bisherige Stand stehen und es erscheint eine **Warnung**. Siehe D23.
|
bisherige Stand stehen und es erscheint eine **Warnung**. Siehe D23.
|
||||||
|
|
||||||
|
### Sprung zwischen Diagramm und Text
|
||||||
|
Jeder Knoten kennt seine Zeilennummer im Notationstext; beide Richtungen sind
|
||||||
|
verknüpft (siehe D25):
|
||||||
|
|
||||||
|
- **Diagramm → Text:** **Alt+Klick** auf einen Knoten markiert die zugehörige
|
||||||
|
Zeile im Texteditor (ganze Zeile ausgewählt, in Sicht gescrollt, Fokus im
|
||||||
|
Textfeld). Tastatur: **Alt+Enter** am fokussierten Knoten. Auf Touch-Geräten:
|
||||||
|
**langer Druck** (≥ 500 ms); ein Wischen bricht ab. Ist das Editor-Panel
|
||||||
|
zugeklappt, öffnet der Sprung es zuerst.
|
||||||
|
- **Text → Diagramm:** Der Knoten der **Cursor-Zeile** wird im Diagramm
|
||||||
|
hervorgehoben (weißer Halo + Ring in Tinte) und beim Zeilenwechsel ins Bild
|
||||||
|
gescrollt. Zeilen ohne Knoten (Kommentar, Leerzeile, ausgeblendetes
|
||||||
|
verworfenes Element) heben nichts hervor.
|
||||||
|
- 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.
|
||||||
|
- Die Hervorhebung ist eine reine Editierhilfe: nicht im Grafikexport, nicht im
|
||||||
|
Druck.
|
||||||
|
|
||||||
### Grafikexport des Diagramms
|
### Grafikexport des Diagramms
|
||||||
Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
|
Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
|
||||||
+ Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen:
|
+ Text, keine externen Ressourcen) nachgezeichnet. Zwei Icon-Schaltflächen:
|
||||||
|
|||||||
@@ -125,6 +125,17 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
Endung `.werkbaum` ist reine Konvention (D24, SPEC §12). Beispieldateien zum
|
Endung `.werkbaum` ist reine Konvention (D24, SPEC §12). Beispieldateien zum
|
||||||
Ausprobieren: `docs/examples/*.werkbaum` (nacheinander geöffnet ergeben sie
|
Ausprobieren: `docs/examples/*.werkbaum` (nacheinander geöffnet ergeben sie
|
||||||
mehrere Dokumente im Wähler).
|
mehrere Dokumente im Wähler).
|
||||||
|
- Sprung Diagramm ↔ Text (D25): `render.js` schreibt die Parser-Zeilennummer als
|
||||||
|
`data-line` an jeden Knoten (Geister-Knoten bekommen keine). `jumpToLine()` in
|
||||||
|
`app.js` klappt bei Bedarf das Editor-Panel auf (`revealEditor()`), markiert die
|
||||||
|
**ganze Zeile** und scrollt über einen Spiegel-`div` (`offsetTopInEditor()`) —
|
||||||
|
Zeilenhöhe × n scheitert an weichen Umbrüchen. Ausgelöst per Alt+Klick,
|
||||||
|
Alt+Enter und langem Druck; der Klick-Handler **muss `preventDefault()`** rufen,
|
||||||
|
sonst lädt Alt+Klick auf einen Link-Knoten das Ziel herunter. Gegenrichtung:
|
||||||
|
`syncCaret()` setzt die Klasse `current` auf den Knoten der Cursor-Zeile;
|
||||||
|
`render()` stellt sie nach jedem Neubau wieder her (ohne zu scrollen). Die
|
||||||
|
CSS-Regel braucht den `#out`-Präfix (`ul.or .node{box-shadow:none}` ist
|
||||||
|
spezifischer).
|
||||||
- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt
|
- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt
|
||||||
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
|
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
|
||||||
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
|
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
|
||||||
|
|||||||
@@ -66,6 +66,9 @@ function render(){
|
|||||||
warnings = warnings.slice().sort((a, b) => (a.line || 0) - (b.line || 0));
|
warnings = warnings.slice().sort((a, b) => (a.line || 0) - (b.line || 0));
|
||||||
warnBox.innerHTML = warnings.map(w => `<div>⚠ ${formatWarning(w, t)}</div>`).join('');
|
warnBox.innerHTML = warnings.map(w => `<div>⚠ ${formatWarning(w, t)}</div>`).join('');
|
||||||
drawCheapPath();
|
drawCheapPath();
|
||||||
|
/* Der Baum ist neu gebaut — die Markierung der Cursor-Zeile neu setzen (D25).
|
||||||
|
Ohne Scrollen: beim Tippen soll das Diagramm stehen bleiben. */
|
||||||
|
highlightCurrentNode(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Günstigster-Pfad-Linie ----------
|
/* ---------- Günstigster-Pfad-Linie ----------
|
||||||
@@ -305,6 +308,152 @@ src.addEventListener('keydown', e => {
|
|||||||
src.addEventListener('input', render);
|
src.addEventListener('input', render);
|
||||||
src.addEventListener('input', saveSrc);
|
src.addEventListener('input', saveSrc);
|
||||||
|
|
||||||
|
/* ---------- Sprung zwischen Diagramm und Text (D25) ----------
|
||||||
|
Jeder Knoten trägt seine Zeilennummer als `data-line` (render.js).
|
||||||
|
Diagramm -> Text: Alt+Klick (bzw. Alt+Enter am fokussierten Knoten, mobil
|
||||||
|
langer Druck) markiert die Zeile im Textfeld. Text -> Diagramm: die Zeile
|
||||||
|
des Cursors hebt den zugehörigen Knoten hervor. Alt statt einfachem Klick,
|
||||||
|
weil ein Knoten mit URL als <a> den ganzen Kasten belegt (SPEC §6). */
|
||||||
|
|
||||||
|
/* Zeichenbereich einer 1-basierten Zeile; null, wenn es sie nicht (mehr) gibt. */
|
||||||
|
function lineRange(line){
|
||||||
|
const lines = src.value.split('\n');
|
||||||
|
if(!(line >= 1 && line <= lines.length)) return null;
|
||||||
|
let start = 0;
|
||||||
|
for(let i = 0; i < line - 1; i++) start += lines[i].length + 1;
|
||||||
|
return {start, end: start + lines[line - 1].length};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertikale Position eines Zeichenoffsets im Textfeld. Zeilenhöhe × n scheitert
|
||||||
|
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){
|
||||||
|
if(!mirrorEl){
|
||||||
|
mirrorEl = document.createElement('div');
|
||||||
|
mirrorEl.setAttribute('aria-hidden', 'true');
|
||||||
|
mirrorEl.style.cssText = 'position:absolute;visibility:hidden;top:0;left:-9999px;' +
|
||||||
|
'white-space:pre-wrap;overflow-wrap:break-word;';
|
||||||
|
document.body.appendChild(mirrorEl);
|
||||||
|
}
|
||||||
|
const cs = getComputedStyle(src);
|
||||||
|
for(const p of ['fontFamily','fontSize','fontWeight','lineHeight','letterSpacing',
|
||||||
|
'paddingTop','paddingLeft','paddingRight','borderTopWidth','tabSize']){
|
||||||
|
mirrorEl.style[p] = cs[p];
|
||||||
|
}
|
||||||
|
mirrorEl.style.width = src.clientWidth + 'px';
|
||||||
|
mirrorEl.textContent = src.value.slice(0, offset);
|
||||||
|
const marker = document.createElement('span');
|
||||||
|
marker.textContent = '';
|
||||||
|
mirrorEl.appendChild(marker);
|
||||||
|
const top = marker.offsetTop;
|
||||||
|
mirrorEl.textContent = '';
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ist das Editor-Panel zugeklappt, muss der Sprung es erst öffnen. */
|
||||||
|
function revealEditor(){
|
||||||
|
if(isMobile()){
|
||||||
|
const raw = app.style.getPropertyValue('--drow');
|
||||||
|
const collapsed = raw ? parseFloat(raw) > mobileMaxDrow() - 40 : splitState === 'b';
|
||||||
|
if(collapsed) setMobileDrow(app.getBoundingClientRect().height * 0.45, true);
|
||||||
|
} else if(splitState === 'b'){
|
||||||
|
splitState = 'normal';
|
||||||
|
applySplit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Diagramm -> Text: ganze Zeile markieren (die native Auswahl ist die einzige
|
||||||
|
Hervorhebung, die ein <textarea> kennt — und sie verschwindet beim Tippen). */
|
||||||
|
function jumpToLine(line){
|
||||||
|
const r = lineRange(line);
|
||||||
|
if(!r) return;
|
||||||
|
revealEditor();
|
||||||
|
src.focus({preventScroll: true});
|
||||||
|
src.setSelectionRange(r.start, r.end);
|
||||||
|
scrollEditorToOffset(r.start);
|
||||||
|
caretLine = line;
|
||||||
|
highlightCurrentNode(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeFromEvent(e){
|
||||||
|
const el = e.target && e.target.closest ? e.target.closest('.node[data-line]') : null;
|
||||||
|
return el && out.contains(el) ? el : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.addEventListener('click', e => {
|
||||||
|
if(!e.altKey) return;
|
||||||
|
const el = nodeFromEvent(e);
|
||||||
|
if(!el) return;
|
||||||
|
/* Ohne preventDefault lädt der Browser bei Alt+Klick auf einen Link das Ziel
|
||||||
|
herunter (Chrome/Firefox) — das ist hier ausdrücklich nicht gemeint. */
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
jumpToLine(+el.dataset.line);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Tastatur: Alt+Enter am fokussierten Knoten. Enter allein bleibt dem Link. */
|
||||||
|
out.addEventListener('keydown', e => {
|
||||||
|
if(e.key !== 'Enter' || !e.altKey) return;
|
||||||
|
const el = nodeFromEvent(e);
|
||||||
|
if(!el) return;
|
||||||
|
e.preventDefault();
|
||||||
|
jumpToLine(+el.dataset.line);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Mobil gibt es kein Alt: langer Druck (500 ms) auf einen Knoten springt.
|
||||||
|
Der folgende Klick wird unterdrückt, sonst öffnete ein Link-Knoten zusätzlich
|
||||||
|
seine URL; das Kontextmenü/Callout ebenso (die Geste ist hier vergeben). */
|
||||||
|
let pressTimer = null, pressFired = false;
|
||||||
|
out.addEventListener('touchstart', e => {
|
||||||
|
const el = nodeFromEvent(e);
|
||||||
|
pressFired = false;
|
||||||
|
if(!el) return;
|
||||||
|
pressTimer = setTimeout(() => {
|
||||||
|
pressTimer = null;
|
||||||
|
pressFired = true;
|
||||||
|
jumpToLine(+el.dataset.line);
|
||||||
|
}, 500);
|
||||||
|
}, {passive: true});
|
||||||
|
function cancelPress(){ if(pressTimer){ clearTimeout(pressTimer); pressTimer = null; } }
|
||||||
|
out.addEventListener('touchmove', cancelPress, {passive: true});
|
||||||
|
out.addEventListener('touchend', e => {
|
||||||
|
if(pressFired) e.preventDefault();
|
||||||
|
cancelPress();
|
||||||
|
}, {passive: false});
|
||||||
|
out.addEventListener('touchcancel', () => { cancelPress(); pressFired = false; });
|
||||||
|
out.addEventListener('contextmenu', e => { if(pressTimer || pressFired) 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;
|
||||||
|
function highlightCurrentNode(scroll){
|
||||||
|
if(currentNodeEl) currentNodeEl.classList.remove('current');
|
||||||
|
currentNodeEl = caretLine == null
|
||||||
|
? null
|
||||||
|
: out.querySelector('.node[data-line="' + caretLine + '"]');
|
||||||
|
if(!currentNodeEl) return;
|
||||||
|
currentNodeEl.classList.add('current');
|
||||||
|
/* Nur beim Zeilenwechsel scrollen, sonst ruckelte das Diagramm beim Tippen. */
|
||||||
|
if(scroll) currentNodeEl.scrollIntoView({block:'nearest', inline:'nearest', behavior:'smooth'});
|
||||||
|
}
|
||||||
|
function syncCaret(){
|
||||||
|
const line = src.value.slice(0, src.selectionStart).split('\n').length;
|
||||||
|
const moved = line !== caretLine;
|
||||||
|
caretLine = line;
|
||||||
|
highlightCurrentNode(moved);
|
||||||
|
}
|
||||||
|
for(const ev of ['click','keyup','input','focus']) src.addEventListener(ev, syncCaret);
|
||||||
|
|
||||||
const app = document.getElementById('app');
|
const app = document.getElementById('app');
|
||||||
function applyLayout(mode){
|
function applyLayout(mode){
|
||||||
out.classList.toggle('vertical', mode === 'vertikal');
|
out.classList.toggle('vertical', mode === 'vertikal');
|
||||||
@@ -569,6 +718,7 @@ const I18N = {
|
|||||||
privacy:"Datenschutz",
|
privacy:"Datenschutz",
|
||||||
legendTooltip:"Legende ein-/ausblenden",
|
legendTooltip:"Legende ein-/ausblenden",
|
||||||
ghostTooltip:"Ab Größe M sollte ein Element weiter untergliedert werden.",
|
ghostTooltip:"Ab Größe M sollte ein Element weiter untergliedert werden.",
|
||||||
|
jumpHint:"Alt+Klick: zur Zeile im Text",
|
||||||
riskTooltip:"High Risk – Aufwand noch unklar.",
|
riskTooltip:"High Risk – Aufwand noch unklar.",
|
||||||
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
|
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
|
||||||
cheapTooltip:"Günstigsten Pfad hervorheben – nicht benötigte Alternativen treten zurück",
|
cheapTooltip:"Günstigsten Pfad hervorheben – nicht benötigte Alternativen treten zurück",
|
||||||
@@ -615,6 +765,7 @@ const I18N = {
|
|||||||
privacy:"Privacy",
|
privacy:"Privacy",
|
||||||
legendTooltip:"Show/hide legend",
|
legendTooltip:"Show/hide legend",
|
||||||
ghostTooltip:"From size M upward, an item should be broken down further.",
|
ghostTooltip:"From size M upward, an item should be broken down further.",
|
||||||
|
jumpHint:"Alt+click: jump to the line in the text",
|
||||||
riskTooltip:"High risk – effort still unclear.",
|
riskTooltip:"High risk – effort still unclear.",
|
||||||
discardedTooltip:"Show/hide discarded nodes and their subtree",
|
discardedTooltip:"Show/hide discarded nodes and their subtree",
|
||||||
cheapTooltip:"Highlight the cheapest path – unneeded alternatives recede",
|
cheapTooltip:"Highlight the cheapest path – unneeded alternatives recede",
|
||||||
@@ -661,6 +812,7 @@ const I18N = {
|
|||||||
privacy:"Privacidad",
|
privacy:"Privacidad",
|
||||||
legendTooltip:"Mostrar u ocultar la leyenda",
|
legendTooltip:"Mostrar u ocultar la leyenda",
|
||||||
ghostTooltip:"A partir de la talla M, un elemento debería desglosarse más.",
|
ghostTooltip:"A partir de la talla M, un elemento debería desglosarse más.",
|
||||||
|
jumpHint:"Alt+clic: ir a la línea en el texto",
|
||||||
riskTooltip:"Alto riesgo – esfuerzo aún incierto.",
|
riskTooltip:"Alto riesgo – esfuerzo aún incierto.",
|
||||||
discardedTooltip:"Mostrar u ocultar los nodos descartados y su subárbol",
|
discardedTooltip:"Mostrar u ocultar los nodos descartados y su subárbol",
|
||||||
cheapTooltip:"Resaltar la ruta más económica: las alternativas no necesarias se atenúan",
|
cheapTooltip:"Resaltar la ruta más económica: las alternativas no necesarias se atenúan",
|
||||||
@@ -707,6 +859,7 @@ const I18N = {
|
|||||||
privacy:"Confidentialité",
|
privacy:"Confidentialité",
|
||||||
legendTooltip:"Afficher/masquer la légende",
|
legendTooltip:"Afficher/masquer la légende",
|
||||||
ghostTooltip:"À partir de la taille M, un élément devrait être décomposé davantage.",
|
ghostTooltip:"À partir de la taille M, un élément devrait être décomposé davantage.",
|
||||||
|
jumpHint:"Alt+clic : aller à la ligne dans le texte",
|
||||||
riskTooltip:"Risque élevé – effort encore incertain.",
|
riskTooltip:"Risque élevé – effort encore incertain.",
|
||||||
discardedTooltip:"Afficher/masquer les nœuds abandonnés et leur sous-arbre",
|
discardedTooltip:"Afficher/masquer les nœuds abandonnés et leur sous-arbre",
|
||||||
cheapTooltip:"Mettre en évidence le chemin le moins coûteux – les alternatives inutiles s'estompent",
|
cheapTooltip:"Mettre en évidence le chemin le moins coûteux – les alternatives inutiles s'estompent",
|
||||||
@@ -753,6 +906,7 @@ const I18N = {
|
|||||||
privacy:"Prywatność",
|
privacy:"Prywatność",
|
||||||
legendTooltip:"Pokaż/ukryj legendę",
|
legendTooltip:"Pokaż/ukryj legendę",
|
||||||
ghostTooltip:"Od rozmiaru M element powinien być dalej podzielony.",
|
ghostTooltip:"Od rozmiaru M element powinien być dalej podzielony.",
|
||||||
|
jumpHint:"Alt+kliknięcie: przejdź do wiersza w tekście",
|
||||||
riskTooltip:"Wysokie ryzyko – nakład jeszcze niejasny.",
|
riskTooltip:"Wysokie ryzyko – nakład jeszcze niejasny.",
|
||||||
discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem",
|
discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem",
|
||||||
cheapTooltip:"Wyróżnij najtańszą ścieżkę – niepotrzebne alternatywy są przygaszone",
|
cheapTooltip:"Wyróżnij najtańszą ścieżkę – niepotrzebne alternatywy są przygaszone",
|
||||||
@@ -799,6 +953,7 @@ const I18N = {
|
|||||||
privacy:"Конфиденциальность",
|
privacy:"Конфиденциальность",
|
||||||
legendTooltip:"Показать/скрыть легенду",
|
legendTooltip:"Показать/скрыть легенду",
|
||||||
ghostTooltip:"Начиная с размера M элемент следует далее декомпозировать.",
|
ghostTooltip:"Начиная с размера M элемент следует далее декомпозировать.",
|
||||||
|
jumpHint:"Alt+клик: перейти к строке в тексте",
|
||||||
riskTooltip:"Высокий риск – оценка ещё не ясна.",
|
riskTooltip:"Высокий риск – оценка ещё не ясна.",
|
||||||
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
|
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
|
||||||
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
|
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
|
||||||
@@ -845,6 +1000,7 @@ const I18N = {
|
|||||||
privacy:"गोपनीयता",
|
privacy:"गोपनीयता",
|
||||||
legendTooltip:"लेजेंड दिखाएँ/छिपाएँ",
|
legendTooltip:"लेजेंड दिखाएँ/छिपाएँ",
|
||||||
ghostTooltip:"आकार M से ऊपर किसी तत्व को और अधिक उप-विभाजित करना चाहिए।",
|
ghostTooltip:"आकार M से ऊपर किसी तत्व को और अधिक उप-विभाजित करना चाहिए।",
|
||||||
|
jumpHint:"Alt+क्लिक: टेक्स्ट में उस पंक्ति पर जाएँ",
|
||||||
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
|
riskTooltip:"उच्च जोखिम – प्रयास अभी अस्पष्ट।",
|
||||||
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
|
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
|
||||||
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
|
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
|
||||||
@@ -896,6 +1052,7 @@ const I18N = {
|
|||||||
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
|
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
|
||||||
implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计",
|
implicitSizeTooltip:"未指定尺寸——成本估算时按 M 计",
|
||||||
ghostTooltip:"从 M 号起,元素应进一步细分。",
|
ghostTooltip:"从 M 号起,元素应进一步细分。",
|
||||||
|
jumpHint:"Alt+点击:跳转到文本中的该行",
|
||||||
riskTooltip:"高风险 – 工作量尚不明确。",
|
riskTooltip:"高风险 – 工作量尚不明确。",
|
||||||
editorTitle:"结构(文本)", diagramTitle:"图表",
|
editorTitle:"结构(文本)", diagramTitle:"图表",
|
||||||
docSwitchTooltip:"选择或管理文档", docMenuAria:"文档",
|
docSwitchTooltip:"选择或管理文档", docMenuAria:"文档",
|
||||||
@@ -942,6 +1099,7 @@ const I18N = {
|
|||||||
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
|
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
|
||||||
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う",
|
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため M として扱う",
|
||||||
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
|
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
|
||||||
|
jumpHint:"Alt+クリック:テキストの該当行へ移動",
|
||||||
riskTooltip:"高リスク – 規模はまだ不明。",
|
riskTooltip:"高リスク – 規模はまだ不明。",
|
||||||
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
|
editorTitle:"構造(テキスト)", diagramTitle:"ダイアグラム",
|
||||||
docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント",
|
docSwitchTooltip:"ドキュメントを選択・管理", docMenuAria:"ドキュメント",
|
||||||
|
|||||||
@@ -39,7 +39,13 @@ function nodeHtml(n, extra, opts){
|
|||||||
const need = needsBreakdown(n);
|
const need = needsBreakdown(n);
|
||||||
const cls = ['node', extra || '', n.status ? 'st-' + n.status.key : '']
|
const cls = ['node', extra || '', n.status ? 'st-' + n.status.key : '']
|
||||||
.filter(Boolean).join(' ');
|
.filter(Boolean).join(' ');
|
||||||
const title = n.status ? ` title="${attr(t('st_' + n.status.key))}"` : '';
|
/* Zeilennummer am Knoten (D25): Grundlage für den Sprung ins Textfeld und
|
||||||
|
für die Gegenrichtung (Cursor-Zeile -> Knoten hervorheben). Der Hinweis im
|
||||||
|
Tooltip macht die sonst unsichtbare Alt-Klick-Geste auffindbar. */
|
||||||
|
const lineAttr = n.line ? ` data-line="${n.line}"` : '';
|
||||||
|
const tip = [n.status ? t('st_' + n.status.key) : '', t('jumpHint')]
|
||||||
|
.filter(Boolean).join(' · ');
|
||||||
|
const title = ` title="${attr(tip)}"`;
|
||||||
const tagsHtml = n.tags && n.tags.length
|
const tagsHtml = n.tags && n.tags.length
|
||||||
? `<span class="tags" aria-hidden="true">${n.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}</span>`
|
? `<span class="tags" aria-hidden="true">${n.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}</span>`
|
||||||
: '';
|
: '';
|
||||||
@@ -59,8 +65,8 @@ function nodeHtml(n, extra, opts){
|
|||||||
tagsHtml;
|
tagsHtml;
|
||||||
const aria = ` aria-label="${attr(nodeAria(n, opts))}"`;
|
const aria = ` aria-label="${attr(nodeAria(n, opts))}"`;
|
||||||
const html = n.url
|
const html = n.url
|
||||||
? `<a class="${cls}" href="${attr(n.url)}" target="_blank" rel="noopener"${aria}${title}>${inner}</a>`
|
? `<a class="${cls}" href="${attr(n.url)}" target="_blank" rel="noopener"${lineAttr}${aria}${title}>${inner}</a>`
|
||||||
: `<div class="${cls}" tabindex="0"${aria}${title}>${inner}</div>`;
|
: `<div class="${cls}" tabindex="0"${lineAttr}${aria}${title}>${inner}</div>`;
|
||||||
const ghostTip = attr(t('ghostTooltip'));
|
const ghostTip = attr(t('ghostTooltip'));
|
||||||
const ghost = `<div class="ghost-node" aria-label="${ghostTip}" title="${ghostTip}">${esc(t('ghost'))}</div>`;
|
const ghost = `<div class="ghost-node" aria-label="${ghostTip}" title="${ghostTip}">${esc(t('ghost'))}</div>`;
|
||||||
return html + (need ? ghost : '');
|
return html + (need ? ghost : '');
|
||||||
|
|||||||
@@ -434,6 +434,15 @@
|
|||||||
/* Sichtbarer Tastatur-Fokus (Knoten sind fokussierbar: Fokusreihenfolge =
|
/* Sichtbarer Tastatur-Fokus (Knoten sind fokussierbar: Fokusreihenfolge =
|
||||||
Lese-/Dokumentreihenfolge; Screenreader liest den aria-label). */
|
Lese-/Dokumentreihenfolge; Screenreader liest den aria-label). */
|
||||||
.node:focus-visible{outline:2px solid var(--or);outline-offset:2px;box-shadow:0 2px 8px rgba(36,52,71,.22)}
|
.node:focus-visible{outline:2px solid var(--or);outline-offset:2px;box-shadow:0 2px 8px rgba(36,52,71,.22)}
|
||||||
|
/* Knoten der Cursor-Zeile im Editor (D25). Weißer Halo + Tinte-Ring: hebt sich
|
||||||
|
von allen Pastell-Status UND vom dunklen Wurzelknoten ab und ist vom
|
||||||
|
petrolfarbenen Fokusring unterscheidbar. Rein visuelle Editierhilfe — nicht
|
||||||
|
im Druck. Der `#out`-Präfix ist nötig: `ul.or .node{box-shadow:none}`
|
||||||
|
(weiter unten) ist spezifischer als `.node.current` und schluckte den Ring
|
||||||
|
sonst überall unterhalb einer any-of-Gruppe. */
|
||||||
|
#out .node.current{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--ink)}
|
||||||
|
/* Auf Touch ist der lange Druck die Sprung-Geste — kein iOS-Link-Callout. */
|
||||||
|
a.node{-webkit-touch-callout:none}
|
||||||
.ext{font-size:.72em;margin-left:5px;opacity:.55}
|
.ext{font-size:.72em;margin-left:5px;opacity:.55}
|
||||||
.size{
|
.size{
|
||||||
position:absolute;top:-9px;right:-9px;
|
position:absolute;top:-9px;right:-9px;
|
||||||
@@ -747,5 +756,6 @@
|
|||||||
*{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
|
*{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}
|
||||||
/* Knoten nicht über den Seitenrand zerschneiden */
|
/* Knoten nicht über den Seitenrand zerschneiden */
|
||||||
#out li{break-inside:avoid}
|
#out li{break-inside:avoid}
|
||||||
|
.node.current{box-shadow:none!important} /* Editierhilfe, nicht drucken (D25) */
|
||||||
@page{margin:12mm}
|
@page{margin:12mm}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`renderTreeHtml — kanonisches Beispiel > Grundzustand (Pfad aus, verworfene aus): Struktur-Snapshot 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="has-or"><div class="node st-geplant" tabindex="0" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" aria-label="WordPress, a11yStatus" title="st_geplant">WordPress</div></li><li><div class="node st-idee" tabindex="0" aria-label="Headless CMS, a11yStatus" title="st_idee">Headless CMS</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" aria-label="Cloud">Cloud</div></li><li><div class="node" tabindex="0" aria-label="On-Premise">On-Premise</div></li></ul></li></ul></li>"`;
|
exports[`renderTreeHtml — kanonisches Beispiel > Grundzustand (Pfad aus, verworfene aus): Struktur-Snapshot 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="9" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="10" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="11" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="13" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="14" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="15" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
|
||||||
|
|
||||||
exports[`renderTreeHtml — kanonisches Beispiel > günstigster Pfad an: cheap/cheap-leaf + implizite M-Badges 1`] = `"<li class="has-and"><a class="node root-node cheap st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node cheap st-fertig" tabindex="0" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node cheap st-arbeit" tabindex="0" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node cheap cheap-leaf st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="has-or"><div class="node cheap st-geplant" tabindex="0" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" aria-label="WordPress, a11yStatus, a11ySizeImplicit" title="st_geplant">WordPress<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node st-idee" tabindex="0" aria-label="Headless CMS, a11yStatus, a11ySizeImplicit" title="st_idee">Headless CMS<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li><li class="has-or"><div class="node cheap st-idee" tabindex="0" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf" tabindex="0" aria-label="Cloud, a11ySizeImplicit">Cloud<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node" tabindex="0" aria-label="On-Premise, a11ySizeImplicit">On-Premise<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li>"`;
|
exports[`renderTreeHtml — kanonisches Beispiel > günstigster Pfad an: cheap/cheap-leaf + implizite M-Badges 1`] = `"<li class="has-and"><a class="node root-node cheap st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node cheap st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node cheap cheap-leaf st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node cheap st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node cheap cheap-leaf st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="has-or"><div class="node cheap st-geplant" tabindex="0" data-line="9" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf st-geplant" tabindex="0" data-line="10" aria-label="WordPress, a11yStatus, a11ySizeImplicit" title="st_geplant · jumpHint">WordPress<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node st-idee" tabindex="0" data-line="11" aria-label="Headless CMS, a11yStatus, a11ySizeImplicit" title="st_idee · jumpHint">Headless CMS<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li><li class="has-or"><div class="node cheap st-idee" tabindex="0" data-line="13" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node cheap cheap-leaf" tabindex="0" data-line="14" aria-label="Cloud, a11ySizeImplicit" title="jumpHint">Cloud<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li><li><div class="node" tabindex="0" data-line="15" aria-label="On-Premise, a11ySizeImplicit" title="jumpHint">On-Premise<span class="size implicit" aria-hidden="true" title="implicitSizeTooltip">M</span></div></li></ul></li></ul></li>"`;
|
||||||
|
|
||||||
exports[`renderTreeHtml — kanonisches Beispiel > verworfene einblenden: Eigenentwicklung erscheint (durchgestrichen) 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="has-or"><div class="node st-geplant" tabindex="0" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" aria-label="WordPress, a11yStatus" title="st_geplant">WordPress</div></li><li><div class="node st-idee" tabindex="0" aria-label="Headless CMS, a11yStatus" title="st_idee">Headless CMS</div></li><li><div class="node st-verworfen" tabindex="0" aria-label="Eigenentwicklung, a11yStatus" title="st_verworfen">Eigenentwicklung</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" aria-label="Cloud">Cloud</div></li><li><div class="node" tabindex="0" aria-label="On-Premise">On-Premise</div></li></ul></li></ul></li>"`;
|
exports[`renderTreeHtml — kanonisches Beispiel > verworfene einblenden: Eigenentwicklung erscheint (durchgestrichen) 1`] = `"<li class="has-and"><a class="node root-node st-arbeit" href="https://wiki.example.de/relaunch" target="_blank" rel="noopener" data-line="2" aria-label="Website-Relaunch, a11yStatus, a11ySize, a11yLink" title="st_arbeit · jumpHint">Website-Relaunch<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">XL</span></a><ul class="and"><li class="has-and"><div class="node st-fertig" tabindex="0" data-line="3" aria-label="Konzeption, a11yStatus, a11ySize" title="st_fertig · jumpHint">Konzeption<span class="size" aria-hidden="true">M</span></div><ul class="and"><li><div class="node st-fertig" tabindex="0" data-line="4" aria-label="Zielgruppenanalyse, a11yStatus, a11ySize" title="st_fertig · jumpHint">Zielgruppenanalyse<span class="size" aria-hidden="true">S</span></div></li><li><div class="node st-fertig" tabindex="0" data-line="5" aria-label="Sitemap, a11yStatus, a11ySize" title="st_fertig · jumpHint">Sitemap<span class="size" aria-hidden="true">XS</span></div></li></ul></li><li class="has-and"><div class="node st-arbeit" tabindex="0" data-line="6" aria-label="Umsetzung, a11yStatus, a11ySize" title="st_arbeit · jumpHint">Umsetzung<span class="size" aria-hidden="true">XL</span></div><ul class="and"><li><a class="node st-durchstich" href="https://git.example.de/frontend" target="_blank" rel="noopener" data-line="7" aria-label="Frontend, a11yStatus, a11ySize, a11yTags, a11yLink" title="st_durchstich · jumpHint">Frontend<span class="ext" aria-hidden="true">↗</span><span class="size" aria-hidden="true">S</span><span class="tags" aria-hidden="true"><span class="tag">anna</span></span></a></li><li><div class="node st-geplant" tabindex="0" data-line="8" aria-label="Backend, a11yStatus, a11ySize, a11yTags" title="st_geplant · jumpHint">Backend<span class="size" aria-hidden="true">L</span><span class="tags" aria-hidden="true"><span class="tag">ben</span><span class="tag">carla</span></span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li><li class="has-or"><div class="node st-geplant" tabindex="0" data-line="9" aria-label="CMS-Anbindung, a11yStatus, a11ySize" title="st_geplant · jumpHint">CMS-Anbindung<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node st-geplant" tabindex="0" data-line="10" aria-label="WordPress, a11yStatus" title="st_geplant · jumpHint">WordPress</div></li><li><div class="node st-idee" tabindex="0" data-line="11" aria-label="Headless CMS, a11yStatus" title="st_idee · jumpHint">Headless CMS</div></li><li><div class="node st-verworfen" tabindex="0" data-line="12" aria-label="Eigenentwicklung, a11yStatus" title="st_verworfen · jumpHint">Eigenentwicklung</div></li></ul></li></ul></li><li class="has-or"><div class="node st-idee" tabindex="0" data-line="13" aria-label="Hosting, a11yStatus, a11ySize" title="st_idee · jumpHint">Hosting<span class="size" aria-hidden="true">M</span></div><ul class="or"><li><div class="node" tabindex="0" data-line="14" aria-label="Cloud" title="jumpHint">Cloud</div></li><li><div class="node" tabindex="0" data-line="15" aria-label="On-Premise" title="jumpHint">On-Premise</div></li></ul></li></ul></li>"`;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ describe('renderTreeHtml — „Untergliederung fehlt" (Geister-Knoten, SPEC §5
|
|||||||
const {html} = renderTreeHtml(roots, {t, showDiscarded: false, cheapPath: false, cheapSet: new Set()});
|
const {html} = renderTreeHtml(roots, {t, showDiscarded: false, cheapPath: false, cheapSet: new Set()});
|
||||||
expect(count(html, 'ghost-node')).toBe(1);
|
expect(count(html, 'ghost-node')).toBe(1);
|
||||||
expect(html).toContain('title="ghostTooltip"');
|
expect(html).toContain('title="ghostTooltip"');
|
||||||
expect(html).toMatchInlineSnapshot(`"<li><div class="node root-node st-geplant" tabindex="0" aria-label="Großes Paket, a11yStatus, a11ySize" title="st_geplant">Großes Paket<span class="size" aria-hidden="true">L</span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li>"`);
|
expect(html).toMatchInlineSnapshot(`"<li><div class="node root-node st-geplant" tabindex="0" data-line="1" aria-label="Großes Paket, a11yStatus, a11ySize" title="st_geplant · jumpHint">Großes Paket<span class="size" aria-hidden="true">L</span></div><div class="ghost-node" aria-label="ghostTooltip" title="ghostTooltip">ghost</div></li>"`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('verworfenes M+ löst die Regel nicht aus', () => {
|
it('verworfenes M+ löst die Regel nicht aus', () => {
|
||||||
@@ -102,3 +102,37 @@ describe('renderTreeHtml — Moduswechsel ist CSS, nicht Renderer', () => {
|
|||||||
// vertikal, kompakt) — sie unterscheiden sich nur in der Container-Klasse.
|
// vertikal, kompakt) — sie unterscheiden sich nur in der Container-Klasse.
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Grundlage für den Sprung Diagramm <-> Text (D25, SPEC §9): Jeder Knoten muss
|
||||||
|
die Zeilennummer aus dem Parser als data-line tragen — auch verlinkte Knoten
|
||||||
|
(die als <a> gerendert werden) und Knoten in any-of-Gruppen. */
|
||||||
|
describe('renderTreeHtml — data-line je Knoten (D25)', () => {
|
||||||
|
const lineOf = (html, label) => {
|
||||||
|
const re = new RegExp('<(?:a|div) class="node[^"]*"[^>]*?data-line="(\\d+)"[^>]*>' + label);
|
||||||
|
const m = html.match(re);
|
||||||
|
return m ? Number(m[1]) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('trägt die Parser-Zeilennummer an jeden Knoten', () => {
|
||||||
|
const {html} = renderExample({showDiscarded: true});
|
||||||
|
expect(lineOf(html, 'Website-Relaunch')).toBe(2); // Zeile 1 ist ein %%-Kommentar
|
||||||
|
expect(lineOf(html, 'Zielgruppenanalyse')).toBe(4);
|
||||||
|
expect(lineOf(html, 'Frontend')).toBe(7); // verlinkt -> <a>
|
||||||
|
expect(lineOf(html, 'Headless CMS')).toBe(11); // any-of-Alternative
|
||||||
|
expect(lineOf(html, 'Eigenentwicklung')).toBe(12); // verworfen, eingeblendet
|
||||||
|
});
|
||||||
|
|
||||||
|
it('vergibt jede Zeilennummer genau einmal', () => {
|
||||||
|
const {html} = renderExample({showDiscarded: true});
|
||||||
|
const lines = [...html.matchAll(/class="node[^"]*"[^>]*?data-line="(\d+)"/g)].map(m => m[1]);
|
||||||
|
expect(lines.length).toBe(new Set(lines).size);
|
||||||
|
expect(lines.length).toBe(14); // alle Zeilen des SPEC-Beispiels außer dem Kommentar
|
||||||
|
});
|
||||||
|
|
||||||
|
it('der Geister-Knoten bekommt keine Zeilennummer (er steht für keine Zeile)', () => {
|
||||||
|
const {html} = renderTreeHtml(parse('[ ] Großes Paket (L)').roots,
|
||||||
|
{t, showDiscarded: false, cheapPath: false, cheapSet: new Set()});
|
||||||
|
expect(html).toMatch(/class="ghost-node"/);
|
||||||
|
expect(html).not.toMatch(/ghost-node[^>]*data-line/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user