4 Punkte von GN⁺ 2023-09-30 | 1 Kommentare | Auf WhatsApp teilen
  • Die interaktiven Diagramme von Red Blob Games müssen sofort reagieren, wenn Leser Objekte selbst bewegen; deshalb ist die Eingabeverarbeitung auf Pointer Events ausgerichtet und behandelt Maus und Touch gemeinsam
  • Der frühere Ansatz mit mousedown/mouseup/mousemove konnte den Drag-Zustand durcheinanderbringen, wenn der Pointer das Objekt verließ oder die Taste außerhalb losgelassen wurde, weshalb Ergänzungen wie globale Handler nötig waren
  • Touch-Eingaben werden anders zugestellt als Mausereignisse, und ein Drag mit einem Finger löst zugleich Seitenscrolling aus; deshalb wird in touchstart preventDefault() aufgerufen, um Konflikte zu verringern
  • Das finale Rezept kombiniert pointerdown, pointermove, pointerup, pointercancel mit setPointerCapture() und prüft die linke Maustaste sowie den Start-Offset, damit das Objekt nicht plötzlich springt
  • Objekte mit Text und Bildern, Multi-Touch, verschachteltes Dragging, <canvas>, Kombinationen aus Hover und Drag sowie plattformabhängige Sonderfälle wie lostpointercapture müssen in echten UIs separat behandelt werden

Ziele eines Drag-Eingabehandlers

  • Viele interaktive Seiten enthalten ziehbare Objekte, die Leser selbst bewegen können, und das Diagramm muss auf Änderungen der Objektposition reagieren
  • Ziel ist es, einen Eingabe-Event-Handler zu erstellen, der sowohl mit Maus- als auch mit Touch-Eingaben funktioniert
  • Der Grundablauf besteht darin, den Drag in pointerdown zu starten, die Position in pointermove zu aktualisieren und den Drag bei pointerup oder einem Abbruch-Event zu beenden
  • In der tatsächlichen Implementierung wird es durch Maustasten, Touch mit mehreren Fingern, das Ziel der Event-Zustellung und Kontextmenüs per Rechtsklick komplexer als im einfachen Modell
  • Das Grundrezept kombiniert die folgenden Elemente
    • In pointerdown zurückkehren, wenn event.button !== 0, und so nur die primäre Taste behandeln
    • Event-Koordinaten in Diagrammkoordinaten umrechnen
    • Beim Start dx und dy speichern, also die Differenz zwischen Objektposition und Pointerposition
    • Mit el.setPointerCapture(event.pointerId) den Pointer erfassen
    • In pointermove den gespeicherten Offset addieren und state.pos aktualisieren
    • In pointerup, pointercancel state.dragging = null setzen
    • In touchstart preventDefault() aufrufen
  • Dieses Rezept ist ein Eingabe-Event-Handler; die tatsächliche Ausgabe muss mit einem separaten Zustands-Handler verbunden werden
  • Der vollständige Code steht auf der examples page; sie enthält auch Beispiele für horizontales Ziehen von Zahlen statt Objektbewegung, Canvas-Malen und eingeschränktes Dragging
  • Der Testumfang umfasst Gecko/Firefox, Blink/Chrome und WebKit/Safari auf Mac, Windows, Linux, Android, iPhone und iPad
  • Hover-fähige Stifte, hybride Touch+Maus-Geräte und Spracheingabe wurden nicht getestet
  • Dieser Ansatz ist nicht die HTML-Drag and Drop API, bei der Elemente über andere Elemente gelegt werden; in Diagrammen müssen Maus- und Touch-Events direkt gelesen werden

Grenzen bei ausschließlicher Nutzung von Maus-Events

  • Die erste Implementierung bestand darin, mousedown, mouseup und mousemove direkt am ziehbaren Element zu registrieren
  • Oberflächlich funktioniert das, doch es entstehen zwei Probleme
    • Wenn der Pointer schnell bewegt wird, befindet er sich nicht mehr über dem Kreis und dieser erhält keine Events mehr
    • Wenn die Taste außerhalb des Kreises losgelassen wird, kann der Zustand in dragging hängen bleiben
  • Die Verbesserung besteht darin, mousedown am Kreis zu empfangen und während des Drags mousemove und mouseup vorübergehend an document zu registrieren
  • Wenn in mouseup die an document registrierten Handler für mousemove und mouseup entfernt werden, funktioniert Maus-Dragging stabiler
  • Dieser Ansatz behandelt nur die Maus und berücksichtigt Touch-Events nicht

