diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 515d98c..6f46d6f 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -542,3 +542,26 @@ bedeutet aber, dass derselbe Klick je nach Knoten Verschiedenes tut. Ebenfalls verworfen: ein **einmaliger Hinweis** nach dem ersten Sprung (localStorage-Flag) — zusätzlicher i18n-Text in 9 Sprachen für einen Effekt, den Cursor und Legende bereits abdecken. + +**Touch: der Sprung passiert beim Loslassen, nicht nach 500 ms (Nachtrag).** +Erste Fassung führte den Sprung im 500-ms-Timer aus. Auf echten Touch-Geräten +flackerte die Markierung im Editor dann nur kurz auf und der Fokus fiel sofort +wieder heraus: **`focus()` aus einem Timer-Callback gilt in mobilen Browsern +nicht als Nutzergeste**, das Textfeld darf sich so nicht selbst fokussieren +(sonst könnte jede Seite ungefragt die Bildschirmtastatur aufziehen). +`touchend` **ist** eine Nutzergeste — dort bleibt der Fokus. Der Timer macht +seither nur noch die Rückmeldung („scharf": Petrol-Ring am Zielknoten, dieselbe +Sprache wie unter dem Alt-Zeiger), der eigentliche Sprung hängt am Loslassen. + +Zweiter Beitrag zum selben Symptom: Die **native Textauswahl bzw. das +Link-Callout** des Browsers startet bei etwa derselben Druckdauer und riss die +Auswahl an sich. Dagegen drei Schichten, weil keine allein überall wirkt: +`preventDefault()` auf `contextmenu` während des Drucks (überall), +`user-select:none` an `.node` für grobe Zeiger (`@media (hover:none) and +(pointer:coarse)`) und `-webkit-touch-callout:none` (nur iOS/Safari; Chrome +kennt die Eigenschaft nicht mehr). Auf Touch lässt sich Knotentext damit nicht +mehr markieren — bewusst in Kauf genommen, die Geste ist dort vergeben. + +Lehre für die Prüfung: Synthetische `TouchEvent`s beweisen nur die eigene +Ereignis-Logik. Weder die Nutzergesten-Regel noch die nativen Langdruck-Gesten +lassen sich so auslösen — beides fiel erst auf echter Hardware auf. diff --git a/docs/SPEC.md b/docs/SPEC.md index aff44e5..a0b47a8 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -232,8 +232,9 @@ verknüpft (siehe D25): - **Diagramm → Text:** **Alt+Klick** auf einen Knoten markiert die zugehörige Zeile im Texteditor (ganze Zeile ausgewählt, in Sicht gescrollt, Fokus im Textfeld). Tastatur: **Alt+Enter** am fokussierten Knoten. Auf Touch-Geräten: - **langer Druck** (≥ 500 ms); ein Wischen bricht ab. Ist das Editor-Panel - zugeklappt, öffnet der Sprung es zuerst. + **langer Druck** — nach ≥ 500 ms zeigt der Knoten einen Petrol-Ring („scharf"), + der Sprung erfolgt beim **Loslassen**; ein Wischen bricht ab. Ist das + Editor-Panel zugeklappt, öffnet der Sprung es zuerst. - **Text → Diagramm:** Der Knoten der **Cursor-Zeile** wird im Diagramm hervorgehoben (weißer Halo + Ring in Tinte) und beim Zeilenwechsel ins Bild gescrollt. Zeilen ohne Knoten (Kommentar, Leerzeile, ausgeblendetes diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 81b9322..5a531dd 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -139,6 +139,15 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der die Klasse `alt` an `#out` (Cursor + Ring am Knoten unter dem Zeiger) — der `blur`-Handler ist Pflicht, sonst bleibt der Modus nach Alt+Tab hängen; dazu `jumpHint` im Knoten-Tooltip und `hint_jump` als letzte Zeile der Legende. +- Touch-Langdruck (D25): Der Timer (500 ms) setzt **nur** die Klasse `armed` + (Petrol-Ring); `jumpToLine()` läuft im `touchend`-Handler. Nicht zurück in den + Timer verlegen — **`focus()` aus einem Timer gilt in mobilen Browsern nicht als + Nutzergeste**, das Textfeld verliert den Fokus sofort wieder (Symptom: die + Markierung flackert nur auf). Gegen die native Langdruck-Auswahl braucht es + alle drei Schichten: `contextmenu`-`preventDefault` während des Drucks, + `user-select:none` per `@media (hover:none) and (pointer:coarse)` und + `-webkit-touch-callout:none` (nur iOS). Synthetische `TouchEvent`s prüfen + davon **nichts** — nur die eigene Ereignis-Logik. - Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den diff --git a/frontend/src/app.js b/frontend/src/app.js index 406d2d3..25cc1d5 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -410,27 +410,38 @@ out.addEventListener('keydown', e => { }); /* Mobil gibt es kein Alt: langer Druck (500 ms) auf einen Knoten springt. - Der folgende Klick wird unterdrückt, sonst öffnete ein Link-Knoten zusätzlich - seine URL; das Kontextmenü/Callout ebenso (die Geste ist hier vergeben). */ -let pressTimer = null, pressFired = false; + Der Sprung passiert erst beim LOSLASSEN: `focus()` aus einem Timer heraus + gilt in mobilen Browsern nicht als Nutzergeste — der Fokus fiel sofort wieder + aus dem Textfeld (die Markierung flackerte nur kurz auf). `touchend` ist eine + echte Geste, dort bleibt er. Nach 500 ms passiert deshalb nur die Rückmeldung: + der Zielknoten bekommt den Petrol-Ring („scharf"). Der folgende Klick wird + unterdrückt, sonst öffnete ein Link-Knoten zusätzlich seine URL; das + Kontextmenü/Callout ebenso (die Geste ist hier vergeben). */ +let pressTimer = null, armedEl = null; +function disarmPress(){ + if(pressTimer){ clearTimeout(pressTimer); pressTimer = null; } + if(armedEl){ armedEl.classList.remove('armed'); armedEl = null; } +} out.addEventListener('touchstart', e => { + disarmPress(); const el = nodeFromEvent(e); - pressFired = false; if(!el) return; pressTimer = setTimeout(() => { pressTimer = null; - pressFired = true; - jumpToLine(+el.dataset.line); + armedEl = el; + el.classList.add('armed'); }, 500); }, {passive: true}); -function cancelPress(){ if(pressTimer){ clearTimeout(pressTimer); pressTimer = null; } } -out.addEventListener('touchmove', cancelPress, {passive: true}); +out.addEventListener('touchmove', disarmPress, {passive: true}); out.addEventListener('touchend', e => { - if(pressFired) e.preventDefault(); - cancelPress(); + const el = armedEl; + disarmPress(); + if(!el) return; + e.preventDefault(); /* unterdrückt den nachfolgenden Klick/Link */ + jumpToLine(+el.dataset.line); /* echte Nutzergeste -> der Fokus bleibt */ }, {passive: false}); -out.addEventListener('touchcancel', () => { cancelPress(); pressFired = false; }); -out.addEventListener('contextmenu', e => { if(pressTimer || pressFired) e.preventDefault(); }); +out.addEventListener('touchcancel', disarmPress); +out.addEventListener('contextmenu', e => { if(pressTimer || armedEl) e.preventDefault(); }); /* Text -> Diagramm: Knoten der Cursor-Zeile hervorheben. `caretLine` bleibt null, bis der Cursor das erste Mal bewegt wurde — sonst wäre nach dem Laden diff --git a/frontend/src/style.css b/frontend/src/style.css index 72a022a..fb913d8 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -448,8 +448,14 @@ Rückmeldung, weil dort der einfache Klick dem Link gehört (D25). */ #out.alt .node{cursor:alias} #out.alt .node:hover{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--or)} - /* Auf Touch ist der lange Druck die Sprung-Geste — kein iOS-Link-Callout. */ - a.node{-webkit-touch-callout:none} + /* Langer Druck „scharf": derselbe Petrol-Ring wie unter dem Alt-Zeiger. */ + #out .node.armed{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--or)} + /* Auf Touch ist der lange Druck die Sprung-Geste — die native Textauswahl und + das Link-Callout des Browsers starten zur selben Zeit und rissen sonst die + Auswahl im Textfeld wieder an sich (D25). */ + @media (hover:none) and (pointer:coarse){ + .node{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none} + } .ext{font-size:.72em;margin-left:5px;opacity:.55} .size{ position:absolute;top:-9px;right:-9px;