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:
co-authored by
Claude Fable 5
parent
69c9be7a6d
commit
33443016ce
@@ -19,6 +19,9 @@ reverse.
|
||||
|
||||
## 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
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
`navigator.brave`-Stub zur Laufzeit): mit Stub trägt das Banner die zweite
|
||||
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
@@ -1083,19 +1083,32 @@ fokussierten Knoten (WAI-ARIA-Baum-Idiom).
|
||||
weil eine Fokusmarke `!!!` ihren Knoten immer wieder hervorholt): Dann wird
|
||||
lieber nichts geschrieben, als einen Text zu hinterlassen, der etwas anderes
|
||||
sagt als das Bild.
|
||||
- **Für den ganzen Baum** gibt es im Diagramm-Kopf einen **Umschalter**:
|
||||
gedrückt ist alles **ab Größe M abwärts** zugeklappt, nicht gedrückt ist der
|
||||
ganze Baum **offen**. Betroffen ist jeder Knoten mit Kindern, dessen
|
||||
**angegebene** Größe (§5) `M` oder kleiner ist — offen bleiben `L`, `XL`,
|
||||
`XXL`. Ein Knoten **ohne** Größenangabe wird nicht zugeklappt: Der günstigste
|
||||
Pfad rechnet fehlende Größen zwar als `M` (unten), das ist aber eine
|
||||
Kostenannahme und keine Aussage des Autors.
|
||||
- Beide Stellungen beschreiben einen **vollständigen** Faltzustand — was der
|
||||
Umschalter nicht zuklappt, ist danach offen. Sein Zustand wird **nicht
|
||||
gemerkt, sondern am Baum abgelesen**: Wer danach einen Knoten von Hand
|
||||
umklappt, sieht ihn von selbst herausspringen. Geschrieben wird wie beim
|
||||
einzelnen Umklappen — ein einziger Rückgängig-Schritt für den ganzen
|
||||
Vorgang. Siehe D44.
|
||||
- **Für den ganzen Baum** gibt es im Diagramm-Kopf einen **Durchschalter**:
|
||||
Jeder Druck stellt die **nächste** von vier Voreinstellungen her, reihum —
|
||||
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`, `XXL`. Ein Knoten **ohne** Größenangabe wird nicht zugeklappt:
|
||||
Der günstigste Pfad schätzt fehlende Größen zwar (unten), das ist aber
|
||||
eine Kostenannahme und keine Aussage des Autors.
|
||||
2. **alles abseits des günstigsten Pfads zugeklappt**: jeder Knoten, durch
|
||||
dessen Teilbaum der günstigste Pfad (§9) **nicht** läuft — weder er
|
||||
selbst noch ein Unterknoten liegt auf ihm. Der Pfad bleibt vollständig
|
||||
sichtbar, alles Übrige steht als je ein eingeklappter Knoten da.
|
||||
Gerechnet wird dafür auch bei **ausgeschaltetem** Pfad-Umschalter — die
|
||||
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
|
||||
weiter gemeldet (sie gelten dem Text), und der günstigste Pfad rechnet
|
||||
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
|
||||
der Knoten der Cursor-Zeile — sind in Tinte hervorgehoben, ein- wie
|
||||
ausgehende; sie liegen über den übrigen Kanten, aber unter jedem Knoten.
|
||||
- Kanten zu gerade nicht sichtbaren Knoten (eingeklappt §9, verworfen
|
||||
ausgeblendet §4) entfallen; bei doppelter ID zielt die Kante auf die erste
|
||||
Vergabe (D36/D39).
|
||||
- Die Basis-Kanten erscheinen auch im **Grafikexport** und im **Druck**; die
|
||||
Hervorhebung ist Interaktion und erscheint nicht. Siehe D41.
|
||||
- **Eingeklappte Knoten vertreten ihre Teilbäume auch hier** (§9, Falten):
|
||||
Liegt ein Endpunkt — Quelle wie Ziel — in einem eingeklappten Teilbaum,
|
||||
endet die Kante am nächsten **sichtbaren Vorfahren**. Fallen beide
|
||||
Endpunkte in denselben sichtbaren Knoten zusammen, entfällt die Kante (sie
|
||||
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
|
||||
Das Diagramm wird aus der Live-Geometrie in ein eigenständiges SVG (nur Formen
|
||||
|
||||
@@ -42,6 +42,8 @@
|
||||
- [^] #ed.deplink: Cross links for dependencies (M)
|
||||
- [^] #ed.deplink.calm: Visually secondary — the tree carries the message (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.cost: Cost model, a size prices its subtree (S)
|
||||
- [^] #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.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.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.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)
|
||||
@@ -397,6 +399,16 @@
|
||||
The selected node — keyboard focus in the diagram, otherwise the node of the
|
||||
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
|
||||
Highlight the cheapest way through the tree: with and, everything; with or,
|
||||
the cheapest alternative. This is the feature the whole notation exists for.
|
||||
@@ -590,9 +602,11 @@
|
||||
restores exactly what you were looking at.
|
||||
|
||||
#ed.fold.preset
|
||||
One switch puts the whole tree at working height: everything of size M and
|
||||
smaller closed, L and above open. Size rather than depth — depth only says
|
||||
how finely somebody happened to write it down.
|
||||
One button cycles the whole tree through four complete fold states: size M
|
||||
and smaller closed, everything off the cheapest path closed, everything
|
||||
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
|
||||
A safety net against mishaps: the text is put aside every ten minutes, but
|
||||
|
||||
+14
-4
@@ -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">
|
||||
<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>
|
||||
<!-- 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
|
||||
Umschalter, sondern eine Bewegung: jeder Druck holt die nächste noch
|
||||
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
|
||||
Kopfzeile gewandert: Er sagt jetzt nicht nur etwas über das aktive
|
||||
Dokument, sondern über das Produkt — und ist deshalb immer da. -->
|
||||
<!-- Faltung für den ganzen Baum (SPEC §9, D44): ein Umschalter wie die
|
||||
beiden Nachbarn. Gedrückt = alles ab Größe M abwärts ist zugeklappt. -->
|
||||
<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">
|
||||
<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>
|
||||
<!-- Faltung für den ganzen Baum (SPEC §9, D44/D75): ein DURCHSCHALTER —
|
||||
jeder Druck stellt die nächste von vier Voreinstellungen her. Icon
|
||||
und Tooltip zeigen den NÄCHSTEN Schritt (was Drücken tun wird);
|
||||
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>
|
||||
<!-- Knoten-IDs vor dem Titel einblenden (SPEC §9, D56) — geschrieben wie
|
||||
im Text: `#some.id: Titel`. Das Zeichen selbst ist das Symbol; ein
|
||||
|
||||
+146
-47
@@ -1,6 +1,6 @@
|
||||
import './style.css';
|
||||
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 { formatWarning, warningText } from './warnings.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
|
||||
nur der UI-State des Günstigster-Pfad-Toggles (persistiert). */
|
||||
let cheapPathOn = true;
|
||||
let depLinksOn = true; /* Querverbindungen der Abhängigkeiten anzeigen (D75) */
|
||||
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
|
||||
deklariert, weil saveUI() sie liest und schon aus applySplit() heraus laufen
|
||||
kann — weiter unten stünde sie dann noch in der temporalen Todeszone. */
|
||||
@@ -167,25 +175,29 @@ function render(){
|
||||
? freshProdSet(freshPrevRoots, roots) : new Set();
|
||||
}
|
||||
const collapsedSet = new Set();
|
||||
let foldSmallExact = true, foldSmallAny = false;
|
||||
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) => {
|
||||
const ov = foldOverrides.get(key);
|
||||
const collapsed = (ov !== undefined ? ov : initFold.has(n)) && n.children.length > 0;
|
||||
if(collapsed) collapsedSet.add(n);
|
||||
foldByLine.set(n.line, {key, collapsed, canFold: n.children.length > 0});
|
||||
/* Der Umschalter (D44) merkt sich nichts, er liest ab: gedrückt genau
|
||||
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;
|
||||
}
|
||||
if(presetSet && n.children.length > 0 && collapsed !== presetSet.has(n)) presetMatch = false;
|
||||
});
|
||||
/* `foldSmallAny`: Gäbe es gar keinen Knoten bis M, wären „zugeklappt" und
|
||||
„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');
|
||||
if(presetSet && !presetMatch){ foldCycleApplied = -1; foldCycleNext = 0; }
|
||||
/* Aus DENSELBEN Mengen wie das Rendern — die Map muss dieselbe Faltung
|
||||
beschreiben, die gleich im DOM steht (dieselbe Regel wie bei freshSet). */
|
||||
lineTargetMap = lineTargets(roots, collapsedSet, showDiscarded);
|
||||
@@ -223,6 +235,7 @@ function render(){
|
||||
revealFocusMark(); /* `!!!` ins Bild holen, wenn die Marke neu ist (SPEC §1) */
|
||||
updateFreshBtn(); /* Zähler folgt der gerade gerenderten Menge (D28) */
|
||||
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 —
|
||||
@@ -409,16 +422,32 @@ function drawCheapPath(){
|
||||
fokussierten Knotens bzw. der Cursor-Zeile liegen hervorgehoben in Tinte
|
||||
auf einer vorderen Ebene. */
|
||||
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();
|
||||
out.querySelectorAll('.node[data-id]').forEach(el => {
|
||||
if(!byId.has(el.dataset.id)) byId.set(el.dataset.id, el); /* erste Vergabe gewinnt (D36) */
|
||||
out.querySelectorAll('.node[data-id], .node[data-sub-ids]').forEach(el => {
|
||||
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 = [];
|
||||
out.querySelectorAll('.node[data-deps]').forEach(el => {
|
||||
for(const d of el.dataset.deps.split(' ')){
|
||||
const edges = [], seen = new Set();
|
||||
out.querySelectorAll('.node[data-deps], .node[data-sub-deps]').forEach(el => {
|
||||
const deps = ((el.dataset.deps || '') + ' ' + (el.dataset.subDeps || ''))
|
||||
.split(' ').filter(Boolean);
|
||||
for(const d of deps){
|
||||
const target = byId.get(d);
|
||||
/* verborgene (eingeklappte/ausgeblendete) oder unbekannte Ziele: keine Kante */
|
||||
if(target && target !== el) edges.push([el, target]);
|
||||
/* ausgeblendete (verworfene) oder unbekannte Ziele: keine Kante; beide
|
||||
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;
|
||||
@@ -464,6 +493,7 @@ function activeDepNode(){
|
||||
}
|
||||
function drawDepLinks(){
|
||||
out.querySelectorAll('svg.dep-overlay').forEach(e => e.remove());
|
||||
if(!depLinksOn) return; /* Umschalter (SPEC §9/D75), Voreinstellung an */
|
||||
const edges = depEdges();
|
||||
if(!edges.length) return;
|
||||
const outRect = out.getBoundingClientRect();
|
||||
@@ -656,8 +686,9 @@ function diagramToSvg(){
|
||||
});
|
||||
|
||||
/* 1a) Querverbindungen der Abhängigkeiten (D41) — Basis-Kanten hinter den
|
||||
Knoten; die Fokus-Hervorhebung ist Interaktion und wird nicht exportiert. */
|
||||
depEdges().forEach(([from, to]) => {
|
||||
Knoten; die Fokus-Hervorhebung ist Interaktion und wird nicht exportiert.
|
||||
Der Export folgt dem Umschalter (D75) wie den übrigen Ansichts-Filtern. */
|
||||
(depLinksOn ? depEdges() : []).forEach(([from, 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="${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));
|
||||
}
|
||||
|
||||
/* Voreinstellungen aus dem Diagramm-Kopf (SPEC §9, D44): den ganzen Baum auf
|
||||
einmal auf- bzw. bis unter Größe M zuklappen. Gesetzt wird über dieselben
|
||||
Sitzungs-Überlagerungen wie beim einzelnen Umklappen und danach in den Text
|
||||
geschrieben — es ist derselbe Vorgang, nur für viele Knoten. Deshalb auch
|
||||
EIN Undo-Schritt: `replaceTextUndoable` schreibt genau einmal. */
|
||||
/* Voreinstellungen aus dem Diagramm-Kopf (SPEC §9, D44/D75): den ganzen Baum
|
||||
auf einmal in einen der vier Durchschalter-Zustände stellen. Gesetzt wird
|
||||
über dieselben Sitzungs-Überlagerungen wie beim einzelnen Umklappen und
|
||||
danach in den Text geschrieben — es ist derselbe Vorgang, nur für viele
|
||||
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){
|
||||
const roots = parse(src.value).roots;
|
||||
if(!roots.length) return;
|
||||
const cs = mode === 'path' ? computeCheapPlan(roots).set : null;
|
||||
const want = presetFoldSet(roots, mode, cs);
|
||||
nodeKeys(roots).forEach((key, n) => {
|
||||
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();
|
||||
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'); }
|
||||
showc.addEventListener('click', () => { setDiscarded(!discardedShown()); render(); saveUI(); });
|
||||
|
||||
/* Faltung für den ganzen Baum (SPEC §9, D44): ein Umschalter wie die beiden
|
||||
Nachbarn. Gedrückt heißt „alles unter Größe M ist zugeklappt". Der Zustand
|
||||
wird nicht gemerkt, sondern bei jedem Rendern am Baum ABGELESEN
|
||||
(`syncFoldSmallBtn`) — klappt jemand danach einen Knoten von Hand um, geht
|
||||
der Knopf von selbst heraus, statt etwas zu behaupten, das nicht mehr
|
||||
stimmt. Deshalb auch nicht in `werkbaum-ui` persistiert: Der Faltzustand
|
||||
steht im Text (D38-Nachtrag 2), der Knopf liest ihn nur ab. */
|
||||
const foldSmallBtn = document.getElementById('foldSmallBtn');
|
||||
foldSmallBtn.addEventListener('click', () => {
|
||||
applyFoldPreset(foldSmallBtn.getAttribute('aria-pressed') === 'true' ? 'all' : 'small');
|
||||
/* Faltung für den ganzen Baum (SPEC §9, D44/D75): ein DURCHSCHALTER — jeder
|
||||
Druck stellt die nächste von vier Voreinstellungen her (ab M abwärts zu →
|
||||
alles abseits des Pfads zu → alles zu → alles offen → wieder vorn). Icon
|
||||
und Tooltip zeigen den NÄCHSTEN Schritt, also was Drücken tun wird — den
|
||||
Zustand hat man vor sich (dieselbe Logik wie beim Bereichs-Umschalter,
|
||||
D17). Die Position wird nicht persistiert; render() prüft sie gegen den
|
||||
Baum und setzt sie zurück, sobald jemand von Hand umklappt (D44-Linie). */
|
||||
const foldBtn = document.getElementById('foldBtn');
|
||||
foldBtn.addEventListener('click', () => {
|
||||
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 */
|
||||
const cheapBtn = document.getElementById('cheapBtn');
|
||||
@@ -2032,6 +2078,18 @@ cheapBtn.addEventListener('click', () => {
|
||||
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
|
||||
`aria-pressed`; der Zustand gehört zur Ansicht und wird wie Modus, Zoom und
|
||||
Aufteilung global gemerkt (D22). Neu gerendert statt per CSS versteckt —
|
||||
@@ -2175,7 +2233,11 @@ const I18N = {
|
||||
discardedTooltip:"Verworfene Knoten samt Teilbaum ein-/ausblenden",
|
||||
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)",
|
||||
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",
|
||||
fullscreenTooltip:"Vollbild – Panels nutzen die ganze Fensterbreite",
|
||||
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",
|
||||
cheapTooltip:"Highlight the cheapest path – unneeded alternatives recede",
|
||||
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",
|
||||
fullscreenTooltip:"Full screen – panels use the full window width",
|
||||
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",
|
||||
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)",
|
||||
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",
|
||||
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).",
|
||||
@@ -2500,7 +2570,11 @@ const I18N = {
|
||||
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",
|
||||
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",
|
||||
fullscreenTooltip:"Plein écran – les panneaux occupent toute la largeur de la fenêtre",
|
||||
brandTooltip:"« Werkbaum » signifie à peu près « arbre de travail » — l’arbre de l’organigramme des tâches (WBS).",
|
||||
@@ -2608,7 +2682,11 @@ const I18N = {
|
||||
discardedTooltip:"Pokaż/ukryj odrzucone węzły wraz z poddrzewem",
|
||||
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)",
|
||||
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}",
|
||||
fullscreenTooltip:"Pełny ekran – panele wykorzystują całą szerokość okna",
|
||||
brandTooltip:"„Werkbaum” znaczy mniej więcej ‚drzewo pracy’ — drzewo struktury podziału pracy (WBS).",
|
||||
@@ -2716,7 +2794,11 @@ const I18N = {
|
||||
discardedTooltip:"Показать/скрыть отклонённые узлы вместе с поддеревом",
|
||||
cheapTooltip:"Выделить самый дешёвый путь — ненужные альтернативы приглушаются",
|
||||
leanNextTooltip:"Перейти к следующей станции самого дешёвого пути — что делать дальше ({n} открыто)",
|
||||
foldSmallTooltip:"Свернуть узлы размера M и меньше — нажмите ещё раз, чтобы развернуть все",
|
||||
depsTooltip:"Показать связи зависимостей",
|
||||
foldCycle_small:"Свернуть узлы размера M и меньше",
|
||||
foldCycle_path:"Свернуть всё вне самого дешёвого пути",
|
||||
foldCycle_closed:"Свернуть все узлы",
|
||||
foldCycle_open:"Развернуть все узлы",
|
||||
implicitSizeTooltip:"Размер не указан — для оценки затрат принят не меньше {size}",
|
||||
fullscreenTooltip:"Полный экран – панели занимают всю ширину окна",
|
||||
brandTooltip:"«Werkbaum» примерно означает ‚дерево работ’ — дерево структуры декомпозиции работ (СДР).",
|
||||
@@ -2824,7 +2906,11 @@ const I18N = {
|
||||
discardedTooltip:"अस्वीकृत नोड्स और उनके उप-वृक्ष दिखाएँ/छिपाएँ",
|
||||
cheapTooltip:"सबसे किफ़ायती पथ को उजागर करें – अनावश्यक विकल्प मंद हो जाते हैं",
|
||||
leanNextTooltip:"सबसे किफ़ायती पथ के अगले पड़ाव पर जाएँ – अगला काम ({n} शेष)",
|
||||
foldSmallTooltip:"आकार M और उससे छोटे नोड समेटें – सभी को खोलने के लिए फिर दबाएँ",
|
||||
depsTooltip:"निर्भरता के आड़े संबंध दिखाएँ",
|
||||
foldCycle_small:"आकार M और उससे छोटे नोड समेटें",
|
||||
foldCycle_path:"सबसे किफ़ायती पथ से बाहर सब कुछ समेटें",
|
||||
foldCycle_closed:"सभी नोड समेटें",
|
||||
foldCycle_open:"सभी नोड खोलें",
|
||||
implicitSizeTooltip:"कोई आकार नहीं दिया गया – लागत अनुमान के लिए कम से कम {size} माना गया",
|
||||
fullscreenTooltip:"पूर्ण स्क्रीन – पैनल पूरी विंडो चौड़ाई का उपयोग करते हैं",
|
||||
brandTooltip:"„Werkbaum“ का अर्थ लगभग ‚कार्य-वृक्ष‘ है — कार्य विभाजन संरचना (WBS) का वृक्ष।",
|
||||
@@ -2917,7 +3003,11 @@ const I18N = {
|
||||
discardedTooltip:"显示/隐藏已放弃的节点及其子树",
|
||||
cheapTooltip:"突出显示成本最低的路径——不需要的备选项将淡化",
|
||||
leanNextTooltip:"跳到成本最低路径的下一站——接下来该做的事(还有 {n} 项)",
|
||||
foldSmallTooltip:"折叠尺寸 M 及更小的节点——再次点击可全部展开",
|
||||
depsTooltip:"显示依赖关系连线",
|
||||
foldCycle_small:"折叠尺寸 M 及更小的节点",
|
||||
foldCycle_path:"折叠最低成本路径之外的所有节点",
|
||||
foldCycle_closed:"折叠所有节点",
|
||||
foldCycle_open:"展开所有节点",
|
||||
implicitSizeTooltip:"未指定尺寸——成本估算时按至少 {size} 计",
|
||||
ghostTooltip:"从 M 号起,元素应进一步细分。",
|
||||
jumpHint:"Alt+点击:跳转到文本中的该行",
|
||||
@@ -3025,7 +3115,11 @@ const I18N = {
|
||||
discardedTooltip:"破棄したノードとその下位ツリーを表示/非表示",
|
||||
cheapTooltip:"最も低コストの経路を強調 – 不要な選択肢は控えめに表示",
|
||||
leanNextTooltip:"最も低コストの経路の次の駅へ移動 – 次にやること(残り {n} 件)",
|
||||
foldSmallTooltip:"サイズ M 以下のノードを折りたたむ – もう一度押すとすべて展開",
|
||||
depsTooltip:"依存関係のリンクを表示",
|
||||
foldCycle_small:"サイズ M 以下のノードを折りたたむ",
|
||||
foldCycle_path:"最も低コストの経路以外をすべて折りたたむ",
|
||||
foldCycle_closed:"すべてのノードを折りたたむ",
|
||||
foldCycle_open:"すべてのノードを展開",
|
||||
implicitSizeTooltip:"サイズ未指定 – コスト見積もりのため少なくとも {size} として扱う",
|
||||
ghostTooltip:"サイズ M 以上の要素はさらに分解すべきです。",
|
||||
jumpHint:"Alt+クリック:テキストの該当行へ移動",
|
||||
@@ -3367,6 +3461,7 @@ function saveUI(){
|
||||
mode: modeEl ? modeEl.value : 'horizontal',
|
||||
discarded: discardedShown(),
|
||||
cheapPath: cheapPathOn,
|
||||
depLinks: depLinksOn,
|
||||
showIds,
|
||||
split: splitState,
|
||||
col: app.style.getPropertyValue('--col') || null,
|
||||
@@ -3406,6 +3501,10 @@ function restoreState(){
|
||||
cheapPathOn = ui.cheapPath;
|
||||
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'){
|
||||
showIds = ui.showIds;
|
||||
idsBtn.setAttribute('aria-pressed', showIds ? 'true' : 'false');
|
||||
|
||||
@@ -503,6 +503,31 @@ export function initialCollapsed(roots, rescueFocus){
|
||||
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. */
|
||||
export function statusByKey(roots){
|
||||
const map = new Map();
|
||||
|
||||
+29
-6
@@ -155,6 +155,16 @@ function nodeHtml(n, extra, opts, fold){
|
||||
Querverbindungs-Ebene und den Export — beide arbeiten auf dem DOM. */
|
||||
const idAttr = n.id ? ` data-id="${attr(n.id)}"` : '';
|
||||
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
|
||||
Knoten als ausgewählt — die Zeile hat keinen eigenen. Als Liste, damit
|
||||
der Attribut-Selektor `~=` sie einzeln trifft. */
|
||||
@@ -263,8 +273,8 @@ function nodeHtml(n, extra, opts, fold){
|
||||
ownChip;
|
||||
const aria = ` aria-label="${attr(nodeAria(n, opts, fold))}"`;
|
||||
const html = n.url
|
||||
? `<a class="${cls}" href="${attr(n.url)}" target="_blank" rel="noopener"${lineAttr}${idAttr}${depsAttr}${descLinesAttr}${aria}${expanded}${title}>${inner}</a>`
|
||||
: `<div class="${cls}" tabindex="0"${lineAttr}${idAttr}${depsAttr}${descLinesAttr}${aria}${expanded}${title}>${inner}</div>`;
|
||||
? `<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}${subIdsAttr}${subDepsAttr}${descLinesAttr}${aria}${expanded}${title}>${inner}</div>`;
|
||||
const ghostTip = attr(t('ghostTooltip'));
|
||||
const ghost = `<div class="ghost-node" aria-label="${ghostTip}" title="${ghostTip}">${esc(t('ghost'))}</div>`;
|
||||
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
|
||||
Warnungen des verborgenen Teils werden trotzdem gemeldet — sie sind eine
|
||||
Aussage über den TEXT, nicht über die Ansicht. Derselbe Lauf zählt die
|
||||
verborgenen Knoten für das „▸ n"-Kennzeichen. */
|
||||
function walkFolded(node, warnings, opts){
|
||||
verborgenen Knoten für das „▸ n"-Kennzeichen und sammelt deren IDs und
|
||||
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);
|
||||
if(!kids.length) return 0;
|
||||
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});
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -291,8 +312,10 @@ function itemHtml(n, extra, warnings, opts){
|
||||
const vk = visibleChildren(n, opts.showDiscarded);
|
||||
const canFold = vk.length > 0;
|
||||
const collapsed = canFold && !!(opts.collapsedSet && opts.collapsedSet.has(n));
|
||||
const agg = collapsed ? {ids: [], deps: new Set()} : null;
|
||||
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;
|
||||
/* `opt` auch am <li>: den Abzweig zeichnen dessen Pseudoelemente, er wird
|
||||
für optionale Knoten gestrichelt (D29). Eingeklappt ist der Knoten ein
|
||||
|
||||
@@ -444,6 +444,17 @@
|
||||
.copybtn.padview[data-view="both"] .ic-both,
|
||||
.copybtn.padview[data-view="pad"] .ic-pad,
|
||||
.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
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
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';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
/* 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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user