Unterschiede bei Touch-Events

  • Touch-Eingaben verwenden statt mousedown, mouseup, mousemove die Events touchstart, touchend, touchmove
  • Touch-Events werden nach touchstart automatisch captured, sodass alle touchmove-Events an das ursprüngliche Element zugestellt werden
  • Wegen dieser Eigenschaft ist es nicht nötig, wie bei Maus-Events vorübergehend einen globalen Event-Handler an document zu registrieren
  • Wenn der Browser eine Touch-Sequenz abbrechen muss, sendet er touchcancel
  • Eine Touch-Drag-Implementierung ruft in touchstart preventDefault() auf und aktualisiert in touchmove die Position anhand der Koordinaten von event.changedTouches[0]
  • Dieser Ansatz behandelt nur Touch und verarbeitet Maus-Events nicht

Maus und Touch mit Pointer Events vereinheitlichen

  • Um sowohl Maus als auch Touch zu behandeln, waren viele Event-Handler nötig; vor 2021 wurde dieser Ansatz verwendet
  • Von 2011 bis 2014 wurde in d3-Projekten d3-drag verwendet, während Projekte ohne d3 eigenen Maus+Touch-Code schrieben
  • Pointer Events vereinheitlichen und vereinfachen die Verarbeitung von Maus und Touch
    • MS IE fügte 2012 Unterstützung hinzu
    • Chrome fügte 2017 Unterstützung hinzu, Firefox 2018 und Safari 2020
  • Das Browserverhalten hat sich im Laufe der Zeit geändert; 2017 stellte Chrome einige Events standardmäßig auf passive mode um, was dazu führte, dass die Seite scrollte, wenn man ein Objekt ziehen wollte
  • Safari und Firefox übernahmen 2018 dieselbe Änderung
  • Mit pointer capture lässt sich die Logik einfacher halten, ohne die bei Maus-Events nötigen globalen document-Handler hinzuzufügen und zu entfernen

Pointer-Verfolgung mit Pointer Capture aufrechterhalten

  • Pointer capture ermöglicht es, die Verfolgung fortzusetzen, auch wenn der Pointer den Kreis, das Diagramm oder das Browserfenster verlässt
  • Bei Maus-Events musste man für denselben Effekt Event-Handler an document registrieren; bei Pointer Events ist setPointerCapture(event.pointerId) einfacher
  • Ohne Pointer Capture kann der Drag stoppen, wenn man schnell zieht oder das Diagramm verlässt und wieder zurückkehrt
  • Auch wenn die Taste außerhalb des Diagramms losgelassen wird oder man mit Alt+Tab in ein anderes Fenster wechselt, hängt die Behandlung des Drag-Endes davon ab, ob Capture aktiv ist
  • Auf Touch-Geräten ist Pointer Capture das Standardverhalten, bei der Maus muss es jedoch explizit gesetzt werden

Touch-Scrolling und preventDefault()

  • Auf Touch-Geräten löst ein Drag mit einem Finger standardmäßig Seitenscrolling aus
  • Wenn derselbe Ein-Finger-Drag auch den Kreis bewegt, treten Scrolling und Objekt-Dragging gleichzeitig auf
  • Die einfachste Lösung ist, auf das Diagramm per CSS touch-action: none anzuwenden; dann kann jedoch nirgends im Diagramm gescrollt werden
  • Gewünscht ist ein Verhalten, bei dem Scrolling nur beim Ziehen des Kreises verhindert wird, während Scrollen beim Ziehen des Diagramms selbst erlaubt bleibt
  • Das Vergleichsergebnis lautet wie folgt
    • touch-action: none auf dem gesamten Diagramm: Sowohl Kreis-Drag als auch Diagramm-Drag stoppen das Scrolling
    • Standardwert: Keines von beiden verhindert Scrolling
    • touch-action: none nur auf dem Kreis: Funktioniert nicht wie erwartet
    • preventDefault() in touchstart: Verhindert nur während des Kreis-Drags das Scrolling und erlaubt das Scrollen des Diagramms
  • preventDefault() wird an touchstart gehängt, nicht an pointerstart
  • Dieser Ansatz funktioniert in den meisten Situationen, jedoch nicht im „mobile emulation mode“ von Desktop-Firefox; im mobilen Firefox auf Android funktioniert er

