frontend: Touch-Langdruck springt beim Loslassen (Fokus blieb sonst nicht)
Auf echten Touch-Geräten flackerte die Markierung im Editor nach dem langen
Druck nur kurz auf und der Fokus fiel sofort wieder heraus.
Ursache: Der Sprung lief im 500-ms-Timer, und `focus()` aus einem
Timer-Callback gilt in mobilen Browsern nicht als Nutzergeste — ein Textfeld
darf sich so nicht selbst fokussieren (sonst könnte jede Seite ungefragt die
Bildschirmtastatur aufziehen). `touchend` ist eine Nutzergeste.
- Der Timer setzt jetzt nur noch die Rückmeldung: der Zielknoten bekommt die
Klasse `armed` mit demselben Petrol-Ring wie unter dem Alt-Zeiger.
`jumpToLine()` läuft im touchend-Handler; Wischen bricht weiterhin ab.
- Zweiter Beitrag zum selben Symptom: die native Textauswahl bzw. das
Link-Callout des Browsers startet bei derselben Druckdauer und riss die
Auswahl an sich. Drei Schichten dagegen, weil keine allein überall wirkt:
contextmenu-preventDefault 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; Chrome kennt es nicht mehr). Die
frühere Regel `a.node{-webkit-touch-callout:none}` geht darin auf.
Verifiziert (synthetische TouchEvents): während des Drucks nur `armed` +
Petrol-Ring, Auswahl noch unverändert, Fokus noch nicht im Textfeld; beim
Loslassen Sprung, Fokus TEXTAREA, Klick unterdrückt; Wischen und kurzer Druck
springen nicht und lassen den Klick durch. Ring greift auch unterhalb einer
any-of-Gruppe. Vitest 37/37.
Einschränkung: Weder die Nutzergesten-Regel noch die nativen Langdruck-Gesten
lassen sich synthetisch auslösen — der eigentliche Beweis ist ein Test auf
echter Hardware.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c496850b88
commit
bbdecce75a
+23
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user