feat(diagram): Knoten-Fenster öffnet nur noch auf Klick, nicht mehr bei Hover (D92, SPEC §6/§9)

Der einfache Klick toggelt das Fenster überall — wie der Tipp auf Touch
(D52); der Link eines Knotens ist damit auch am Zeiger der ↗-Knopf im
Fenster, Enter und Strg+Klick öffnen die URL weiterhin direkt. Tastatur-
fokus öffnet nur noch echten (:focus-visible), focusout schließt nicht in
das Fenster hinein, und die Fokus-Rückgabe nach dem Falten ruft das
Fenster ausdrücklich nie.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-28 16:43:01 +02:00
co-authored by Claude Fable 5
parent a223e6af57
commit 0fe4c07851
5 changed files with 154 additions and 71 deletions
+2
View File
@@ -19,6 +19,8 @@ reverse.
## 2026-08-28 ## 2026-08-28
- The node window no longer opens on hover: click a node to open it, click again or elsewhere to close — folding and moving across the tree stay snappy
- A linked node's plain click now opens the window everywhere, like on touch — the URL sits behind the ↗ button, and Enter on the focused node or Ctrl+click still open it directly
- The Taiga buttons in the node window sit side by side in one row now — the window grows a little wider for them, and they wrap only when space truly runs out - The Taiga buttons in the node window sit side by side in one row now — the window grows a little wider for them, and they wrap only when space truly runs out
- The open button moved into the ticket box's action row, next to write and take, and its label shrank to "Open #US-123" — the neighbours say where - The open button moved into the ticket box's action row, next to write and take, and its label shrank to "Open #US-123" — the neighbours say where
- "Link ticket" in the node window binds an existing Taiga story or task to a node: paste the ticket's URL — it carries project, type and number by itself — or enter a ref or bare number - "Link ticket" in the node window binds an existing Taiga story or task to a node: paste the ticket's URL — it carries project, type and number by itself — or enter a ref or bare number
+65
View File
@@ -8568,3 +8568,68 @@ längsten Labels): drei Anlege-Knöpfe eine Zeile, „→ nach Taiga schreiben
Ref-Knoten ohne Projekt-Slug (keine Knöpfe) fällt auf 336px zurück — Ref-Knoten ohne Projekt-Slug (keine Knöpfe) fällt auf 336px zurück —
`tipwide` ist weg. 628 Tests unverändert (reine UI-Verdrahtung, `tipwide` ist weg. 628 Tests unverändert (reine UI-Verdrahtung,
Browser-geprüft — die D54-Nachtrag-3-Grenze). Browser-geprüft — die D54-Nachtrag-3-Grenze).
## D92 — Das Knoten-Fenster öffnet nur noch auf Klick, nicht mehr beim Überfahren
Gemeldet vom Nutzer: „Mit mehr Funktionalität in dem Popup, so dass es auch
träger geworden ist als der einfache Tooltip vorher, gibt es ein
Usability-Problem: Wenn man nur einen Knoten auf-/zuklappen will, kommt schon
das Popup." Das trifft die Sache: D57 hat dem Fenster den Hover gegeben, als
es noch ein besserer Tooltip war — Beschreibung plus Kurz-Fakten. Seit D91
trägt es Ticket-Stand, Anlege-, Verknüpfen- und Abgleich-Knöpfe und ist
breiter geworden (`tipwide`, D91-Nachtrag 12); beim Ansteuern des Falt-Chips
stand es regelmäßig schon im Weg. Ein Fenster mit Aktionen ist kein Tooltip
mehr — es verdient eine ausdrückliche Geste.
**Der einfache Klick öffnet, ein zweiter (oder daneben) schließt** — wörtlich
die Touch-Regel aus D52, jetzt überall. Damit fällt zwangsläufig eine
Entscheidung über die **verlinkten Knoten** (§6: der einfache Klick öffnete
dort die URL): Eine Geste kann nicht beides tun, und D25 hat ausdrücklich
abgelehnt, dass derselbe Klick je nach Knoten Verschiedenes tut. Also gilt
die D52-Abwägung jetzt auch am Zeiger — „ansehen ist die häufigere Absicht,
und der Link bleibt einen Klick entfernt": Der **↗-Knopf steht in jedem
Fenster eines verlinkten Knotens**, nicht mehr nur auf Touch. Direkt zur URL
führen weiterhin **Enter** am fokussierten Knoten (der native Weg; im
Klick-Handler an `e.detail === 0` erkannt und durchgelassen) und
**Strg+Klick** (Browser-Default, Hintergrund-Tab — auf Zeilen mit
Ticket-Referenz gewinnt das Taiga-Öffnen, D91-Nachtrag 5, wie bisher).
Alt+Klick (Sprung, D25) und der Falt-Chip (D38) sind im Handler ausgenommen
und bleiben, was sie sind.
**Tastaturfokus öffnet weiter sofort — aber nur echten.** Das war die
verdeckte Falle des Umbaus: Auch der Mausklick fokussiert den Knoten, und der
`focusin`-Handler (D57) hätte das Fenster geöffnet, bevor der Klick-Toggle es
für „schon offen" hielte und sofort wieder schlösse — netto bliebe es zu.
Unterschieden wird mit `:focus-visible`, der Browser-eigenen Heuristik, die
D25-Nachtrag 2 für genau diese Frage schon vermessen hat (nach Mausklick
falsch, nach Tab wahr). Nebenwirkung, bewusst hingenommen: Auch der
programmatische Fokus nach Alt+Klick aus dem Text und nach dem
Stationen-Knopf (D47) folgt der Heuristik — nach einer Maus-Geste öffnet dort
kein Fenster mehr mit. Das passt zur Stoßrichtung dieses Eintrags: weniger
ungefragte Fenster.
**Und `focusout` darf den Knopf-Klick nicht mehr töten.** Mit dem Klick ist
der Knoten beim offenen Fenster jetzt regelmäßig **fokussiert** — vorher
(Hover) war er das nicht, und der `focusout`-Schließer war harmlos. Die
Knöpfe im Fenster sind trotz `tabindex="-1"` klick-fokussierbar: mousedown
auf einen Taiga-Knopf blurte den Knoten, `focusout` schlösse das Fenster
zwischen mousedown und mouseup, und der Klick ginge an einem
`display:none`-Element verloren. Deshalb schließt `focusout` nicht, wenn
`relatedTarget` im Fenster liegt; Tab zum nächsten Knoten schließt weiter.
**Ersatzlos weg:** die 350-ms-Öffnungs- und 120-ms-Schließ-Timer, die
Sofort-Weiterreiche bei schon offenem Fenster, `pointerenter`/`pointerleave`
am Fenster und die `finePointer`-Weiche — das Fenster ist jetzt auf allen
Geräten dieselbe Klick-Sache, und ein einmal geöffnetes bleibt stehen, bis
etwas es ausdrücklich schließt (zweiter Klick, Klick daneben, Esc, ×,
Scrollen, Neubau — die D52-Liste, unverändert). Die 400-ms-Verzögerung des
Ticket-Abrufs (D91-Nachtrag 6) bleibt: Sie schützt jetzt das Durch-Tabben
statt des Vorüberfahrens, und der Bulk-Cache (D91-Nachtrag 10) macht sie
ohnehin meist unsichtbar.
**Der Preis, benannt:** Beim schnellen Überfliegen eines fremden Plans
zeigte der Hover die Beschreibungen ohne Klick — das ist weg, je Knoten
kostet es jetzt einen Klick. Die ”-Marke (D40) und der ↗ hinter dem Label
sagen weiterhin, wo sich einer lohnt. Und ein Doppelklick öffnet und
schließt in einem Zug — hinnehmbar, auf Knoten doppelklickt niemand
absichtlich.
+29 -23
View File
@@ -414,13 +414,16 @@ Größe als **Bereich** gelesen (Untergrenzen verdoppeln sich):
## 6. Links ## 6. Links
- Ein nacktes `https://…`-Token macht den ganzen Knoten klickbar - Ein nacktes `https://…`-Token verlinkt den Knoten; Kennzeichnung mit ↗
(neuer Tab, `rel="noopener"`); Kennzeichnung mit ↗ hinter dem Label. hinter dem Label.
- **Ausnahme auf Touch-Geräten:** Dort öffnet der einfache Tipp nicht die URL, - Geöffnet wird die URL über den **↗-Knopf im Knoten-Fenster** (§9; neuer
sondern das Knoten-Fenster (§9) — der Link steht darin als eigener Knopf. Ohne Tab, `rel="noopener"`): Der einfache Klick wie der einfache Tipp öffnen das
Zeiger gibt es keinen Tooltip, und ein einziger Tipp kann nicht beides tun; **Fenster**, nicht die URL — eine Geste kann nicht beides tun, „ansehen"
„ansehen" ist die häufigere Absicht, und der Link bleibt einen Tipp entfernt. ist die häufigere Absicht, und der Link bleibt einen Klick entfernt
Siehe D52. (D52 für Touch, D92 für den Zeiger). **Enter** am fokussierten Knoten und
**Strg+Klick** (Browser-Default, Hintergrund-Tab) öffnen die URL weiterhin
direkt — auf Zeilen mit Ticket-Referenz gewinnt bei Strg+Klick das
Taiga-Öffnen (§9).
## 7. Personen-Tags ## 7. Personen-Tags
@@ -469,11 +472,13 @@ Grafikexport: Der Text selbst kann dort nicht angezeigt werden, eine Marke
ohne Ziel wäre Rauschen. Siehe D40. ohne Ziel wäre Rauschen. Siehe D40.
**Gezeigt wird das immer im eigenen Knoten-Fenster**, nicht im Tooltip des **Gezeigt wird das immer im eigenen Knoten-Fenster**, nicht im Tooltip des
Browsers: am Zeiger nach kurzer Verzögerung beim Überfahren, bei Tastaturfokus Browsers: beim **einfachen Klick bzw. Tipp** auf den Knoten (ein zweiter
sofort, auf Touch beim einfachen Tipp (§6). Ein `title` kann weder Absätze noch schließt), bei **Tastaturfokus** sofort. Beim bloßen Überfahren mit dem
eine Linie und erscheint nie beim Tastaturfokus; das Fenster kann beides. Der Zeiger öffnet es **nicht** — das tat es zwischenzeitlich (D57) und stand
Inhalt ist derselbe — die Beschreibung als Absatz, die Kurz-Fakten dahinter damit dem Falten und dem Bewegen über den Baum im Weg (D92). Ein `title`
durch eine echte Linie abgesetzt. Siehe D57. kann weder Absätze noch eine Linie und erscheint nie beim Tastaturfokus; das
Fenster kann beides. Der Inhalt ist derselbe — die Beschreibung als Absatz,
die Kurz-Fakten dahinter durch eine echte Linie abgesetzt. Siehe D57, D92.
- Das Fenster hängt am angetippten Knoten (kleine Spitze zu ihm hin) und weicht - Das Fenster hängt am angetippten Knoten (kleine Spitze zu ihm hin) und weicht
nach oben aus, wenn unten kein Platz ist. Der Knoten trägt, solange es offen nach oben aus, wenn unten kein Platz ist. Der Knoten trägt, solange es offen
@@ -481,13 +486,12 @@ durch eine echte Linie abgesetzt. Siehe D57.
- Es schließt beim Tipp daneben, beim erneuten Tipp auf denselben Knoten, mit - Es schließt beim Tipp daneben, beim erneuten Tipp auf denselben Knoten, mit
**Esc**, über sein ×, beim Scrollen des Diagramms und bei jedem Neubau. **Esc**, über sein ×, beim Scrollen des Diagramms und bei jedem Neubau.
- Ein **verlinkter** Knoten (§6) bekommt darin einen ↗-Knopf, der die URL im - Ein **verlinkter** Knoten (§6) bekommt darin einen ↗-Knopf, der die URL im
neuen Tab öffnet — auf Touch ist das der Weg zum Link. neuen Tab öffnet — der einfache Klick/Tipp gehört dem Fenster (§6, D92).
- Der **Sprung-Hinweis** nennt hier den langen Druck statt Alt+Klick (§9, - Der **Sprung-Hinweis** nennt auf Touch den langen Druck statt Alt+Klick (§9,
Sprung zwischen Diagramm und Text) — Alt gibt es auf dem Telefon nicht. Sprung zwischen Diagramm und Text) — Alt gibt es auf dem Telefon nicht.
- Reine Bedienhilfe: nicht im Grafikexport, nicht im Druck. Auf Geräten mit - Reine Bedienhilfe: nicht im Grafikexport, nicht im Druck.
Zeiger ändert sich nichts, dort bleibt es beim Tooltip.
Siehe D52. Siehe D52, D92.
**Ticket-Stand im Knoten-Fenster (§11).** Trägt die Zeile eine **Ticket-Stand im Knoten-Fenster (§11).** Trägt die Zeile eine
Ticket-Referenz (`#US-123` / `#T-1234`) und ist ihr Teilbaum per Ticket-Referenz (`#US-123` / `#T-1234`) und ist ihr Teilbaum per
@@ -519,8 +523,9 @@ es einen gibt, den **Zuständigen**.
Farbe des Knotens ist die Rückmeldung. Farbe des Knotens ist die Rückmeldung.
- Beide Richtungen betreffen **einen** Knoten; eine Sammelaktion über einen - Beide Richtungen betreffen **einen** Knoten; eine Sammelaktion über einen
Teilbaum gibt es nicht. Teilbaum gibt es nicht.
- Geholt wird erst, wenn das Fenster **kurz stehen bleibt** (nicht im - Geholt wird erst, wenn das Fenster **kurz stehen bleibt** (nicht beim
Vorüberfahren), und je Ticket **einmal je Sitzung** ein ↻-Knopf im Fenster Durch-Tabben von Knoten zu Knoten), und je Ticket **einmal je Sitzung**
ein ↻-Knopf im Fenster
holt neu. Ohne Anmeldung an der Instanz, ohne Projekt-Zuordnung oder ohne holt neu. Ohne Anmeldung an der Instanz, ohne Projekt-Zuordnung oder ohne
konfiguriertes Backend geschieht still nichts; ein Fehler steht als Zeile im konfiguriertes Backend geschieht still nichts; ein Fehler steht als Zeile im
Fenster. Fenster.
@@ -1086,8 +1091,9 @@ verknüpft (siehe D25):
freistehend — dort behält Strg+Klick den Sprung zur Zeile; die freistehend — dort behält Strg+Klick den Sprung zur Zeile; die
D67-Ausschlüsse (Kommentar, Beschreibungsteil, URL) gelten auch hier. D67-Ausschlüsse (Kommentar, Beschreibungsteil, URL) gelten auch hier.
Siehe D91-Nachtrag 5. Siehe D91-Nachtrag 5.
- Der **einfache** Klick bleibt unverändert der Link (§6): Ein Knoten mit URL - Der **einfache** Klick gehört dem Knoten-Fenster (§6/§9, D92): Er öffnet die
öffnet sie weiterhin im neuen Tab. Deshalb Alt und nicht der einfache Klick. Beschreibung, nicht die Zeile im Text. Deshalb Alt und nicht der einfache
Klick.
- **Auffindbarkeit:** Solange **Alt gedrückt** ist, zeigen alle Knoten den - **Auffindbarkeit:** Solange **Alt gedrückt** ist, zeigen alle Knoten den
Sprung-Cursor und der Knoten unter dem Zeiger einen Petrol-Ring — die Geste Sprung-Cursor und der Knoten unter dem Zeiger einen Petrol-Ring — die Geste
ist im Moment des Ausprobierens sichtbar, auch auf verlinkten Knoten. Dazu ist im Moment des Ausprobierens sichtbar, auch auf verlinkten Knoten. Dazu
@@ -1207,8 +1213,8 @@ Label: **▾** offen, **„▸ n“** eingeklappt (n = Zahl der verborgenen Knot
als kleiner **gerahmter Chip**, damit das Klickziel zu treffen ist (D64; das als kleiner **gerahmter Chip**, damit das Klickziel zu treffen ist (D64; das
nackte Glyph war zu klein). Klick auf das Zeichen klappt um — der einfache nackte Glyph war zu klein). Klick auf das Zeichen klappt um — der einfache
Klick auf den Knoten selbst Klick auf den Knoten selbst
bleibt der Link (§6); Tastatur: **←** klappt zu, **→** klappt auf am öffnet das Knoten-Fenster (§9, D92); Tastatur: **←** klappt zu, **→** klappt
fokussierten Knoten (WAI-ARIA-Baum-Idiom). auf am fokussierten Knoten (WAI-ARIA-Baum-Idiom).
- **Anfangszustand aus dem Text (§1):** `>` klappt ein. `<` holt seinen - **Anfangszustand aus dem Text (§1):** `>` klappt ein. `<` holt seinen
Teilbaum hervor, indem die Faltung die Pfad-Ebenen **hinunterwandert**: Teilbaum hervor, indem die Faltung die Pfad-Ebenen **hinunterwandert**:
+9
View File
@@ -654,6 +654,15 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
Es schließt bei allem, was seine Aussage hinfällig macht — Tipp daneben, Es schließt bei allem, was seine Aussage hinfällig macht — Tipp daneben,
zweiter Tipp, Esc, ×, Diagramm-Scroll, Bereichswechsel, Sprung in den Text, zweiter Tipp, Esc, ×, Diagramm-Scroll, Bereichswechsel, Sprung in den Text,
`render()`. Die letzten drei sind beim Bauen zuerst vergessen worden. `render()`. Die letzten drei sind beim Bauen zuerst vergessen worden.
- **Das Knoten-Fenster öffnet auf Klick, nie auf Hover (D92):** Der einfache
Klick toggelt es überall (auch auf Link-Knoten — die URL ist der ↗-Knopf im
Fenster; Enter kommt als Klick mit `e.detail === 0` und wird durchgelassen).
Zwei Fallen: `focusin` öffnet nur bei `:focus-visible` — der Mausklick
fokussiert den Knoten ebenfalls, und ohne die Weiche schlösse der
Klick-Toggle das vom focusin geöffnete Fenster sofort wieder. Und
`focusout` schließt nicht, wenn `relatedTarget` im Fenster liegt — die
Knöpfe darin sind trotz `tabindex="-1"` klick-fokussierbar, sonst stirbt
der Knopf-Klick zwischen mousedown und mouseup.
- **Taiga-Ticket-Anlage (D91):** Die entscheidbaren Regeln liegen headless in - **Taiga-Ticket-Anlage (D91):** Die entscheidbaren Regeln liegen headless in
`taiga.js` (`ticketRefOf` = Idempotenz-Marker, `taskCandidates` = `taiga.js` (`ticketRefOf` = Idempotenz-Marker, `taskCandidates` =
Dialog-Vorbelegung, `storyAncestor` = der nächste Vorfahr mit Story-Ref, Dialog-Vorbelegung, `storyAncestor` = der nächste Vorfahr mit Story-Ref,
+49 -48
View File
@@ -1462,7 +1462,18 @@ function applyFoldPreset(mode){
/* Faltung umklappen (SPEC §9, D38). Gelingt das Zurückschreiben, ist der Text /* Faltung umklappen (SPEC §9, D38). Gelingt das Zurückschreiben, ist der Text
der Zustand die Überlagerungen werden dann geleert, damit sie ihn nicht der Zustand die Überlagerungen werden dann geleert, damit sie ihn nicht
maskieren können. Nach dem Neubau bekommt derselbe Knoten den Fokus zurück, maskieren können. Nach dem Neubau bekommt derselbe Knoten den Fokus zurück,
sonst risse die Tastaturbedienung ab (das alte Element ist weg). */ sonst risse die Tastaturbedienung ab (das alte Element ist weg) aber ohne
das Knoten-Fenster zu rufen: Falten ist genau die Geste, der das Fenster im
Weg stand (D92), und die `:focus-visible`-Heuristik ist beim
programmatischen Fokus nicht verlässlich genug für diese Zusage. */
let suppressTipFocus = false;
function refocusNode(line){
const again = out.querySelector('.node[data-line="' + line + '"]');
if(!again) return;
suppressTipFocus = true; /* focusin läuft synchron im focus() */
again.focus({preventScroll: true});
suppressTipFocus = false;
}
function toggleFold(el){ function toggleFold(el){
const line = +el.dataset.line; const line = +el.dataset.line;
const st = foldByLine.get(line); const st = foldByLine.get(line);
@@ -1473,16 +1484,14 @@ function toggleFold(el){
if(lens){ if(lens){
lensOverrides.set(st.key, !st.collapsed); lensOverrides.set(st.key, !st.collapsed);
render(); render();
const again2 = out.querySelector('.node[data-line="' + line + '"]'); refocusNode(line);
if(again2) again2.focus({preventScroll: true});
return; return;
} }
foldOverrides.set(st.key, !st.collapsed); foldOverrides.set(st.key, !st.collapsed);
/* Das Schreiben löst per `input`-Ereignis schon ein render() aus. */ /* Das Schreiben löst per `input`-Ereignis schon ein render() aus. */
if(writeFoldToText(line, !st.collapsed)) foldOverrides.clear(); if(writeFoldToText(line, !st.collapsed)) foldOverrides.clear();
else render(); else render();
const again = out.querySelector('.node[data-line="' + line + '"]'); refocusNode(line);
if(again) again.focus({preventScroll: true});
} }
/* Klick auf das Falt-Zeichen ▾/ klappt um. preventDefault, weil das Zeichen /* Klick auf das Falt-Zeichen ▾/ klappt um. preventDefault, weil das Zeichen
@@ -1577,7 +1586,7 @@ out.addEventListener('touchend', e => {
`preventDefault()` öffnete ein Link-Knoten zusätzlich seine URL auf Touch `preventDefault()` öffnete ein Link-Knoten zusätzlich seine URL auf Touch
ist der Link stattdessen ein Knopf IM Fenster. */ ist der Link stattdessen ein Knopf IM Fenster. */
e.preventDefault(); e.preventDefault();
toggleNodeTip(tapped); toggleNodeTip(tapped, true);
}, {passive: false}); }, {passive: false});
out.addEventListener('touchcancel', disarmPress); out.addEventListener('touchcancel', disarmPress);
out.addEventListener('contextmenu', e => { if(pressTimer || armedEl) e.preventDefault(); }); out.addEventListener('contextmenu', e => { if(pressTimer || armedEl) e.preventDefault(); });
@@ -1605,14 +1614,14 @@ function closeNodeTip(){
nodeTip.hidden = true; nodeTip.hidden = true;
} }
function toggleNodeTip(el){ function toggleNodeTip(el, touch){
if(tipNode === el){ closeNodeTip(); return; } /* zweiter Tipp schließt */ if(tipNode === el){ closeNodeTip(); return; } /* zweiter Tipp schließt */
showNodeTip(el, true); showNodeTip(el, touch);
} }
/* `touch` unterscheidet die beiden Anlässe: Am Zeiger ist der ganze Knoten der /* `touch` wählt nur noch den Sprung-Hinweis (langer Druck statt Alt+Klick).
Link (§6), ein -Knopf wäre dort ein zweiter Weg zum selben Ziel; und der Der -Knopf steht seit D92 an JEDEM verlinkten Knoten: Der einfache Klick
Sprung-Hinweis nennt Alt+Klick, den es auf dem Telefon nicht gibt. */ öffnet überall das Fenster, nicht mehr die URL wie der Tipp auf Touch. */
function showNodeTip(el, touch){ function showNodeTip(el, touch){
closeNodeTip(); closeNodeTip();
const title = el.getAttribute('data-tip') || ''; const title = el.getAttribute('data-tip') || '';
@@ -1621,7 +1630,7 @@ function showNodeTip(el, touch){
const desc = i >= 0 ? title.slice(0, i) : ''; const desc = i >= 0 ? title.slice(0, i) : '';
const roh = i >= 0 ? title.slice(i + sep.length) : title; const roh = i >= 0 ? title.slice(i + sep.length) : title;
const facts = touch ? roh.replace(t('jumpHint'), t('jumpHintTouch')) : roh; const facts = touch ? roh.replace(t('jumpHint'), t('jumpHintTouch')) : roh;
const href = touch && el.tagName === 'A' ? el.getAttribute('href') : null; const href = el.tagName === 'A' ? el.getAttribute('href') : null;
/* Absätze: Leerzeilen trennen (SPEC §1), einzelne Zeilenumbrüche sind bloß /* Absätze: Leerzeilen trennen (SPEC §1), einzelne Zeilenumbrüche sind bloß
der Umbruch der Quelldatei und werden zu Leerzeichen. Der `title` kann das der Umbruch der Quelldatei und werden zu Leerzeichen. Der `title` kann das
nicht er zeigt die harten Umbrüche und sah im schmalen Fenster nicht er zeigt die harten Umbrüche und sah im schmalen Fenster
@@ -1661,49 +1670,41 @@ function placeNodeTip(el){
Math.max(12, Math.min(anchorX - left, w - 12)).toFixed(1) + 'px'); Math.max(12, Math.min(anchorX - left, w - 12)).toFixed(1) + 'px');
} }
/* ---------- Dasselbe Fenster am Zeiger und an der Tastatur (D57) ---------- /* ---------- Dasselbe Fenster am Klick und an der Tastatur (D57, D92) ----------
Es löst den nativen `title` ab: Der konnte weder Absätze noch eine Linie Es löst den nativen `title` ab: Der konnte weder Absätze noch eine Linie
(der Trennstrich war aus 24 `` gemalt, D40-Nachtrag) und erschien nie beim (der Trennstrich war aus 24 `` gemalt, D40-Nachtrag) und erschien nie beim
Tastaturfokus. Beides kann das Fenster ohnehin schon es wurde nur für Tastaturfokus. Der Hover-Weg aus D57 ist ausgebaut (D92): Mit den
Touch gebaut (D52). Taiga-Aktionen wurde das Fenster gewichtiger, und beim bloßen Ansteuern des
Falt-Chips stand es schon im Weg. Der einfache Klick öffnet es jetzt
Die Verzögerung ist Absicht-Erkennung: Über einen dichten Baum fährt man überall wie der Tipp auf Touch (D52); der Link eines Knotens ist damit
hinweg, ohne etwas wissen zu wollen. Beim Wechsel von Knoten zu Knoten auch am Zeiger der -Knopf IM Fenster (Strg+Klick und Enter behalten den
zeigt es sofort weiter wer schon liest, wartet nicht noch einmal. */ Browser-Default und öffnen die URL direkt). */
const TIP_DELAY = 350; out.addEventListener('click', e => {
let tipTimer = null, tipLeave = null; if(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
const finePointer = () => window.matchMedia('(hover:hover) and (pointer:fine)').matches; if(e.detail === 0) return; /* Enter am Link-Knoten bleibt der Link (D25) */
function cancelTipTimers(){ if(e.target && e.target.closest && e.target.closest('.fold')) return;
clearTimeout(tipTimer); clearTimeout(tipLeave); const el = nodeFromEvent(e);
tipTimer = tipLeave = null; if(!el) return;
e.preventDefault(); /* der Klick gehört dem Fenster, nicht der URL */
toggleNodeTip(el, false);
});
/* Tastaturfokus öffnet weiter sofort (D57) aber nur echten
(`:focus-visible`): Auch der Mausklick fokussiert den Knoten, und dann
entschiede sonst der focusin statt des Klick-Handlers der Toggle darüber
schlösse das eben geöffnete Fenster sofort wieder. */
function kbFocus(el){
try{ return el.matches(':focus-visible'); }catch(_){ return false; }
} }
out.addEventListener('pointerover', e => {
if(e.pointerType === 'touch' || !finePointer()) return;
const el = e.target.closest && e.target.closest('.node');
if(!el || el === tipNode) return;
cancelTipTimers();
/* Steht schon eines offen, ist die Absicht erwiesen — dann ohne Warten. */
tipTimer = setTimeout(() => showNodeTip(el, false), tipNode ? 0 : TIP_DELAY);
});
out.addEventListener('pointerout', e => {
if(e.pointerType === 'touch' || !finePointer()) return;
clearTimeout(tipTimer);
/* Nicht sofort zumachen: Der Weg ins Fenster führt über den Zwischenraum,
und dort ist der Zeiger kurz über keinem von beiden. */
clearTimeout(tipLeave);
tipLeave = setTimeout(() => {
if(!nodeTip.matches(':hover')) closeNodeTip();
}, 120);
});
nodeTip.addEventListener('pointerenter', cancelTipTimers);
nodeTip.addEventListener('pointerleave', () => { closeNodeTip(); });
/* Tastaturfokus: ohne Verzögerung er ist bereits die ausdrückliche Absicht,
und ein `title` hat hier noch nie etwas gezeigt. */
out.addEventListener('focusin', e => { out.addEventListener('focusin', e => {
if(suppressTipFocus) return; /* Fokus-Rückgabe nach dem Falten (D38/D92) */
const el = e.target.closest && e.target.closest('.node'); const el = e.target.closest && e.target.closest('.node');
if(el && el !== tipNode) showNodeTip(el, false); if(el && el !== tipNode && kbFocus(el)) showNodeTip(el, false);
}); });
out.addEventListener('focusout', e => { out.addEventListener('focusout', e => {
/* Fokus INS Fenster (die Knöpfe darin sind klick-fokussierbar) schließt
nicht sonst verschwände der Knopf zwischen mousedown und mouseup und
sein Klick ginge verloren. */
if(e.relatedTarget && nodeTip.contains(e.relatedTarget)) return;
const el = e.target.closest && e.target.closest('.node'); const el = e.target.closest && e.target.closest('.node');
if(el && el === tipNode) closeNodeTip(); if(el && el === tipNode) closeNodeTip();
}); });