Start-Offset beim Draggen speichern

  • Wenn beim Ziehen am Rand eines Objekts dessen Mittelpunkt sofort zur Pointer-Position springt, fühlt sich die Bedienung schlecht an
  • Die Lösung besteht darin, beim Start des Draggens die relative Position zwischen Objektmittelpunkt und Pointer-Position zu speichern
  • Bei anschließenden Bewegungen wird die Objektposition berechnet, indem die gespeicherten Werte dx und dy zu den aktuellen Pointer-Koordinaten addiert werden
  • Dadurch wird state.dragging nicht mehr als einfaches true/false geführt, sondern als relative Positionsinformation wie {dx, dy}
  • Bei der Maus ist der Sprung beim Greifen am Rand deutlich sichtbar, bei Touch kann er weniger auffallen, weil der Finger die Stelle verdeckt
  • Ein verwandtes kleines Detail ist auch auf der Seite little details beschrieben

Kontextmenü und Maustasten

  • Wie das Kontextmenü aktiviert wird, unterscheidet sich je nach Plattform
    • Windows: Rechtsklick down+up, Shift+F10
    • Linux: rechte Taste down, Shift+F10
    • Mac: rechte Taste down, Ctrl+Linksklick
    • iOS: Long Press nur auf Text
    • Android: Long Press überall
  • Das Problem ist, dass pointerdown sichtbar ist, pointerup aber nicht immer
  • Die Taste kann tatsächlich bereits losgelassen sein, während der Code sie noch als gedrückt interpretiert
  • Die Gegenmaßnahme besteht darin, die rechte Taste zu ignorieren und den Zustand dragging nur bei der linken Maustaste zu setzen
  • In der Pointer-Events-Spezifikation bedeutet beim Attribut button der Wert button = 0 die primary button; damit werden mittlere und rechte Taste ausgeschlossen
  • In Chrome und Safari auf dem Mac kann Ctrl+Klick wie ein pointerdown der linken Taste ankommen, daher muss auch event.ctrlKey geprüft werden
  • Die Optionen sind folgende
    • Mit preventDefault() in contextmenu lässt sich das Menü unterdrücken, dies kann aber mit Standardaktionen für Mittelklick oder Dragging kollidieren
    • Wenn nur mit der linken Taste gezogen wird, lassen sich die meisten Unterschiede beim Rechtsklick vermeiden
    • Wer auch Mac Ctrl+Klick vermeiden will, muss zusätzlich ctrlKey ausschließen
  • Selbst wenn Änderungen der Tasten nicht über pointerdown oder pointerup übertragen werden, lassen sie sich teilweise über den Tastenzustand in pointermove prüfen

Objekte mit Text und Bildern

  • Wenn ein ziehbares Element Text oder Bilder enthält, ist zusätzliche Behandlung nötig
  • Während ein Kreis gezogen wird, kann der darin enthaltene Text ausgewählt werden
  • Die Lösung besteht darin, per CSS user-select: none anzuwenden
    • Wird es immer angewendet, ist der darin enthaltene Text nie auswählbar
    • Wird es nur während des Draggens angewendet, bleibt die Textauswahl im Normalzustand möglich
  • Stand Anfang 2023 unterstützt Safari user-select ohne Präfix noch nicht, daher muss auch webkitUserSelect gesetzt werden
  • Windows, Linux und Mac unterstützen Drag and Drop von Text und Bildern zwischen Anwendungen, was mit dem Ziehen von Objekten kollidiert
  • Wenn in dragstart preventDefault() aufgerufen wird, kann das Objekt-Dragging Vorrang vor dem Ziehen von ausgewähltem Text oder Bildern erhalten
  • Je nach Browser und Betriebssystem unterscheidet sich das Verhalten leicht

