feat: Querverbindungen folgen der Faltung + Schalter; Falt-Knopf mit vier Voreinstellungen (D75)

- Abhängigkeits-Kanten enden am nächsten sichtbaren Vorfahren, für Quelle
  wie Ziel; zusammengefallene Kanten entfallen bzw. werden dedupliziert
  (data-sub-ids/data-sub-deps am eingeklappten Vertreter, SPEC §9)
- Umschalter für die Querverbindungen neben dem Günstigster-Pfad-Knopf,
  Voreinstellung an, persistiert; Export und Druck folgen ihm
- Der Falt-Knopf schaltet durch: M und kleiner zu → alles abseits des
  Pfads zu → alles zu → alles offen; Icon/Tooltip zeigen den nächsten
  Schritt, Handfaltung setzt die Position zurück (presetFoldSet, model.js)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-25 17:19:32 +02:00
co-authored by Claude Fable 5
parent 69c9be7a6d
commit 33443016ce
10 changed files with 452 additions and 80 deletions
+3
View File
@@ -19,6 +19,9 @@ reverse.
## 2026-08-25 ## 2026-08-25
- The fold button cycles through four presets now: size M and smaller, everything off the cheapest path, everything, all open
- A button next to the cheapest-path toggle switches the dependency cross links on and off
- Dependency links follow the folding: an edge to a hidden node now ends at its first visible ancestor, for source and target alike
- In Brave, the file notice names the flag that enables the File System Access API by hand - In Brave, the file notice names the flag that enables the File System Access API by hand
- Fix: the file notice and the legend blamed "Chromium", but Brave is Chromium without the File System Access API — they now name the feature and example browsers - Fix: the file notice and the legend blamed "Chromium", but Brave is Chromium without the File System Access API — they now name the feature and example browsers
- Browsers without the File System Access API (Firefox, Safari) explain themselves once: files open as copies and saving downloads a new file - Browsers without the File System Access API (Firefox, Safari) explain themselves once: files open as copies and saving downloads a new file
+84
View File
@@ -5660,3 +5660,87 @@ in den Übersetzungen — eine Stelle statt neun, an denen sie vertippt sein
kann. **Nachgemessen** im Dev-Server (Stufe-1-Pfad erzwungen, Brave per kann. **Nachgemessen** im Dev-Server (Stufe-1-Pfad erzwungen, Brave per
`navigator.brave`-Stub zur Laufzeit): mit Stub trägt das Banner die zweite `navigator.brave`-Stub zur Laufzeit): mit Stub trägt das Banner die zweite
Zeile samt Mono-Code der Adresse, ohne Stub fehlt beides. Zeile samt Mono-Code der Adresse, ohne Stub fehlt beides.
## D75 — Querverbindungen folgen der Faltung und bekommen einen Schalter; der Falt-Knopf schaltet vier Voreinstellungen durch
Drei zusammenhängende Nutzerwünsche an derselben Stelle des Diagramms, in
einem Zug gebaut.
**1. Abhängigkeits-Kanten enden am nächsten sichtbaren Vorfahren.** D41 ließ
Kanten zu eingeklappten Knoten schlicht entfallen — gerade in einem dicht
gefalteten Plan verschwand damit die Aussage „dieser Zweig braucht jenen"
genau dann, wenn man sie am nötigsten hat. Die Regel gab es längst: **Der
eingeklappte Knoten vertritt seinen Teilbaum** — für die Pfad-Station, den
„▸ n"-Zähler und die Cursor-Zeile (D38-Nachträge). Jetzt gilt sie auch für
die Querverbindungen, für **Quelle wie Ziel** (Nutzer-Vorgabe). Fallen beide
Endpunkte in denselben sichtbaren Knoten, entfällt die Kante (sie sagte
nichts mehr); mehrere so zusammengefallene Kanten desselben Paars werden
**eine**. Kanten zu ausgeblendeten **verworfenen** Knoten entfallen
weiterhin — dieselbe Grenze wie bei der Cursor-Zeile (D38-Nachtrag 4):
Faltung ist Ansicht, der Verworfen-Filter ist eine Aussage über den Plan.
**Umgesetzt im Renderer, nicht in einer App-Nebenrechnung:** `walkFolded()`
läuft ohnehin durch jeden verborgenen Teilbaum (Warnungen, „▸ n") und sammelt
jetzt dessen IDs und Abhängigkeiten mit; der eingeklappte Knoten trägt sie
als `data-sub-ids`/`data-sub-deps` — getrennt von den eigenen Attributen,
damit die Bedeutung ablesbar bleibt, und headless testbar (Hausregel
D54-Nachtrag 3). `depEdges()` in app.js löst dann nur noch auf: Die
Sub-IDs stehen in DFS- und damit Dokumentreihenfolge, „erste Vergabe
gewinnt" (D36) gilt so auch über die Faltgrenze hinweg. Der Grafikexport
nutzt dasselbe `depEdges()` und folgt ohne Zusatzcode.
**2. Ein Umschalter für die Querverbindungen**, neben dem
Günstigster-Pfad-Knopf (Nutzer-Vorgabe): Voreinstellung an, persistiert in
`werkbaum-ui` wie die Nachbarn (D22). Export und Druck folgen ihm wie den
übrigen Ansichts-Filtern (die D38/D44/D56-Linie: das Bild zeigt, was
sichtbar ist). Der Klick zeichnet nur die Overlays neu statt zu rendern —
am Baum ändert sich nichts.
**3. Der Falt-Knopf wird ein Durchschalter mit vier Voreinstellungen**
(Nutzer-Vorgabe, Reihenfolge wie gewünscht): **(1)** ab Größe M abwärts zu
(die D44-Regel, unverändert), **(2)** alles zu, durch dessen Teilbaum der
günstigste Pfad nicht läuft — weder der Knoten selbst noch ein Unterknoten
liegt darauf; sichtbar bleibt genau der Pfad, alles Übrige steht als je ein
eingeklappter Knoten da —, **(3)** alles zu, **(4)** alles offen, dann
wieder von vorn. Jede Stufe beschreibt einen **vollständigen** Faltzustand
(die D44-Eigenschaft bleibt: zweimal Drücken derselben Stufe ergäbe
dasselbe), geschrieben wird über denselben Weg wie bisher — ein
Undo-Schritt je Stufe, beim Pad trägt die Sitzungs-Überlagerung.
- **Der Knopf zeigt den NÄCHSTEN Schritt** (Icon per `data-next`, Tooltip
aus vier neuen i18n-Schlüsseln × 9 Sprachen; `foldSmallTooltip` entfällt).
Das ist die D17-Logik des Bereichs-Umschalters: Ein Knopf, der den
Zustand zeigt, den man vor sich hat, sagt nichts — einer, der das Ziel
zeigt, sagt, was passiert. Mit vier Stufen ist er zudem kein Umschalter
mehr, `aria-pressed` entfällt.
- **Die Reihum-Position wird nicht gemerkt, sondern geprüft** — die
D44-Fortschreibung für vier Stufen: `render()` rechnet nach, ob der Baum
noch die zuletzt hergestellte Stufe beschreibt (`presetFoldSet` in
model.js, headless getestet); wenn nicht — Handfaltung, Textänderung,
Dokumentwechsel —, beginnt der nächste Druck wieder bei 1. Ein reines
Ablesen ohne Position (D44) trägt bei vier Stufen nicht mehr: Ein voll
offener Baum kann zugleich Stufe 4 und einer leeren Stufe 1 entsprechen —
die Mehrdeutigkeit ist den Stufen inhärent, die geprüfte Position löst
sie deterministisch.
- **Stufe 2 rechnet den Pfad auch bei ausgeschaltetem Pfad-Umschalter**
die Voreinstellung fragt nach dem Pfad, nicht nach seiner Anzeige; die
Rechnung ist dieselbe, die bei eingeschaltetem Pfad ohnehin je Tastendruck
läuft (D42).
**Preis in der Kopfzeile, benannt:** Der Querverbindungs-Knopf ist das elfte
Element. Auf dem Telefon (375 px) bricht die Zeile damit regulär in zwei
Reihen à 78 px — genau der Umbruch, den D50/D56 unterhalb von 440 px
vorsehen; am Schreibtisch bleibt sie einreihig (gemessen 44 px bei 800 px).
**Nachgemessen** im Browser an einem Wegwerf-Dokument (10 Knoten, 3 Kanten,
über die echte UI angelegt und gelöscht): Mittel eingeklappt → Kante endet
am Vertreter (`data-sub-deps="z1"`), Ziel-Seite ebenso; beide Quellen unter
einem Vorfahren eingeklappt → **eine** Kante statt zwei (3 → 2); Wurzel zu →
0 Kanten. Durchschalter: small → path → closed → open → wieder small, je mit
korrekter Faltmenge und Tooltip; Handfaltung danach setzt auf small zurück;
`path` faltet bei ausgeschaltetem Pfad-Umschalter identisch; Undo nimmt eine
Stufe in einem Zug zurück (4 Marken → 1 → 4). Umschalter: aus → 0 Overlays
und 0 Kanten im exportierten SVG, an → 3/3; `depLinks` überlebt den Reload.
480 → 487 Tests, davon 7 neue in `tests/fold.test.js` (presetFoldSet-Modi
inkl. des per `:#…` gezogenen Ziels unter einer Zugabe; `data-sub-*` in
Dokumentreihenfolge, dedupliziert, nie an offenen Knoten, nie für
ausgeblendete verworfene).
+41 -18
View File
@@ -1083,19 +1083,32 @@ fokussierten Knoten (WAI-ARIA-Baum-Idiom).
weil eine Fokusmarke `!!!` ihren Knoten immer wieder hervorholt): Dann wird weil eine Fokusmarke `!!!` ihren Knoten immer wieder hervorholt): Dann wird
lieber nichts geschrieben, als einen Text zu hinterlassen, der etwas anderes lieber nichts geschrieben, als einen Text zu hinterlassen, der etwas anderes
sagt als das Bild. sagt als das Bild.
- **Für den ganzen Baum** gibt es im Diagramm-Kopf einen **Umschalter**: - **Für den ganzen Baum** gibt es im Diagramm-Kopf einen **Durchschalter**:
gedrückt ist alles **ab Größe M abwärts** zugeklappt, nicht gedrückt ist der Jeder Druck stellt die **nächste** von vier Voreinstellungen her, reihum —
ganze Baum **offen**. Betroffen ist jeder Knoten mit Kindern, dessen 1. **ab Größe M abwärts zugeklappt**: jeder Knoten mit Kindern, dessen
**angegebene** Größe (§5) `M` oder kleiner ist — offen bleiben `L`, `XL`, **angegebene** Größe (§5) `M` oder kleiner ist — offen bleiben `L`,
`XXL`. Ein Knoten **ohne** Größenangabe wird nicht zugeklappt: Der günstigste `XL`, `XXL`. Ein Knoten **ohne** Größenangabe wird nicht zugeklappt:
Pfad rechnet fehlende Größen zwar als `M` (unten), das ist aber eine Der günstigste Pfad schätzt fehlende Größen zwar (unten), das ist aber
Kostenannahme und keine Aussage des Autors. eine Kostenannahme und keine Aussage des Autors.
- Beide Stellungen beschreiben einen **vollständigen** Faltzustand — was der 2. **alles abseits des günstigsten Pfads zugeklappt**: jeder Knoten, durch
Umschalter nicht zuklappt, ist danach offen. Sein Zustand wird **nicht dessen Teilbaum der günstigste Pfad (§9) **nicht** läuft — weder er
gemerkt, sondern am Baum abgelesen**: Wer danach einen Knoten von Hand selbst noch ein Unterknoten liegt auf ihm. Der Pfad bleibt vollständig
umklappt, sieht ihn von selbst herausspringen. Geschrieben wird wie beim sichtbar, alles Übrige steht als je ein eingeklappter Knoten da.
einzelnen Umklappen — ein einziger Rückgängig-Schritt für den ganzen Gerechnet wird dafür auch bei **ausgeschaltetem** Pfad-Umschalter — die
Vorgang. Siehe D44. Voreinstellung fragt nach dem Pfad, nicht nach seiner Anzeige.
3. **alles zugeklappt**: jeder Knoten mit Kindern.
4. **alles offen**.
Der Knopf zeigt als Icon und Tooltip den **nächsten** Schritt — also was
Drücken tun wird, nicht was gerade gilt: Den Zustand hat man ohnehin vor
sich (dieselbe Logik wie beim Bereichs-Umschalter auf kleinem Bildschirm).
- Jede Voreinstellung beschreibt einen **vollständigen** Faltzustand — was
sie nicht zuklappt, ist danach offen. Die Reihum-Position wird **nicht
gemerkt, sondern am Baum geprüft**: Beschreibt der Baum nicht mehr den
zuletzt hergestellten Schritt (jemand hat von Hand umgeklappt oder den
Text geändert), beginnt der nächste Druck wieder bei 1. Geschrieben wird
wie beim einzelnen Umklappen — ein einziger Rückgängig-Schritt je
Voreinstellung. Siehe D44, D75.
- Faltung ist **reine Ansicht**: Warnungen aus eingeklappten Teilbäumen werden - Faltung ist **reine Ansicht**: Warnungen aus eingeklappten Teilbäumen werden
weiter gemeldet (sie gelten dem Text), und der günstigste Pfad rechnet weiter gemeldet (sie gelten dem Text), und der günstigste Pfad rechnet
unverändert über den ganzen Baum. unverändert über den ganzen Baum.
@@ -1139,11 +1152,21 @@ Strichen, kein gefülltes Dreieck). Der Baum trägt weiterhin die Hauptaussage.
- Die Kanten des **ausgewählten** Knotens — Tastaturfokus im Diagramm, sonst - Die Kanten des **ausgewählten** Knotens — Tastaturfokus im Diagramm, sonst
der Knoten der Cursor-Zeile — sind in Tinte hervorgehoben, ein- wie der Knoten der Cursor-Zeile — sind in Tinte hervorgehoben, ein- wie
ausgehende; sie liegen über den übrigen Kanten, aber unter jedem Knoten. ausgehende; sie liegen über den übrigen Kanten, aber unter jedem Knoten.
- Kanten zu gerade nicht sichtbaren Knoten (eingeklappt §9, verworfen - **Eingeklappte Knoten vertreten ihre Teilbäume auch hier** (§9, Falten):
ausgeblendet §4) entfallen; bei doppelter ID zielt die Kante auf die erste Liegt ein Endpunkt — Quelle wie Ziel — in einem eingeklappten Teilbaum,
Vergabe (D36/D39). endet die Kante am nächsten **sichtbaren Vorfahren**. Fallen beide
- Die Basis-Kanten erscheinen auch im **Grafikexport** und im **Druck**; die Endpunkte in denselben sichtbaren Knoten zusammen, entfällt die Kante (sie
Hervorhebung ist Interaktion und erscheint nicht. Siehe D41. sagte nichts mehr); mehrere so zusammengefallene Kanten desselben Paars
werden zu **einer**. Kanten zu **ausgeblendeten verworfenen** Knoten (§4)
entfallen weiterhin — der Verworfen-Filter ist eine Aussage über den Plan,
keine Ansicht. Bei doppelter ID zielt die Kante auf die erste Vergabe
(D36/D39).
- **Ein Umschalter im Diagramm-Kopf** (neben dem günstigsten Pfad) blendet
die Querverbindungen ein und aus; Voreinstellung **an**, Zustand gemerkt.
- Die Basis-Kanten erscheinen auch im **Grafikexport** und im **Druck**,
sofern der Umschalter an ist (beide folgen ihm wie den übrigen
Ansichts-Filtern); die Hervorhebung ist Interaktion und erscheint nicht.
Siehe D41, D75.
### 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
+18 -4
View File
@@ -42,6 +42,8 @@
- [^] #ed.deplink: Cross links for dependencies (M) - [^] #ed.deplink: Cross links for dependencies (M)
- [^] #ed.deplink.calm: Visually secondary — the tree carries the message (S) - [^] #ed.deplink.calm: Visually secondary — the tree carries the message (S)
- [^] #ed.deplink.focus: Highlight what a selected node needs and feeds (S) - [^] #ed.deplink.focus: Highlight what a selected node needs and feeds (S)
- [x] #ed.deplink.fold: A hidden endpoint falls to its visible ancestor (S) %% source and target alike, see D75
- [x] #ed.deplink.toggle: A switch hides the cross links (XS)
- [^] #ed.path: Cheapest path (XL) - [^] #ed.path: Cheapest path (XL)
- [^] #ed.path.cost: Cost model, a size prices its subtree (S) - [^] #ed.path.cost: Cost model, a size prices its subtree (S)
- [^] #ed.path.est: Missing size estimated from the sub-packages (S) %% see D66 - [^] #ed.path.est: Missing size estimated from the sub-packages (S) %% see D66
@@ -83,7 +85,7 @@
- [^] #ed.fold.marks: Initial state from the text: > collapses, < re-opens (S) - [^] #ed.fold.marks: Initial state from the text: > collapses, < re-opens (S)
- [^] #ed.fold.click: Fold and unfold in the diagram (S) - [^] #ed.fold.click: Fold and unfold in the diagram (S)
- [^] #ed.fold.write: Folding writes the mark back into the text (S) %% one undo step - [^] #ed.fold.write: Folding writes the mark back into the text (S) %% one undo step
- [^] #ed.fold.preset: One switch folds everything from size M downward (S) %% see D44 - [x] #ed.fold.preset: The fold button cycles through four presets (S) %% M-and-smaller, off-path, all, none — see D44/D75
- [^] #ed.snaps: Earlier states of a document, every ten minutes (S) %% only when something changed - [^] #ed.snaps: Earlier states of a document, every ten minutes (S) %% only when something changed
- [^] #ed.snaps.manual: Save a state by hand, before a larger change (XS) %% ten minutes is the wrong beat for that moment - [^] #ed.snaps.manual: Save a state by hand, before a larger change (XS) %% ten minutes is the wrong beat for that moment
- [^] #ed.files: Open and save .werkbaum files (S) - [^] #ed.files: Open and save .werkbaum files (S)
@@ -397,6 +399,16 @@
The selected node — keyboard focus in the diagram, otherwise the node of the The selected node — keyboard focus in the diagram, otherwise the node of the
caret line — pulls its incoming and outgoing edges forward in ink. caret line — pulls its incoming and outgoing edges forward in ink.
#ed.deplink.fold
When an endpoint sits in a folded subtree, the edge ends at the nearest
visible ancestor instead of vanishing — the collapsed node stands in for
its subtree here too, for source and target alike. Edges whose two ends
fall into the same visible node disappear, duplicates merge into one.
#ed.deplink.toggle
A toggle next to the cheapest-path button hides the cross links entirely;
export and print follow it like the other view filters.
#ed.path #ed.path
Highlight the cheapest way through the tree: with and, everything; with or, Highlight the cheapest way through the tree: with and, everything; with or,
the cheapest alternative. This is the feature the whole notation exists for. the cheapest alternative. This is the feature the whole notation exists for.
@@ -590,9 +602,11 @@
restores exactly what you were looking at. restores exactly what you were looking at.
#ed.fold.preset #ed.fold.preset
One switch puts the whole tree at working height: everything of size M and One button cycles the whole tree through four complete fold states: size M
smaller closed, L and above open. Size rather than depth — depth only says and smaller closed, everything off the cheapest path closed, everything
how finely somebody happened to write it down. closed, everything open. Icon and tooltip show the next step; a manual fold
restarts the cycle. Size rather than depth — depth only says how finely
somebody happened to write it down.
#ed.snaps #ed.snaps
A safety net against mishaps: the text is put aside every ten minutes, but A safety net against mishaps: the text is put aside every ten minutes, but
+14 -4
View File
@@ -196,6 +196,11 @@
<button type="button" class="copybtn togglebtn" id="cheapBtn" aria-pressed="true" data-i18n-title="cheapTooltip" data-i18n-aria="cheapTooltip" title="Günstigsten Pfad hervorheben nicht benötigte Alternativen treten zurück" aria-label="Günstigsten Pfad hervorheben nicht benötigte Alternativen treten zurück"> <button type="button" class="copybtn togglebtn" id="cheapBtn" aria-pressed="true" data-i18n-title="cheapTooltip" data-i18n-aria="cheapTooltip" title="Günstigsten Pfad hervorheben nicht benötigte Alternativen treten zurück" aria-label="Günstigsten Pfad hervorheben nicht benötigte Alternativen treten zurück">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5" cy="6" r="2.5"/><circle cx="19" cy="18" r="2.5"/><path d="M7.5 6H14a3 3 0 0 1 0 6H10a3 3 0 0 0 0 6h6.5"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="5" cy="6" r="2.5"/><circle cx="19" cy="18" r="2.5"/><path d="M7.5 6H14a3 3 0 0 1 0 6H10a3 3 0 0 0 0 6h6.5"/></svg>
</button> </button>
<!-- Querverbindungen der Abhängigkeiten ein-/ausblenden (SPEC §9, D75):
ein Umschalter wie der Pfad daneben; Export und Druck folgen ihm. -->
<button type="button" class="copybtn togglebtn" id="depBtn" aria-pressed="true" data-i18n-title="depsTooltip" data-i18n-aria="depsTooltip" title="Querverbindungen der Abhängigkeiten anzeigen" aria-label="Querverbindungen der Abhängigkeiten anzeigen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4.5 19.5 C 9 21.5, 14.5 15.5, 17.3 8.6" stroke-dasharray="0.2 3.4"/><path d="M14.3 7.2 L 18 6.6 L 17.6 10.3"/></svg>
</button>
<!-- Von Station zu Station des günstigsten Pfads (SPEC §9, D47). Kein <!-- Von Station zu Station des günstigsten Pfads (SPEC §9, D47). Kein
Umschalter, sondern eine Bewegung: jeder Druck holt die nächste noch Umschalter, sondern eine Bewegung: jeder Druck holt die nächste noch
offene Station in die Mitte. Verborgen, solange es keine gibt. --> offene Station in die Mitte. Verborgen, solange es keine gibt. -->
@@ -205,10 +210,15 @@
<!-- Der Stern stand hier bis D58 („Was ist neu?", D28) und ist in die <!-- Der Stern stand hier bis D58 („Was ist neu?", D28) und ist in die
Kopfzeile gewandert: Er sagt jetzt nicht nur etwas über das aktive Kopfzeile gewandert: Er sagt jetzt nicht nur etwas über das aktive
Dokument, sondern über das Produkt — und ist deshalb immer da. --> Dokument, sondern über das Produkt — und ist deshalb immer da. -->
<!-- Faltung für den ganzen Baum (SPEC §9, D44): ein Umschalter wie die <!-- Faltung für den ganzen Baum (SPEC §9, D44/D75): ein DURCHSCHALTER —
beiden Nachbarn. Gedrückt = alles ab Größe M abwärts ist zugeklappt. --> jeder Druck stellt die nächste von vier Voreinstellungen her. Icon
<button type="button" class="copybtn togglebtn" id="foldSmallBtn" aria-pressed="false" data-i18n-title="foldSmallTooltip" data-i18n-aria="foldSmallTooltip" title="Knoten der Größe M und kleiner zuklappen erneut drücken klappt alle wieder auf" aria-label="Knoten der Größe M und kleiner zuklappen erneut drücken klappt alle wieder auf"> und Tooltip zeigen den NÄCHSTEN Schritt (was Drücken tun wird);
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21V3M8 8l4 4 4-4M8 16l4-4 4 4"/></svg> welches Icon sichtbar ist, steuert `data-next` per CSS. -->
<button type="button" class="copybtn foldbtn" id="foldBtn" data-next="small" title="Knoten der Größe M und kleiner zuklappen" aria-label="Knoten der Größe M und kleiner zuklappen">
<svg class="fc-small" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3.5l4 4 4-4M8 20.5l4-4 4 4"/><text x="12" y="15" text-anchor="middle" font-size="9.5" font-weight="600" fill="currentColor" stroke="none" font-family="inherit">M</text></svg>
<svg class="fc-path" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3.5l4 4 4-4M8 20.5l4-4 4 4"/><circle cx="12" cy="12" r="2.6"/></svg>
<svg class="fc-closed" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21V3M8 8l4 4 4-4M8 16l4-4 4 4"/></svg>
<svg class="fc-open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v18M8 7l4-4 4 4M8 17l4 4 4-4"/></svg>
</button> </button>
<!-- Knoten-IDs vor dem Titel einblenden (SPEC §9, D56) — geschrieben wie <!-- Knoten-IDs vor dem Titel einblenden (SPEC §9, D56) — geschrieben wie
im Text: `#some.id: Titel`. Das Zeichen selbst ist das Symbol; ein im Text: `#some.id: Titel`. Das Zeichen selbst ist das Symbol; ein
+146 -47
View File
@@ -1,6 +1,6 @@
import './style.css'; import './style.css';
import { parse, setFoldMark, expandShortIds, shortIdClosed } from './parser.js'; import { parse, setFoldMark, expandShortIds, shortIdClosed } from './parser.js';
import { computeCheapPlan, overloadedAssignee, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM, lineTargets } from './model.js'; import { computeCheapPlan, overloadedAssignee, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, presetFoldSet, lineTargets } from './model.js';
import { esc, renderTreeHtml, TIP_RULE } from './render.js'; import { esc, renderTreeHtml, TIP_RULE } from './render.js';
import { formatWarning, warningText } from './warnings.js'; import { formatWarning, warningText } from './warnings.js';
import { padUrls } from './remote.js'; import { padUrls } from './remote.js';
@@ -59,7 +59,15 @@ const lineNoInner = lineNoBox.firstElementChild;
Pfad) lebt headless in model.js, das HTML-Erzeugen in render.js. Hier bleibt Pfad) lebt headless in model.js, das HTML-Erzeugen in render.js. Hier bleibt
nur der UI-State des Günstigster-Pfad-Toggles (persistiert). */ nur der UI-State des Günstigster-Pfad-Toggles (persistiert). */
let cheapPathOn = true; let cheapPathOn = true;
let depLinksOn = true; /* Querverbindungen der Abhängigkeiten anzeigen (D75) */
let showIds = false; /* Knoten-IDs im Diagramm einblenden (D56) */ let showIds = false; /* Knoten-IDs im Diagramm einblenden (D56) */
/* Falt-Durchschalter (SPEC §9/D75): vier Voreinstellungen, reihum. `Next` ist
der Schritt, den der nächste Druck ausführt (Icon + Tooltip zeigen IHN),
`Applied` der zuletzt hergestellte render() prüft ihn gegen den Baum und
setzt die Position zurück, sobald er nicht mehr stimmt. Nicht persistiert:
Der Faltzustand steht im Text (D38), die Position ist nur der Zeiger darin. */
const FOLD_CYCLE = ['small', 'path', 'closed', 'open'];
let foldCycleNext = 0, foldCycleApplied = -1;
/* Ansicht bei einem Pad-Dokument (D31): beide | nur Pad | nur Spiegel. Hier oben /* Ansicht bei einem Pad-Dokument (D31): beide | nur Pad | nur Spiegel. Hier oben
deklariert, weil saveUI() sie liest und schon aus applySplit() heraus laufen deklariert, weil saveUI() sie liest und schon aus applySplit() heraus laufen
kann weiter unten stünde sie dann noch in der temporalen Todeszone. */ kann weiter unten stünde sie dann noch in der temporalen Todeszone. */
@@ -167,25 +175,29 @@ function render(){
? freshProdSet(freshPrevRoots, roots) : new Set(); ? freshProdSet(freshPrevRoots, roots) : new Set();
} }
const collapsedSet = new Set(); const collapsedSet = new Set();
let foldSmallExact = true, foldSmallAny = false;
foldByLine = new Map(); foldByLine = new Map();
/* Reihum-Position des Falt-Durchschalters (SPEC §9/D75): nicht gemerkt,
sondern am Baum GEPRÜFT die D44-Linie, nur dass der Knopf jetzt vier
Voreinstellungen durchläuft. Beschreibt der Baum nicht mehr den zuletzt
hergestellten Schritt (Hand-Faltung, Textänderung, Dokumentwechsel),
beginnt der nächste Druck wieder vorn. Der 'path'-Schritt braucht den
günstigsten Pfad auch bei ausgeschaltetem Pfad-Umschalter dann wird
er hier eigens für die Prüfung gerechnet (dieselbe Rechnung, die sonst
ohnehin je Tastendruck läuft). */
let presetSet = null, presetMatch = true;
if(foldCycleApplied >= 0){
const mode = FOLD_CYCLE[foldCycleApplied];
const cs = (mode === 'path' && !cheapPathOn) ? computeCheapPlan(roots).set : cheapSet;
presetSet = presetFoldSet(roots, mode, cs);
}
keys.forEach((key, n) => { keys.forEach((key, n) => {
const ov = foldOverrides.get(key); const ov = foldOverrides.get(key);
const collapsed = (ov !== undefined ? ov : initFold.has(n)) && n.children.length > 0; const collapsed = (ov !== undefined ? ov : initFold.has(n)) && n.children.length > 0;
if(collapsed) collapsedSet.add(n); if(collapsed) collapsedSet.add(n);
foldByLine.set(n.line, {key, collapsed, canFold: n.children.length > 0}); foldByLine.set(n.line, {key, collapsed, canFold: n.children.length > 0});
/* Der Umschalter (D44) merkt sich nichts, er liest ab: gedrückt genau if(presetSet && n.children.length > 0 && collapsed !== presetSet.has(n)) presetMatch = false;
dann, wenn JEDER faltbare Knoten so steht, wie die Voreinstellung ihn
stellen würde. */
if(n.children.length > 0){
if(atMostM(n)) foldSmallAny = true;
if(collapsed !== atMostM(n)) foldSmallExact = false;
}
}); });
/* `foldSmallAny`: Gäbe es gar keinen Knoten bis M, wären zugeklappt" und if(presetSet && !presetMatch){ foldCycleApplied = -1; foldCycleNext = 0; }
alles offen" derselbe Zustand der Knopf säße dann gedrückt da, ohne
dass etwas zugeklappt ist. */
foldSmallBtn.setAttribute('aria-pressed', foldSmallExact && foldSmallAny ? 'true' : 'false');
/* Aus DENSELBEN Mengen wie das Rendern die Map muss dieselbe Faltung /* Aus DENSELBEN Mengen wie das Rendern die Map muss dieselbe Faltung
beschreiben, die gleich im DOM steht (dieselbe Regel wie bei freshSet). */ beschreiben, die gleich im DOM steht (dieselbe Regel wie bei freshSet). */
lineTargetMap = lineTargets(roots, collapsedSet, showDiscarded); lineTargetMap = lineTargets(roots, collapsedSet, showDiscarded);
@@ -223,6 +235,7 @@ function render(){
revealFocusMark(); /* `!!!` ins Bild holen, wenn die Marke neu ist (SPEC §1) */ revealFocusMark(); /* `!!!` ins Bild holen, wenn die Marke neu ist (SPEC §1) */
updateFreshBtn(); /* Zähler folgt der gerade gerenderten Menge (D28) */ updateFreshBtn(); /* Zähler folgt der gerade gerenderten Menge (D28) */
updateLeanBtn(); /* Stationen des Pfads haben sich geändert (D47) */ updateLeanBtn(); /* Stationen des Pfads haben sich geändert (D47) */
updateFoldBtn(); /* Icon + Tooltip = nächster Schritt des Durchschalters (D75) */
} }
/* Fokusmarke `!!!` (SPEC §1): Der erste markierte Knoten wird ins Bild geholt /* Fokusmarke `!!!` (SPEC §1): Der erste markierte Knoten wird ins Bild geholt
@@ -409,16 +422,32 @@ function drawCheapPath(){
fokussierten Knotens bzw. der Cursor-Zeile liegen hervorgehoben in Tinte fokussierten Knotens bzw. der Cursor-Zeile liegen hervorgehoben in Tinte
auf einer vorderen Ebene. */ auf einer vorderen Ebene. */
function depEdges(){ function depEdges(){
/* Eingeklappte Knoten vertreten ihre Teilbäume (SPEC §9/D75): Der Renderer
hängt IDs und Abhängigkeiten der verborgenen Knoten als `data-sub-ids`/
`data-sub-deps` an den eingeklappten Vertreter Quelle wie Ziel enden
damit am nächsten sichtbaren Vorfahren. querySelectorAll liefert
Dokumentreihenfolge, und die Sub-IDs stehen (DFS) ebenso darin: erste
Vergabe gewinnt" (D36) gilt so auch über die Faltgrenze hinweg. */
const byId = new Map(); const byId = new Map();
out.querySelectorAll('.node[data-id]').forEach(el => { out.querySelectorAll('.node[data-id], .node[data-sub-ids]').forEach(el => {
if(!byId.has(el.dataset.id)) byId.set(el.dataset.id, el); /* erste Vergabe gewinnt (D36) */ if(el.dataset.id && !byId.has(el.dataset.id)) byId.set(el.dataset.id, el);
if(el.dataset.subIds) for(const id of el.dataset.subIds.split(' '))
if(!byId.has(id)) byId.set(id, el);
}); });
const edges = []; const edges = [], seen = new Set();
out.querySelectorAll('.node[data-deps]').forEach(el => { out.querySelectorAll('.node[data-deps], .node[data-sub-deps]').forEach(el => {
for(const d of el.dataset.deps.split(' ')){ const deps = ((el.dataset.deps || '') + ' ' + (el.dataset.subDeps || ''))
.split(' ').filter(Boolean);
for(const d of deps){
const target = byId.get(d); const target = byId.get(d);
/* verborgene (eingeklappte/ausgeblendete) oder unbekannte Ziele: keine Kante */ /* ausgeblendete (verworfene) oder unbekannte Ziele: keine Kante; beide
if(target && target !== el) edges.push([el, target]); Endpunkte im selben sichtbaren Knoten zusammengefallen: ebenso
und mehrere zusammengefallene Kanten desselben Paars werden EINE. */
if(!target || target === el) continue;
const key = el.dataset.line + '>' + target.dataset.line;
if(seen.has(key)) continue;
seen.add(key);
edges.push([el, target]);
} }
}); });
return edges; return edges;
@@ -464,6 +493,7 @@ function activeDepNode(){
} }
function drawDepLinks(){ function drawDepLinks(){
out.querySelectorAll('svg.dep-overlay').forEach(e => e.remove()); out.querySelectorAll('svg.dep-overlay').forEach(e => e.remove());
if(!depLinksOn) return; /* Umschalter (SPEC §9/D75), Voreinstellung an */
const edges = depEdges(); const edges = depEdges();
if(!edges.length) return; if(!edges.length) return;
const outRect = out.getBoundingClientRect(); const outRect = out.getBoundingClientRect();
@@ -656,8 +686,9 @@ function diagramToSvg(){
}); });
/* 1a) Querverbindungen der Abhängigkeiten (D41) Basis-Kanten hinter den /* 1a) Querverbindungen der Abhängigkeiten (D41) Basis-Kanten hinter den
Knoten; die Fokus-Hervorhebung ist Interaktion und wird nicht exportiert. */ Knoten; die Fokus-Hervorhebung ist Interaktion und wird nicht exportiert.
depEdges().forEach(([from, to]) => { Der Export folgt dem Umschalter (D75) wie den übrigen Ansichts-Filtern. */
(depLinksOn ? depEdges() : []).forEach(([from, to]) => {
const c = depCurve(R(from), R(to)); const c = depCurve(R(from), R(to));
parts.push(`<path d="${c.d}" fill="none" stroke="#6B7A8C" stroke-width="1.5" opacity="0.4" stroke-linecap="round" stroke-dasharray="0.1 3"/>`); parts.push(`<path d="${c.d}" fill="none" stroke="#6B7A8C" stroke-width="1.5" opacity="0.4" stroke-linecap="round" stroke-dasharray="0.1 3"/>`);
parts.push(`<path d="${depArrow(c.end, c.ctrl)}" fill="none" stroke="#6B7A8C" stroke-width="1.5" opacity="0.4" stroke-linecap="round" stroke-linejoin="round"/>`); parts.push(`<path d="${depArrow(c.end, c.ctrl)}" fill="none" stroke="#6B7A8C" stroke-width="1.5" opacity="0.4" stroke-linecap="round" stroke-linejoin="round"/>`);
@@ -1223,17 +1254,22 @@ function writeAllFoldMarks(roots, want){
return withEditorWritable(() => replaceTextUndoable(neu)); return withEditorWritable(() => replaceTextUndoable(neu));
} }
/* Voreinstellungen aus dem Diagramm-Kopf (SPEC §9, D44): den ganzen Baum auf /* Voreinstellungen aus dem Diagramm-Kopf (SPEC §9, D44/D75): den ganzen Baum
einmal auf- bzw. bis unter Größe M zuklappen. Gesetzt wird über dieselben auf einmal in einen der vier Durchschalter-Zustände stellen. Gesetzt wird
Sitzungs-Überlagerungen wie beim einzelnen Umklappen und danach in den Text über dieselben Sitzungs-Überlagerungen wie beim einzelnen Umklappen und
geschrieben es ist derselbe Vorgang, nur für viele Knoten. Deshalb auch danach in den Text geschrieben es ist derselbe Vorgang, nur für viele
EIN Undo-Schritt: `replaceTextUndoable` schreibt genau einmal. */ Knoten. Deshalb auch EIN Undo-Schritt: `replaceTextUndoable` schreibt genau
einmal. Der 'path'-Modus rechnet den günstigsten Pfad selbst er gilt auch
bei ausgeschaltetem Pfad-Umschalter (die Voreinstellung fragt nach dem
Pfad, nicht nach seiner Anzeige). */
function applyFoldPreset(mode){ function applyFoldPreset(mode){
const roots = parse(src.value).roots; const roots = parse(src.value).roots;
if(!roots.length) return; if(!roots.length) return;
const cs = mode === 'path' ? computeCheapPlan(roots).set : null;
const want = presetFoldSet(roots, mode, cs);
nodeKeys(roots).forEach((key, n) => { nodeKeys(roots).forEach((key, n) => {
if(!n.children.length) return; /* nur faltbare Knoten */ if(!n.children.length) return; /* nur faltbare Knoten */
foldOverrides.set(key, mode === 'small' ? atMostM(n) : false); foldOverrides.set(key, want.has(n));
}); });
if(!src.readOnly && writeAllFoldMarks(roots, desiredFoldKeys(roots))) foldOverrides.clear(); if(!src.readOnly && writeAllFoldMarks(roots, desiredFoldKeys(roots))) foldOverrides.clear();
else render(); /* Pad o. nicht ausdrückbar: Überlagerung trägt */ else render(); /* Pad o. nicht ausdrückbar: Überlagerung trägt */
@@ -2011,17 +2047,27 @@ function discardedShown(){ return showc.getAttribute('aria-pressed') === 'true';
function setDiscarded(on){ showc.setAttribute('aria-pressed', on ? 'true' : 'false'); } function setDiscarded(on){ showc.setAttribute('aria-pressed', on ? 'true' : 'false'); }
showc.addEventListener('click', () => { setDiscarded(!discardedShown()); render(); saveUI(); }); showc.addEventListener('click', () => { setDiscarded(!discardedShown()); render(); saveUI(); });
/* Faltung für den ganzen Baum (SPEC §9, D44): ein Umschalter wie die beiden /* Faltung für den ganzen Baum (SPEC §9, D44/D75): ein DURCHSCHALTER jeder
Nachbarn. Gedrückt heißt alles unter Größe M ist zugeklappt". Der Zustand Druck stellt die nächste von vier Voreinstellungen her (ab M abwärts zu
wird nicht gemerkt, sondern bei jedem Rendern am Baum ABGELESEN alles abseits des Pfads zu alles zu alles offen wieder vorn). Icon
(`syncFoldSmallBtn`) klappt jemand danach einen Knoten von Hand um, geht und Tooltip zeigen den NÄCHSTEN Schritt, also was Drücken tun wird den
der Knopf von selbst heraus, statt etwas zu behaupten, das nicht mehr Zustand hat man vor sich (dieselbe Logik wie beim Bereichs-Umschalter,
stimmt. Deshalb auch nicht in `werkbaum-ui` persistiert: Der Faltzustand D17). Die Position wird nicht persistiert; render() prüft sie gegen den
steht im Text (D38-Nachtrag 2), der Knopf liest ihn nur ab. */ Baum und setzt sie zurück, sobald jemand von Hand umklappt (D44-Linie). */
const foldSmallBtn = document.getElementById('foldSmallBtn'); const foldBtn = document.getElementById('foldBtn');
foldSmallBtn.addEventListener('click', () => { foldBtn.addEventListener('click', () => {
applyFoldPreset(foldSmallBtn.getAttribute('aria-pressed') === 'true' ? 'all' : 'small'); const mode = FOLD_CYCLE[foldCycleNext];
foldCycleApplied = foldCycleNext;
foldCycleNext = (foldCycleNext + 1) % FOLD_CYCLE.length;
applyFoldPreset(mode); /* rendert; render() prüft und aktualisiert den Knopf */
}); });
function updateFoldBtn(){
const mode = FOLD_CYCLE[foldCycleNext];
foldBtn.dataset.next = mode;
const tip = t('foldCycle_' + mode);
foldBtn.title = tip;
foldBtn.setAttribute('aria-label', tip);
}
/* Günstigster-Pfad-Hervorhebung an/aus */ /* Günstigster-Pfad-Hervorhebung an/aus */
const cheapBtn = document.getElementById('cheapBtn'); const cheapBtn = document.getElementById('cheapBtn');
@@ -2032,6 +2078,18 @@ cheapBtn.addEventListener('click', () => {
saveUI(); saveUI();
}); });
/* Querverbindungen der Abhängigkeiten an/aus (SPEC §9/D75): ein Umschalter
wie der Pfad daneben; Grafikexport und Druck folgen ihm. Nur die Overlays
werden neu gezeichnet am Baum ändert sich nichts, ein render() wäre
umsonst gearbeitet. */
const depBtn = document.getElementById('depBtn');
depBtn.addEventListener('click', () => {
depLinksOn = !depLinksOn;
depBtn.setAttribute('aria-pressed', depLinksOn ? 'true' : 'false');
drawDepLinks();
saveUI();
});
/* Knoten-IDs im Diagramm an/aus (D56). Wie die Nachbarn ein Umschalter mit /* Knoten-IDs im Diagramm an/aus (D56). Wie die Nachbarn ein Umschalter mit
`aria-pressed`; der Zustand gehört zur Ansicht und wird wie Modus, Zoom und `aria-pressed`; der Zustand gehört zur Ansicht und wird wie Modus, Zoom und
Aufteilung global gemerkt (D22). Neu gerendert statt per CSS versteckt Aufteilung global gemerkt (D22). Neu gerendert statt per CSS versteckt
@@ -2175,7 +2233,11 @@ const I18N = {
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",
leanNextTooltip:"Zur nächsten Station des günstigsten Pfads springen was als Nächstes dran ist ({n} offen)", leanNextTooltip:"Zur nächsten Station des günstigsten Pfads springen was als Nächstes dran ist ({n} offen)",
foldSmallTooltip:"Knoten der Größe M und kleiner zuklappen erneut drücken klappt alle wieder auf", depsTooltip:"Querverbindungen der Abhängigkeiten anzeigen",
foldCycle_small:"Knoten der Größe M und kleiner zuklappen",
foldCycle_path:"Alles abseits des günstigsten Pfads zuklappen",
foldCycle_closed:"Alle Knoten zuklappen",
foldCycle_open:"Alle Knoten aufklappen",
implicitSizeTooltip:"Keine Größe angegeben für die Kostenschätzung mindestens {size} angenommen", implicitSizeTooltip:"Keine Größe angegeben für die Kostenschätzung mindestens {size} angenommen",
fullscreenTooltip:"Vollbild Panels nutzen die ganze Fensterbreite", fullscreenTooltip:"Vollbild Panels nutzen die ganze Fensterbreite",
brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).", brandTooltip:"„Werkbaum“ bedeutet so viel wie Werk-Baum — der Baum des Projektstrukturplans (WBS).",
@@ -2284,7 +2346,11 @@ const I18N = {
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",
leanNextTooltip:"Jump to the next station on the cheapest path what to tackle next ({n} open)", leanNextTooltip:"Jump to the next station on the cheapest path what to tackle next ({n} open)",
foldSmallTooltip:"Collapse nodes of size M and smaller press again to expand them all", depsTooltip:"Show dependency cross-links",
foldCycle_small:"Collapse nodes of size M and smaller",
foldCycle_path:"Collapse everything off the cheapest path",
foldCycle_closed:"Collapse all nodes",
foldCycle_open:"Expand all nodes",
implicitSizeTooltip:"No size given assumed at least {size} for the cost estimate", implicitSizeTooltip:"No size given assumed at least {size} for the cost estimate",
fullscreenTooltip:"Full screen panels use the full window width", fullscreenTooltip:"Full screen panels use the full window width",
brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).", brandTooltip:"“Werkbaum” means roughly work tree — the tree of the work breakdown structure (WBS).",
@@ -2392,7 +2458,11 @@ const I18N = {
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",
leanNextTooltip:"Ir a la siguiente estación de la ruta más económica: lo próximo que toca ({n} pendientes)", leanNextTooltip:"Ir a la siguiente estación de la ruta más económica: lo próximo que toca ({n} pendientes)",
foldSmallTooltip:"Plegar los nodos de talla M o menor: pulsa de nuevo para desplegarlos todos", depsTooltip:"Mostrar los enlaces de dependencias",
foldCycle_small:"Plegar los nodos de talla M o menor",
foldCycle_path:"Plegar todo lo que queda fuera de la ruta más económica",
foldCycle_closed:"Plegar todos los nodos",
foldCycle_open:"Desplegar todos los nodos",
implicitSizeTooltip:"Sin tamaño indicado: se asume al menos {size} para el cálculo de costes", implicitSizeTooltip:"Sin tamaño indicado: se asume al menos {size} para el cálculo de costes",
fullscreenTooltip:"Pantalla completa los paneles usan todo el ancho de la ventana", fullscreenTooltip:"Pantalla completa los paneles usan todo el ancho de la ventana",
brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo — el árbol de la estructura de desglose del trabajo (EDT).", brandTooltip:"«Werkbaum» significa algo así como ‘árbol de trabajo — el árbol de la estructura de desglose del trabajo (EDT).",
@@ -2500,7 +2570,11 @@ const I18N = {
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",
leanNextTooltip:"Aller à la station suivante du chemin le moins coûteux la prochaine chose à faire ({n} en attente)", leanNextTooltip:"Aller à la station suivante du chemin le moins coûteux la prochaine chose à faire ({n} en attente)",
foldSmallTooltip:"Replier les nœuds de taille M et moins appuyez à nouveau pour tout déplier", depsTooltip:"Afficher les liens de dépendances",
foldCycle_small:"Replier les nœuds de taille M et moins",
foldCycle_path:"Replier tout ce qui est hors du chemin le moins coûteux",
foldCycle_closed:"Replier tous les nœuds",
foldCycle_open:"Déplier tous les nœuds",
implicitSizeTooltip:"Aucune taille indiquée au moins {size} supposé pour l'estimation des coûts", implicitSizeTooltip:"Aucune taille indiquée au moins {size} supposé pour l'estimation des coûts",
fullscreenTooltip:"Plein écran les panneaux occupent toute la largeur de la fenêtre", fullscreenTooltip:"Plein écran les panneaux occupent toute la largeur de la fenêtre",
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).", brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — larbre de lorganigramme des tâches (WBS).",
@@ -2608,7 +2682,11 @@ const I18N = {
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",
leanNextTooltip:"Przejdź do następnej stacji najtańszej ścieżki co dalej ({n} otwartych)", leanNextTooltip:"Przejdź do następnej stacji najtańszej ścieżki co dalej ({n} otwartych)",
foldSmallTooltip:"Zwiń węzły o rozmiarze M i mniejsze naciśnij ponownie, aby rozwinąć wszystkie", depsTooltip:"Pokaż powiązania zależności",
foldCycle_small:"Zwiń węzły o rozmiarze M i mniejsze",
foldCycle_path:"Zwiń wszystko poza najtańszą ścieżką",
foldCycle_closed:"Zwiń wszystkie węzły",
foldCycle_open:"Rozwiń wszystkie węzły",
implicitSizeTooltip:"Nie podano rozmiaru do szacowania kosztów przyjęto co najmniej {size}", implicitSizeTooltip:"Nie podano rozmiaru do szacowania kosztów przyjęto co najmniej {size}",
fullscreenTooltip:"Pełny ekran panele wykorzystują całą szerokość okna", fullscreenTooltip:"Pełny ekran panele wykorzystują całą szerokość okna",
brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).", brandTooltip:"„Werkbaum” znaczy mniej więcej drzewo pracy — drzewo struktury podziału pracy (WBS).",
@@ -2716,7 +2794,11 @@ const I18N = {
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом", discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются", cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
leanNextTooltip:"Перейти к следующей станции самого дешёвого пути — что делать дальше ({n} открыто)", leanNextTooltip:"Перейти к следующей станции самого дешёвого пути — что делать дальше ({n} открыто)",
foldSmallTooltip:"Свернуть узлы размера M и меньше — нажмите ещё раз, чтобы развернуть все", depsTooltip:"Показать связи зависимостей",
foldCycle_small:"Свернуть узлы размера M и меньше",
foldCycle_path:"Свернуть всё вне самого дешёвого пути",
foldCycle_closed:"Свернуть все узлы",
foldCycle_open:"Развернуть все узлы",
implicitSizeTooltip:"Размер не указан — для оценки затрат принят не меньше {size}", implicitSizeTooltip:"Размер не указан — для оценки затрат принят не меньше {size}",
fullscreenTooltip:"Полный экран – панели занимают всю ширину окна", fullscreenTooltip:"Полный экран – панели занимают всю ширину окна",
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).", brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
@@ -2824,7 +2906,11 @@ const I18N = {
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ", discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं", cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
leanNextTooltip:"सबसे किफ़ायती पथ के अगले पड़ाव पर जाएँ – अगला काम ({n} शेष)", leanNextTooltip:"सबसे किफ़ायती पथ के अगले पड़ाव पर जाएँ – अगला काम ({n} शेष)",
foldSmallTooltip:"आकार M और उससे छोटे नोड समेटें – सभी को खोलने के लिए फिर दबाएँ", depsTooltip:"निर्भरता के आड़े संबंध दिखाएँ",
foldCycle_small:"आकार M और उससे छोटे नोड समेटें",
foldCycle_path:"सबसे किफ़ायती पथ से बाहर सब कुछ समेटें",
foldCycle_closed:"सभी नोड समेटें",
foldCycle_open:"सभी नोड खोलें",
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए कम से कम {size} माना गया", implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए कम से कम {size} माना गया",
fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं", fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं",
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।", brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
@@ -2917,7 +3003,11 @@ const I18N = {
discardedTooltip:"显示/隐藏已放弃的节点及其子树", discardedTooltip:"显示/隐藏已放弃的节点及其子树",
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化", cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
leanNextTooltip:"跳到成本最低路径的下一站——接下来该做的事(还有 {n} 项)", leanNextTooltip:"跳到成本最低路径的下一站——接下来该做的事(还有 {n} 项)",
foldSmallTooltip:"折叠尺寸 M 及更小的节点——再次点击可全部展开", depsTooltip:"显示依赖关系连线",
foldCycle_small:"折叠尺寸 M 及更小的节点",
foldCycle_path:"折叠最低成本路径之外的所有节点",
foldCycle_closed:"折叠所有节点",
foldCycle_open:"展开所有节点",
implicitSizeTooltip:"未指定尺寸——成本估算时按至少 {size} 计", implicitSizeTooltip:"未指定尺寸——成本估算时按至少 {size} 计",
ghostTooltip:"从 M 号起,元素应进一步细分。", ghostTooltip:"从 M 号起,元素应进一步细分。",
jumpHint:"Alt+点击:跳转到文本中的该行", jumpHint:"Alt+点击:跳转到文本中的该行",
@@ -3025,7 +3115,11 @@ const I18N = {
discardedTooltip:"破棄したノードとその下位ツリーを表示/非表示", discardedTooltip:"破棄したノードとその下位ツリーを表示/非表示",
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示", cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
leanNextTooltip:"最も低コストの経路の次の駅へ移動 – 次にやること(残り {n} 件)", leanNextTooltip:"最も低コストの経路の次の駅へ移動 – 次にやること(残り {n} 件)",
foldSmallTooltip:"サイズ M 以下のノードを折りたたむ – もう一度押すとすべて展開", depsTooltip:"依存関係のリンクを表示",
foldCycle_small:"サイズ M 以下のノードを折りたたむ",
foldCycle_path:"最も低コストの経路以外をすべて折りたたむ",
foldCycle_closed:"すべてのノードを折りたたむ",
foldCycle_open:"すべてのノードを展開",
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため少なくとも {size} として扱う", implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため少なくとも {size} として扱う",
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。", ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
jumpHint:"Alt+クリック:テキストの該当行へ移動", jumpHint:"Alt+クリック:テキストの該当行へ移動",
@@ -3367,6 +3461,7 @@ function saveUI(){
mode: modeEl ? modeEl.value : 'horizontal', mode: modeEl ? modeEl.value : 'horizontal',
discarded: discardedShown(), discarded: discardedShown(),
cheapPath: cheapPathOn, cheapPath: cheapPathOn,
depLinks: depLinksOn,
showIds, showIds,
split: splitState, split: splitState,
col: app.style.getPropertyValue('--col') || null, col: app.style.getPropertyValue('--col') || null,
@@ -3406,6 +3501,10 @@ function restoreState(){
cheapPathOn = ui.cheapPath; cheapPathOn = ui.cheapPath;
cheapBtn.setAttribute('aria-pressed', cheapPathOn ? 'true' : 'false'); cheapBtn.setAttribute('aria-pressed', cheapPathOn ? 'true' : 'false');
} }
if(typeof ui.depLinks === 'boolean'){
depLinksOn = ui.depLinks;
depBtn.setAttribute('aria-pressed', depLinksOn ? 'true' : 'false');
}
if(typeof ui.showIds === 'boolean'){ if(typeof ui.showIds === 'boolean'){
showIds = ui.showIds; showIds = ui.showIds;
idsBtn.setAttribute('aria-pressed', showIds ? 'true' : 'false'); idsBtn.setAttribute('aria-pressed', showIds ? 'true' : 'false');
+25
View File
@@ -503,6 +503,31 @@ export function initialCollapsed(roots, rescueFocus){
return set; return set;
} }
/* ---------- Falt-Voreinstellungen des Durchschalters (SPEC §9, D75) ----------
Je Modus ein VOLLSTÄNDIGER Faltzustand als Menge der zuzuklappenden Knoten
was nicht in der Menge steht, ist offen (dieselbe Eigenschaft wie beim alten
Umschalter, D44). `cheapSet` braucht nur der Modus 'path'; er klappt jeden
Knoten zu, durch dessen Teilbaum der günstigste Pfad nicht läuft weder er
selbst noch ein Unterknoten liegt darauf. Sichtbar bleibt so genau der Pfad,
alles Übrige steht als je ein eingeklappter Knoten da. */
export function presetFoldSet(roots, mode, cheapSet){
const set = new Set();
const walk = ns => {
for(const n of ns){
if(n.children.length){
const zu = mode === 'closed' ? true
: mode === 'small' ? atMostM(n)
: mode === 'path' ? !(cheapSet.has(n) || hidesCheap(n, cheapSet))
: false; /* 'open' */
if(zu) set.add(n);
walk(n.children);
}
}
};
walk(roots);
return set;
}
/* key -> Status-Schlüssel ('' für neutrale Knoten) über den ganzen Baum. */ /* key -> Status-Schlüssel ('' für neutrale Knoten) über den ganzen Baum. */
export function statusByKey(roots){ export function statusByKey(roots){
const map = new Map(); const map = new Map();
+29 -6
View File
@@ -155,6 +155,16 @@ function nodeHtml(n, extra, opts, fold){
Querverbindungs-Ebene und den Export beide arbeiten auf dem DOM. */ Querverbindungs-Ebene und den Export beide arbeiten auf dem DOM. */
const idAttr = n.id ? ` data-id="${attr(n.id)}"` : ''; const idAttr = n.id ? ` data-id="${attr(n.id)}"` : '';
const depsAttr = n.deps && n.deps.length ? ` data-deps="${attr(n.deps.join(' '))}"` : ''; const depsAttr = n.deps && n.deps.length ? ` data-deps="${attr(n.deps.join(' '))}"` : '';
/* Eingeklappt vertritt der Knoten seinen Teilbaum auch für die
Querverbindungen (SPEC §9/D75): IDs und Abhängigkeiten der verborgenen
Knoten hängen als eigene Attribute an ihm getrennt von den eigenen,
damit die Bedeutung ablesbar bleibt. Die ID-Liste steht in
Dokumentreihenfolge: erste Vergabe gewinnt" (D36) gilt damit auch über
die Faltgrenze hinweg. */
const subIdsAttr = fold && fold.subIds.length
? ` data-sub-ids="${attr(fold.subIds.join(' '))}"` : '';
const subDepsAttr = fold && fold.subDeps.length
? ` data-sub-deps="${attr(fold.subDeps.join(' '))}"` : '';
/* Zeilen der Beschreibung (SPEC §9): Steht der Cursor dort, gilt dieser /* Zeilen der Beschreibung (SPEC §9): Steht der Cursor dort, gilt dieser
Knoten als ausgewählt die Zeile hat keinen eigenen. Als Liste, damit Knoten als ausgewählt die Zeile hat keinen eigenen. Als Liste, damit
der Attribut-Selektor `~=` sie einzeln trifft. */ der Attribut-Selektor `~=` sie einzeln trifft. */
@@ -263,8 +273,8 @@ function nodeHtml(n, extra, opts, fold){
ownChip; ownChip;
const aria = ` aria-label="${attr(nodeAria(n, opts, fold))}"`; const aria = ` aria-label="${attr(nodeAria(n, opts, fold))}"`;
const html = n.url const html = n.url
? `<a class="${cls}" href="${attr(n.url)}" target="_blank" rel="noopener"${lineAttr}${idAttr}${depsAttr}${descLinesAttr}${aria}${expanded}${title}>${inner}</a>` ? `<a class="${cls}" href="${attr(n.url)}" target="_blank" rel="noopener"${lineAttr}${idAttr}${depsAttr}${subIdsAttr}${subDepsAttr}${descLinesAttr}${aria}${expanded}${title}>${inner}</a>`
: `<div class="${cls}" tabindex="0"${lineAttr}${idAttr}${depsAttr}${descLinesAttr}${aria}${expanded}${title}>${inner}</div>`; : `<div class="${cls}" tabindex="0"${lineAttr}${idAttr}${depsAttr}${subIdsAttr}${subDepsAttr}${descLinesAttr}${aria}${expanded}${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 : '');
@@ -273,8 +283,13 @@ function nodeHtml(n, extra, opts, fold){
/* Eingeklappter Teilbaum (SPEC §9/D38): Das HTML entfällt, aber die /* Eingeklappter Teilbaum (SPEC §9/D38): Das HTML entfällt, aber die
Warnungen des verborgenen Teils werden trotzdem gemeldet sie sind eine Warnungen des verborgenen Teils werden trotzdem gemeldet sie sind eine
Aussage über den TEXT, nicht über die Ansicht. Derselbe Lauf zählt die Aussage über den TEXT, nicht über die Ansicht. Derselbe Lauf zählt die
verborgenen Knoten für das n"-Kennzeichen. */ verborgenen Knoten für das n"-Kennzeichen und sammelt deren IDs und
function walkFolded(node, warnings, opts){ Abhängigkeiten (`agg`, optional): Der eingeklappte Knoten vertritt seinen
Teilbaum auch für die Querverbindungen (SPEC §9/D75) die Kante endet am
nächsten sichtbaren Vorfahren statt zu entfallen. Ausgeblendete verworfene
Knoten laufen hier NICHT mit (visibleChildren): Der Verworfen-Filter ist
eine Aussage über den Plan, ihre Kanten entfallen weiterhin. */
function walkFolded(node, warnings, opts, agg){
const kids = visibleChildren(node, opts.showDiscarded); const kids = visibleChildren(node, opts.showDiscarded);
if(!kids.length) return 0; if(!kids.length) return 0;
const types = new Set(kids.map(k => k.type)); const types = new Set(kids.map(k => k.type));
@@ -282,7 +297,13 @@ function walkFolded(node, warnings, opts){
warnings.push({type: 'mixedGate', line: kids[0].line, label: node.label}); warnings.push({type: 'mixedGate', line: kids[0].line, label: node.label});
} }
let count = kids.length; let count = kids.length;
for(const k of kids) count += walkFolded(k, warnings, opts); for(const k of kids){
if(agg){
if(k.id) agg.ids.push(k.id); /* DFS = Dokumentreihenfolge */
if(k.deps) for(const d of k.deps) agg.deps.add(d);
}
count += walkFolded(k, warnings, opts, agg);
}
return count; return count;
} }
@@ -291,8 +312,10 @@ function itemHtml(n, extra, warnings, opts){
const vk = visibleChildren(n, opts.showDiscarded); const vk = visibleChildren(n, opts.showDiscarded);
const canFold = vk.length > 0; const canFold = vk.length > 0;
const collapsed = canFold && !!(opts.collapsedSet && opts.collapsedSet.has(n)); const collapsed = canFold && !!(opts.collapsedSet && opts.collapsedSet.has(n));
const agg = collapsed ? {ids: [], deps: new Set()} : null;
const fold = canFold const fold = canFold
? {collapsed, count: collapsed ? walkFolded(n, warnings, opts) : 0} ? {collapsed, count: collapsed ? walkFolded(n, warnings, opts, agg) : 0,
subIds: agg ? agg.ids : [], subDeps: agg ? [...agg.deps] : []}
: null; : null;
/* `opt` auch am <li>: den Abzweig zeichnen dessen Pseudoelemente, er wird /* `opt` auch am <li>: den Abzweig zeichnen dessen Pseudoelemente, er wird
für optionale Knoten gestrichelt (D29). Eingeklappt ist der Knoten ein für optionale Knoten gestrichelt (D29). Eingeklappt ist der Knoten ein
+11
View File
@@ -444,6 +444,17 @@
.copybtn.padview[data-view="both"] .ic-both, .copybtn.padview[data-view="both"] .ic-both,
.copybtn.padview[data-view="pad"] .ic-pad, .copybtn.padview[data-view="pad"] .ic-pad,
.copybtn.padview[data-view="text"] .ic-text{display:block} .copybtn.padview[data-view="text"] .ic-text{display:block}
/* Falt-Durchschalter (SPEC §9/D75): vier Icons, sichtbar ist das des
NÄCHSTEN Schritts `data-next` setzt updateFoldBtn(). Zwei Klassen wie
beim Pad-Wähler, damit die Regel `.copybtn svg{display:block}` schlägt. */
.copybtn.foldbtn .fc-small,
.copybtn.foldbtn .fc-path,
.copybtn.foldbtn .fc-closed,
.copybtn.foldbtn .fc-open{display:none}
.copybtn.foldbtn[data-next="small"] .fc-small,
.copybtn.foldbtn[data-next="path"] .fc-path,
.copybtn.foldbtn[data-next="closed"] .fc-closed,
.copybtn.foldbtn[data-next="open"] .fc-open{display:block}
/* Läuft ein Abruf, dreht das Symbol: bei gedrosselter Gegenseite (D31) können /* Läuft ein Abruf, dreht das Symbol: bei gedrosselter Gegenseite (D31) können
das die vollen 20 s sein, und ein Knopf ohne Rückmeldung wirkt kaputt. */ das die vollen 20 s sein, und ein Knopf ohne Rückmeldung wirkt kaputt. */
/* Sperre nach einem Abruf (D31): sichtbar gedämpft, aber **klickbar** der /* Sperre nach einem Abruf (D31): sichtbar gedämpft, aber **klickbar** der
+81 -1
View File
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { parse, setFoldMark } from '../src/parser.js'; import { parse, setFoldMark } from '../src/parser.js';
import { initialCollapsed, computeCheapSet, atMostM } from '../src/model.js'; import { initialCollapsed, computeCheapSet, atMostM, presetFoldSet } from '../src/model.js';
import { renderTreeHtml } from '../src/render.js'; import { renderTreeHtml } from '../src/render.js';
const t = key => key; const t = key => key;
@@ -334,3 +334,83 @@ describe('lineTargets — der eingeklappte Knoten vertritt seine Zeilen', () =>
expect(lineTargets(r, new Set(), true).get(6)).toBe(6); expect(lineTargets(r, new Set(), true).get(6)).toBe(6);
}); });
}); });
/* Falt-Voreinstellungen des Durchschalters (SPEC §9, D75): je Modus ein
VOLLSTÄNDIGER Faltzustand als Menge der zuzuklappenden Knoten. */
describe('presetFoldSet — die vier Voreinstellungen', () => {
const PLAN = `[ ] #w: Wurzel
- [ ] #a: Gross (L)
- [ ] #a1: Mittel (M)
- [ ] #a1a: Blatt (S)
- [ ] #a2: Ohne Groesse
- [ ] Blatt 2
+ [?] #b: Zugabe (M)
- [?] #b1: Blatt 3`;
const collapsed = (mode, cheapSet) => {
const r = roots(PLAN);
return [...presetFoldSet(r, mode, cheapSet ?? computeCheapSet(r))]
.map(n => n.label).sort();
};
it("'small' klappt jeden faltbaren Knoten mit angegebener Groesse <= M zu", () => {
/* `Mittel` (M) und `Zugabe` (M); `Ohne Groesse` bleibt offen keine
Autoren-Aussage (D44) und `Gross` (L) sowieso. */
expect(collapsed('small')).toEqual(['Mittel', 'Zugabe']);
});
it("'path' klappt jeden Knoten zu, durch dessen Teilbaum der Pfad nicht laeuft", () => {
/* Die unangetastete Zugabe liegt nie auf dem Pfad (D29/D61) sie ist der
einzige faltbare Knoten ohne Pfad im Teilbaum. */
expect(collapsed('path')).toEqual(['Zugabe']);
});
it("'path' laesst einen Knoten offen, sobald ein UNTERKNOTEN auf dem Pfad liegt", () => {
const r = roots(`[ ] Wurzel\n + [?] Zweig\n - [ ] #ziel: Gebraucht (S)\n - [ ] Braucht was (S) :#ziel`);
/* `#ziel` wird per Abhaengigkeit gezogen (D42) der Zweig darueber traegt
Pfad im Teilbaum und bleibt offen, obwohl er selbst nicht gebraucht wird. */
expect([...presetFoldSet(r, 'path', computeCheapSet(r))].map(n => n.label))
.toEqual([]);
});
it("'closed' klappt alle faltbaren Knoten zu, 'open' keinen", () => {
expect(collapsed('closed'))
.toEqual(['Gross', 'Mittel', 'Ohne Groesse', 'Wurzel', 'Zugabe']);
expect(collapsed('open')).toEqual([]);
});
});
/* Der eingeklappte Knoten vertritt seinen Teilbaum auch für die
Querverbindungen (SPEC §9, D75): IDs und Abhaengigkeiten der verborgenen
Knoten haengen als data-sub-ids/data-sub-deps am Vertreter. */
describe('Renderer — data-sub-ids/data-sub-deps am eingeklappten Knoten', () => {
const PLAN = `[ ] Wurzel
- [ ] #box: Zweig
- [ ] #box.a: Drin :#ziel
- [ ] #box.b: Auch drin :#ziel,#box.a
- [ ] #ziel: Draussen`;
it('sammelt IDs (Dokumentreihenfolge) und Abhaengigkeiten (dedupliziert)', () => {
const r = roots(PLAN);
const zweig = r[0].children[0];
const html = renderTreeHtml(r, {t, showDiscarded: false, cheapPath: false,
cheapSet: new Set(), collapsedSet: new Set([zweig])}).html;
expect(html).toContain('data-sub-ids="box.a box.b"');
/* #ziel und #box.a je einmal, obwohl #ziel zweimal gebraucht wird. */
expect(html).toContain('data-sub-deps="ziel box.a"');
});
it('haengt an offene Knoten keine sub-Attribute', () => {
const html = render(PLAN).html;
expect(html).not.toContain('data-sub-ids');
expect(html).not.toContain('data-sub-deps');
});
it('sammelt ausgeblendete verworfene Knoten NICHT mit — deren Kanten entfallen weiter', () => {
const r = roots(`[ ] Wurzel\n - [ ] #box: Zweig\n - [-] #weg: Verworfen :#ziel\n - [ ] #ziel: Draussen`);
const zweig = r[0].children[0];
const html = renderTreeHtml(r, {t, showDiscarded: false, cheapPath: false,
cheapSet: new Set(), collapsedSet: new Set([zweig])}).html;
expect(html).not.toContain('data-sub-ids');
expect(html).not.toContain('data-sub-deps');
});
});