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
@@ -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)
|
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
|
— zusätzlicher i18n-Text in 9 Sprachen für einen Effekt, den Cursor und Legende
|
||||||
bereits abdecken.
|
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.
|
||||||
|
|||||||
+3
-2
@@ -232,8 +232,9 @@ verknüpft (siehe D25):
|
|||||||
- **Diagramm → Text:** **Alt+Klick** auf einen Knoten markiert die zugehörige
|
- **Diagramm → Text:** **Alt+Klick** auf einen Knoten markiert die zugehörige
|
||||||
Zeile im Texteditor (ganze Zeile ausgewählt, in Sicht gescrollt, Fokus im
|
Zeile im Texteditor (ganze Zeile ausgewählt, in Sicht gescrollt, Fokus im
|
||||||
Textfeld). Tastatur: **Alt+Enter** am fokussierten Knoten. Auf Touch-Geräten:
|
Textfeld). Tastatur: **Alt+Enter** am fokussierten Knoten. Auf Touch-Geräten:
|
||||||
**langer Druck** (≥ 500 ms); ein Wischen bricht ab. Ist das Editor-Panel
|
**langer Druck** — nach ≥ 500 ms zeigt der Knoten einen Petrol-Ring („scharf"),
|
||||||
zugeklappt, öffnet der Sprung es zuerst.
|
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
|
- **Text → Diagramm:** Der Knoten der **Cursor-Zeile** wird im Diagramm
|
||||||
hervorgehoben (weißer Halo + Ring in Tinte) und beim Zeilenwechsel ins Bild
|
hervorgehoben (weißer Halo + Ring in Tinte) und beim Zeilenwechsel ins Bild
|
||||||
gescrollt. Zeilen ohne Knoten (Kommentar, Leerzeile, ausgeblendetes
|
gescrollt. Zeilen ohne Knoten (Kommentar, Leerzeile, ausgeblendetes
|
||||||
|
|||||||
@@ -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
|
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
|
`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.
|
`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
|
- Kleiner Bildschirm: `body.mobile` (per `matchMedia`, ≤ 640 px) stapelt
|
||||||
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
|
Diagramm/Editor mit **stufenlosem** Splitter (kein Snap/Collapse wie auf
|
||||||
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
|
Desktop): der Gutter-Drag ruft `setMobileDrow()` (klemmt `--drow` zwischen den
|
||||||
|
|||||||
+23
-12
@@ -410,27 +410,38 @@ out.addEventListener('keydown', e => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* Mobil gibt es kein Alt: langer Druck (500 ms) auf einen Knoten springt.
|
/* 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
|
Der Sprung passiert erst beim LOSLASSEN: `focus()` aus einem Timer heraus
|
||||||
seine URL; das Kontextmenü/Callout ebenso (die Geste ist hier vergeben). */
|
gilt in mobilen Browsern nicht als Nutzergeste — der Fokus fiel sofort wieder
|
||||||
let pressTimer = null, pressFired = false;
|
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 => {
|
out.addEventListener('touchstart', e => {
|
||||||
|
disarmPress();
|
||||||
const el = nodeFromEvent(e);
|
const el = nodeFromEvent(e);
|
||||||
pressFired = false;
|
|
||||||
if(!el) return;
|
if(!el) return;
|
||||||
pressTimer = setTimeout(() => {
|
pressTimer = setTimeout(() => {
|
||||||
pressTimer = null;
|
pressTimer = null;
|
||||||
pressFired = true;
|
armedEl = el;
|
||||||
jumpToLine(+el.dataset.line);
|
el.classList.add('armed');
|
||||||
}, 500);
|
}, 500);
|
||||||
}, {passive: true});
|
}, {passive: true});
|
||||||
function cancelPress(){ if(pressTimer){ clearTimeout(pressTimer); pressTimer = null; } }
|
out.addEventListener('touchmove', disarmPress, {passive: true});
|
||||||
out.addEventListener('touchmove', cancelPress, {passive: true});
|
|
||||||
out.addEventListener('touchend', e => {
|
out.addEventListener('touchend', e => {
|
||||||
if(pressFired) e.preventDefault();
|
const el = armedEl;
|
||||||
cancelPress();
|
disarmPress();
|
||||||
|
if(!el) return;
|
||||||
|
e.preventDefault(); /* unterdrückt den nachfolgenden Klick/Link */
|
||||||
|
jumpToLine(+el.dataset.line); /* echte Nutzergeste -> der Fokus bleibt */
|
||||||
}, {passive: false});
|
}, {passive: false});
|
||||||
out.addEventListener('touchcancel', () => { cancelPress(); pressFired = false; });
|
out.addEventListener('touchcancel', disarmPress);
|
||||||
out.addEventListener('contextmenu', e => { if(pressTimer || pressFired) e.preventDefault(); });
|
out.addEventListener('contextmenu', e => { if(pressTimer || armedEl) e.preventDefault(); });
|
||||||
|
|
||||||
/* Text -> Diagramm: Knoten der Cursor-Zeile hervorheben. `caretLine` bleibt
|
/* Text -> Diagramm: Knoten der Cursor-Zeile hervorheben. `caretLine` bleibt
|
||||||
null, bis der Cursor das erste Mal bewegt wurde — sonst wäre nach dem Laden
|
null, bis der Cursor das erste Mal bewegt wurde — sonst wäre nach dem Laden
|
||||||
|
|||||||
@@ -448,8 +448,14 @@
|
|||||||
Rückmeldung, weil dort der einfache Klick dem Link gehört (D25). */
|
Rückmeldung, weil dort der einfache Klick dem Link gehört (D25). */
|
||||||
#out.alt .node{cursor:alias}
|
#out.alt .node{cursor:alias}
|
||||||
#out.alt .node:hover{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--or)}
|
#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. */
|
/* Langer Druck „scharf": derselbe Petrol-Ring wie unter dem Alt-Zeiger. */
|
||||||
a.node{-webkit-touch-callout:none}
|
#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}
|
.ext{font-size:.72em;margin-left:5px;opacity:.55}
|
||||||
.size{
|
.size{
|
||||||
position:absolute;top:-9px;right:-9px;
|
position:absolute;top:-9px;right:-9px;
|
||||||
|
|||||||
Reference in New Issue
Block a user