Mehrere Pointer und gleichzeitiges Draggen

  • Ob sich mit mehreren Fingern oder mehreren Mäusen mehrere Objekte gleichzeitig ziehen lassen, ist ein Edge Case
  • Bei Touch funktioniert der gezeigte Code bereits zum gleichzeitigen Ziehen mehrerer Objekte
  • Wenn zwei Finger dasselbe Objekt ziehen, ist eine separate Behandlung nötig
  • Die Lösung besteht darin, in pointerdown event.pointerId in state.dragging oder im Zustand zu speichern und in pointermove alles zu ignorieren, was nicht dieselbe pointerId hat
  • Dieses Implementierungsbeispiel ist im Haupttext nicht vollständig angewendet, kann aber im canvas dragging test nachvollzogen werden
  • Für Mäuse hält die Pointer-Events-Spezifikation fest, dass normale Betriebssysteme und User Agents in der Regel kein Konzept mehrerer Mauseingaben haben
  • Selbst wenn Trackpad und externe Maus gleichzeitig vorhanden sind, werden sie normalerweise als ein Mauszeiger behandelt; daher wird davon ausgegangen, dass es keine Möglichkeit gibt, mit unterschiedlichen Mäusen unterschiedliche Objekte zu ziehen

Wenn mehrere Maustasten gleichzeitig gedrückt werden

  • Werden mehrere Maustasten gleichzeitig gedrückt, unterscheidet sich das Verhalten von Pointer Events und Mouse Events
  • Mouse Events senden bei jedem Tastendruck mousedown und bei jedem Loslassen einer Taste mouseup
  • Laut Pointer-Events-Spezifikation wird pointerdown bei der zuerst gedrückten Taste und pointerup bei der zuletzt losgelassenen Taste ausgelöst
  • Dadurch können pointerdown und pointerup bei unterschiedlichen Tastenzuständen auftreten
  • Wenn man zum Beispiel die linke Taste drückt, dann die rechte Taste drückt und anschließend die linke Taste loslässt, kann das Objekt weiter im Dragging-Zustand bleiben
  • Die Lösung besteht darin, in pointermove mit event.buttons & 1 den Zustand der linken Taste zu prüfen und end(event) aufzurufen, wenn sie nicht mehr gedrückt ist
  • Pointer Capture kann bestehen bleiben, bis alle Tasten losgelassen wurden, und ohne explizite Freigabe der Capture weiterlaufen
  • Diese Implementierung behandelt nicht dieses Problem und alle anderen Edge Cases vollständig

Verschachtelte Drag-Elemente

  • Wenn sich in einem ziehbaren Element ein weiteres ziehbares Element befindet, verarbeiten sowohl das innere als auch das äußere Element das Dragging
  • Die Lösung besteht darin, im inneren Drag-Element event.stopPropagation() aufzurufen, damit das Event nicht zum äußeren Drag-Element aufsteigt
  • Dabei wird die Weitergabe sowohl in pointerdown als auch in pointermove gestoppt
  • Eine Demo ist im Haupttext nicht enthalten, aber im Vue-playground-Beispiel ist ein Fall zu sehen, in dem das rote Drag-Element ein Kind des gelben Drag-Elements ist

Drag in Canvas

  • Normalerweise verwendet man SVG, aber in <canvas> lassen sich keine Event-Handler oder Mauszeiger-Formen direkt auf den ziehbaren Elementen selbst setzen
  • Bei 2D Canvas oder WebGL muss man Event-Handler an <canvas> hängen und selbst prüfen, ob die Koordinaten über einem ziehbaren Objekt liegen
  • Die Verarbeitung läuft wie folgt ab
    • pointerdown, touchstart, dragstart: sofort zurückkehren, wenn der Pointer nicht über einem ziehbaren Objekt liegt
    • pointermove: den Cursor abhängig davon setzen, ob der Pointer über einem ziehbaren Objekt liegt
  • Die Demo befindet sich auf der examples page

Kombination aus Maus-Hover und Touch-Drag

  • Manche Diagramme reagieren bei der Maus auf Hover ohne gedrückte Taste; auf Touch-Geräten gibt es kein Hover, daher müssen sie auf Drag reagieren
  • Das Grundrezept erwartet sowohl bei Maus als auch bei Touch einen Drag
  • Um Maus-Hover und Touch-Drag gemeinsam zu verwenden, entfernt man in pointermove die Bedingung if (!state.dragging)
  • Auf der Responsive Design page ändert sich das Layout, wenn man mit der Maus über eine Zeile hovert; bei Touch ändert sich das Layout per Drag
  • Auch im Hexagons Guide verwenden viele Diagramme Maus-Hover und Touch-Drag gemeinsam
  • Wenn beim Ziehen auf Touch von Element A zu B die Capture erhalten bleibt, werden Move-Events an A zugestellt; um B hervorzuheben, kann es daher nötig sein, die Capture freizugeben

Toggle Paint und Erweiterung des Drag-Zustands

  • Beim Rounded Cell Painter erfasst pointerdown die anfängliche Malfarbe
  • Danach verwenden alle pointermove-Events bis pointerup dieselbe Malfarbe
  • Dafür muss state.dragging neben dem anfänglichen x und y auch die anfängliche Malfarbe enthalten
  • Die Demo befindet sich auf der examples page

Edge Case bei lostpointercapture

  • lostpointercapture kann verwendet werden, um zu erkennen, ob die Pointer-Capture verloren gegangen ist
  • In welchen Situationen das auftritt und was man dann tun sollte, ist noch nicht vollständig geklärt
  • Das in den Kommentaren behandelte Szenario sieht so aus
    • Drag eines Kreises mit der linken Taste starten
    • Mit gedrückter linker Taste aus dem Drag-Bereich heraus bewegen
    • Die rechte Taste drücken und anschließend die linke Taste loslassen
    • Irgendwo außerhalb des Drag-Bereichs klicken
  • Der standard besagt, dass der User Agent nach Freigabe der Pointer-Capture zwingend ein lostpointercapture-Event auslösen muss
  • Das tatsächliche Browser-Verhalten unterscheidet sich stark: Auf manchen Systemen tritt lostpointercapture auf, auf anderen erst, wenn an anderer Stelle geklickt wird

1 Kommentare

 
GN⁺ 2023-09-30
Meinungen auf Hacker News
  • Ein Artikel über Dragging; ich bin auf fast alle Fallstricke gestoßen, die Amit nennt, und bei denselben Lösungen gelandet. Hervorragender Artikel.
    Aus Nutzersicht wirkt Reihenfolge per Drag-and-drop ändern völlig selbstverständlich, aber sobald man sich mit unterschiedlich großen Elementen, Platzhaltern zwischen Elementen, Kantenerkennung und dem Kaninchenbau von Box-Fitting-Algorithmen beschäftigt, gehört das schon zu den ziemlich schwierigen Dingen, die man selbst implementieren kann.

    • Für solche Fälle habe ich einen recht allgemeinen Lieblingstrick: Wenn der Nutzer mit dem Ziehen beginnt, berechnet man alle möglichen Drop-Ziele bzw. die Layouts der aktuell sichtbaren Ziele und merkt sich für jedes Layout, wo das gezogene Objekt am Ende landen würde.
      Bei jeder Mausbewegung wählt man aus diesen Positionen diejenige aus, die der aktuellen Position des gezogenen Objekts am nächsten liegt, und rendert das ausgewählte Layout. Das funktioniert auch bei komplexen Layouts oder Reflows mit gutem Gefühl.
    • In meiner Karteikarten-App Card Buddy musste ich dieses Problem lösen. [1] Es war eine interessante Herausforderung, und später habe ich auch eine bessere Methode gefunden.
      Am Ende habe ich das Layout so berechnet, als würde die Karte beim Aufheben vom Board gelöscht; dadurch wurde es einfacher. Man muss dann nur noch die Dinge wegschieben, die sich an der Stelle befinden, über der die Maus ist.
      Trotzdem gab es enorm viele Sonderfälle, vor allem wenn man begann, eine neue Karte zu bearbeiten, die noch nicht ins Datenmodell „committed“ war. Ich musste auch eine Option hinzufügen, vorhandene Karten wegzuschieben, um Platz für eine Geisterkarte zu schaffen.
      Es hilft, anzuerkennen, dass es viele Sonderfälle gibt, die man manuell behandeln muss. Wenn man mit der Annahme herangeht, es müsse eine allgemeinere, homogenere Lösung geben, dreht man sich im Design im Kreis. Irgendwann sollte ich die verschiedenen Sonderfälle wohl in einem Blogpost zusammenfassen.
      Ich habe auch eine bessere Layout-Methode gefunden, die unnötige Berechnungen reduziert und die Layout-Engine flexibler und nutzerfreundlicher macht. Es ist erstaunlich, wie stark die Wahl der Datenmodell-Repräsentation die Lösung beeinflusst.
      [1] https://www.ussherpress.com/cardbuddy/
    • Wenn man bei der Neuordnung per Drag-and-drop auch noch Verschachtelung unterstützen will, wird es noch schwieriger.
    • Sehe ich genauso. Die Idee ist simpel, aber die Implementierung kann brutal sein. Auch Drag-Auswahl habe ich jahrelang für viel zu selbstverständlich gehalten.
  • Der Artikel scheint Abbruchbehandlung nicht zu behandeln. Unter Windows gibt es zum Beispiel die Konvention, dass ein Drag-Vorgang mit Escape abgebrochen wird.
    Manchmal möchte man einen Drag-Vorgang auch abbrechen, wenn die Maus außerhalb eines definierten Bereichs losgelassen wird. Abbrechen bietet Nutzern ein schnelleres Undo oder einen Ausweg nach dem Motto: „Ach, eigentlich wollte ich gar nicht ziehen.“
    Daher sollte man selbst dann, wenn es keine separate Undo-Funktion gibt, den ursprünglichen Zustand zum Zeitpunkt des Drag-Starts speichern und bei einem Abbruch wiederherstellen.
    Beim Abbruch durch Ziehen außerhalb des Bereichs gibt es außerdem eine Abbruch-Aufhebung: Solange sich der Pointer außerhalb des Bereichs befindet, zeigt man visuell an, als sei der ursprüngliche Zustand wiederhergestellt; wenn der Pointer wieder in den Bereich zurückkehrt, wird der Drag-Vorgang fortgesetzt. Der eigentliche Abbruch passiert erst beim Loslassen der Maus, aber schon während des Ziehens wird visuell vorweggenommen, dass abgebrochen würde.

    • Unter Windows gibt es ein Verhalten, bei dem, wenn man etwas zu weit zieht, das gezogene Objekt und der Pointer plötzlich verschwinden und es ohne Warnung an seine ursprüngliche Position zurückspringt.
      Ich kann kaum glauben, dass Microsoft das immer noch für gutes UI-Design hält. Es verwirrt Nutzer völlig; man sieht Leute, die nicht wissen, was passiert, Angst haben, die linke Maustaste loszulassen, oder das Ganze wie ein Jo-Jo wiederholen.
      Ich stimme zu, dass Abbrechen ein großes, oft übersehenes Problem ist und nicht trivial. Normale Nutzer wissen nicht einmal, dass ESC helfen kann.
    • Ich frage mich, ob es auf anderen Plattformen neben ESC noch eigene Abbruch-Tasten gibt, die man behandeln muss.
  • Aus 20 Jahren Erfahrung damit, in der GUI von Ardour [0] alle möglichen Objekte zu ziehen, habe ich ein weiteres Detail gelernt: Jedes Button-Down- und Button-Up-Event sollte als Drag-Event ohne Bewegung behandelt werden.
    Das heißt: Ein Down startet immer einen Drag, ein Up beendet ihn, und der Code, der das Up verarbeitet, behandelt die Bedingung „keine Bewegung“ als Sonderfall.
    [0] https://ardour.org/

    • Dieser Ansatz funktioniert sicher auch, aber ich habe es nicht so gemacht und hatte keine Probleme, nachdem ich die Grundlagen richtig umgesetzt hatte.
      Ich habe normale Handler für mousedown und mouseup und nutze ein konfigurierbares Timeout von etwa 150 ms, um zu entscheiden, ob es ein Klick, der Beginn eines Drags oder eine andere Aktion ist. Wenn mouseup ausgelöst wird oder die Maus den Bildschirm verlassen hat und ein Drag läuft, mache ich stopdrag. Für mich ist Dragging nur einer von mehreren Sonderfällen.
    • Mich würde interessieren, warum man das so macht.
  • Vor sehr langer Zeit gab es in Minneapolis ein von der NSF gefördertes Geometry Center. Dort fand eine Tagung zur computergestützten Gruppentheorie statt, und man hatte ein paar maskottchenartige Leute aus angrenzenden Bereichen eingeladen.
    Ich wurde eingeladen, weil ich ein System für algebraische Geometrie geschrieben hatte, stand bei -20 °F sehr früh auf, sicherte mir die Silicon-Graphics-Workstation, die ich an diesem Tag benutzen würde, und programmierte ein Spiel, um Gruppengeneratoren und Relationen besser zu verstehen.
    In diesem Spiel gab es Dragging. Daraus entstanden gute Gespräche bis 2 Uhr morgens. Meine Ansicht war, dass Dragging nicht den physikalischen Gesetzen der realen Welt folgen muss; Dragging sollte sich anfühlen wie ein sehr starker LSD-Tab. Alle stiegen darauf ein und brachten ihre eigenen Ideen ein.

  • Der Artikel implementiert, wenn man die vielen Ausnahmen im Webbrowser einmal beiseitelässt, ein relativ grundlegendes Dragging. Ich frage mich, ob es Material zu Dragging mit Einschränkungen gibt, etwa Einrasten an Leitlinien, Verhindern von Kollisionen mit Begrenzungen oder anderen Objekten, oder animierte Drop-Ziele, deren Größe oder Position sich als Reaktion auf den Drag-Vorgang ändert.
    Früher wollte ich einmal eine angepasste Pomodoro-Timer-UI bauen, bei der eine runde Uhr in keilförmige Stücke unterschiedlicher Länge unterteilt wird, um Fokus-/Pausenintervalle zu definieren. Bei der Umsetzung des Umordnens dieser Keile per Drag kam ich nicht besonders weit.

    • Bei diesem Code-Ansatz aktualisiert die Drag-Bewegung einen Zustand. Beim Setzen des Zustands kann man Constraints anwenden, und dieser Zustand treibt dann wiederum die Darstellung an.
      Ich wollte das Constraint-System vom Event-Handling-System trennen. Bibliotheken wie jquery-ui koppeln beides, sodass das Event-Handling-System alle möglichen Constraints kennen muss. jquery-ui unterstützt Bounding Boxes, Achsen, quadratische Raster und das Einrasten an DOM-Elementen. Wenn man aber an ein hexagonales Raster, ein logarithmisch skaliertes Raster oder eine kreisförmige Begrenzung einrasten will, wird das nicht unterstützt.
      Im Code sieht man state.pos = …; dort wird der Zustand gesetzt. Für Constraints würde ich pos hinter einem Setter verstecken. Dann kann der Setter die Constraints anwenden, ohne dass der Code zur Verarbeitung der Drag-Events wissen muss, welche Constraints benötigt werden.
      Ich sollte die Seite aktualisieren und ein Constraint-Beispiel zeigen. Ich habe völlig vergessen, diesen Aspekt im Code-Rezept zu erwähnen.
      Ein älteres Constraint-Beispiel gibt es unter https://www.redblobgames.com/articles/curved-paths/making-of..., und ein Beispiel zur Kollisionsvermeidung unter https://redblobgames.github.io/circular-obstacle-pathfinding.... Umordnen per Drag oder animierte Drop-Ziele habe ich allerdings noch nicht ausprobiert.
  • Dieser Website bin auch ich dankbar. Als wir bei Uber ein Hexagon-System für Geoanalysen entwarfen, habe ich mich stark auf https://www.redblobgames.com/grids/hexagons/ gestützt.

  • Ein guter Artikel, der die Fallstricke und Dinge, auf die man achten muss, wenn man echte Interaktionen umsetzt, gut zusammenfasst.
    Für etwas, das eher „sofort einsatzbereit“ ist, habe ich in mehreren Projekten über ziemlich lange Zeit interactjs verwendet.

  • Danke an alle. Ich war überrascht, es heute auf HN zu sehen, und kann Fragen beantworten.

    • Ich frage mich, ob du dir das eingebaute HTML-Attribut draggable="true" und Drag-Events angesehen hast.
      Mich würde auch interessieren, wo die eingebaute Funktionalität im Vergleich zu diesem Code zu kurz kommt.
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
      https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att...
    • Das war eine hervorragende Diskussion über JavaScript-Dragging, und die Informationen waren wirklich nützlich.
      Ich habe eine kurze Frage. Wie kann man mit eingebauten DOM-Events wie dragstart die Drag-Bewegung auf eine Achse beschränken? Ich habe Drag-and-Drop-Funktionalität mit den Events dragstart/dragenter/dragover/drop implementiert, aber keinen schnellen Weg gefunden, die Bewegung auf die x-Achse zu beschränken. Ich meine mich zu erinnern, dass Javascripts Drag-and-Drop-API von jQuery das unterstützt hat. Ich möchte nur native DOM-Events/APIs verwenden, daher wären entsprechende Informationen oder Hinweise hilfreich.
    • Früher habe ich ständig Browser-UI-Code geschrieben, aber dann ein paar Jahre pausiert. Das ist das erste Mal, dass ich jemanden von Pointer Events sprechen höre.
      Angesichts der Tatsache, dass Safari erst 2020 Unterstützung hinzugefügt hat, frage ich mich, ob du dir keine Sorgen machst, dass Browser sie nicht unterstützen. Nutzer von macOS Mojave dürften Safari 13 oder 14 verwenden, also hoffe ich, dass Safari 12 in der Praxis nicht mehr genutzt wird; aber wenn in einer Produktions-App für normale Webnutzer eine so einfache Funktion wie Dragging nicht funktioniert, wäre das ziemlich schlecht.
      Während des Draggings Event-Handler auf document hinzuzufügen, ist eine alte Praxis, und Browser fügen ständig neue Funktionen hinzu, um bestimmte Anwendungsfälle zu vereinfachen, aber jede davon hat ihre eigenen Ausnahmen und Fallstricke. Auch der Artikel sagt, dass er das noch nicht vollständig behandelt. Im Endergebnis bleibt sogar eine Kombination aus Pointer Events und Touch Events übrig.
      Ich frage mich, ob „Einfachheit“ im Sinne von weniger Code es wert ist, dafür zusätzliche Ausnahmen, geringere Browser-Unterstützung und den Aufwand in Kauf zu nehmen, dass Entwickler Details und Browser-Unterschiede von Pointer Events kennen müssen, die vermutlich weniger gut verstanden und dokumentiert sind als Mouse Events.
  • Eines fehlt noch: Barrierefreiheit. Wie sollte man Dragging per Tastaturbedienung ermöglichen?

    • Das wirkt hier wie eine etwas am Thema vorbeigehende Frage. Es ist ungefähr so, als würde man fragen, wie ein Tastaturnutzer in einem Zeichenprogramm das Klicken-und-Halten ausführen soll, um die Leinwand zu pannen. Das macht man nicht so. Mit der Maus zu pannen oder Objekte zu ziehen ist nur eine Schnittstelle; die Aufgabe ist das Pannen der Leinwand, und dafür gibt es verschiedene Wege.
      In meinem Beispiel wäre die Antwort für Barrierefreiheit, zusätzlich eine Tastaturbedienung bereitzustellen, mit der sich die Aufgabe erledigen lässt. Wenn möglich, ohne dass man eine Taste dauerhaft gedrückt halten muss. Zum Beispiel könnte man per Tastenkürzel in einen Panning-Modus wechseln und dann mit den Pfeiltasten den Viewport über die Leinwand bewegen; damit wäre das Problem gelöst.
      Bei ziehbaren Objekten ist die Aufgabe das Ändern der Reihenfolge. Um das barrierefrei zu machen, stellt man eine alternative Möglichkeit bereit, die Reihenfolge zu ändern. Zum Beispiel kann man mit TAB den Fokus zwischen den Objekten durchschalten, mit einer Taste das fokussierte Objekt auswählen und dann mit den Pfeiltasten die Drag-Vorschau zur nächstgelegenen gültigen Position in der jeweiligen Richtung verschieben.
    • Im Großen und Ganzen lautet die Antwort eher: „Das macht man nicht so.“ Die UI sollte eine Alternative zum Dragging bieten, damit auch Menschen ohne Pointer- oder Touch-Gerät mit der Seite interagieren können.
      Das heißt: Tastaturunterstützung nicht mit Gewalt in die Drag-Implementierung hineinzwängen, sondern zusätzlich zur Drag-Funktion eine sinnvolle Tastaturfunktion bereitstellen.
    • Das wäre dann eher ein verschiebbares Objekt, und auf Basis dieser Implementierung ließe sich das vermutlich leicht bauen.
    • Man wählt ein Element mit TAB / Space / Enter aus und bewegt das „gezogene“ Element mit Ctrl + Pfeiltasten. Natürlich kann es pro Tastendruck nur um einen Block verschoben werden.
  • Gut. Anfang der Woche habe ich in einer Vue-App SVG-Element-Dragging implementiert, und ich musste fast alles, was der Autor beschreibt, in derselben Reihenfolge selbst herausfinden; am Ende ist es auch fast dieselbe Komponente geworden.
    Allerdings habe ich es statt als funktionale Komponente als composable Utility namens useDragging geschrieben.

    • Schön. Eine Komponente mit Directive und Slot habe ich ausprobiert, aber ein composable noch nicht. Ich war mir nämlich nicht sicher, wie man auf diese Weise die Event-Handler einrichten sollte. Mit composables habe ich nicht sehr viel Erfahrung.