bpmn-js-touch-interaction
v0.1.2
Published
Restores tap, pan, pinch and drag gestures on touch devices for bpmn-js 18+
Maintainers
Readme
bpmn-js-touch-interaction
Touch gestures for bpmn-js 18+ on phones and tablets.
Install
npm install bpmn-js-touch-interactionPeer dependency: bpmn-js@^18. diagram-js-minimap is an optional peer that
enables minimap touch support when present.
Usage
import BpmnModeler from 'bpmn-js/lib/Modeler';
import touchInteractionModule from 'bpmn-js-touch-interaction';
const modeler = new BpmnModeler({
container: '#canvas',
additionalModules: [touchInteractionModule],
});If you want to keep it out of your desktop bundle, dynamic-import it:
const { default: touchInteractionModule } =
await import('bpmn-js-touch-interaction');Example
A runnable Modeler + NavigatedViewer example with auto-sync, minimap, and
zoom controls lives in examples/index.html.
npm run build # build dist/ once
npm run example # serves repo root on http://localhost:8000Then open http://localhost:8000/examples/index.html.
TLDR;
bpmn-js dropped touch support in v14 (May 2024) when the HammerJS-based implementation was removed as unmaintainable (announcement, diagram-js#845).
This plugin restores it using:
- A pure-function gesture recognizer (no HammerJS, no third-party gesture library, just native Touch Events)
- Direct calls to diagram-js APIs (
canvas.scroll,canvas.zoom,move.start,interactionEvents.fire) - A small set of scoped monkey-patches that work around mouse-only assumptions in diagram-js (see Caveats)
Note: I'm not proud of these monkey-patches, but I hope to file small PRs upstream so they can eventually go away.
Gestures
| Gesture | Action | | --------------------------------------------- | ---------------------------------- | | Tap element | Select element | | Double-tap element | Open label editor | | Pan on empty canvas | Scroll viewport | | Pan on element | Move element | | Two-finger pinch | Zoom (0.2x – 4x) | | Drag from palette | Place element at drop position | | Tap palette entry | Activate tool / enter create mode | | Tap connect tool, then touch-drag source→dest | Create connection (single gesture) | | Tap lasso tool, then touch-drag | Draw selection rectangle | | Tap space tool, then touch-drag | Make or remove space | | Touch-drag a resize handle | Resize element | | Touch-drag on minimap | Pan via minimap |
Auto-detection
window.matchMedia('(pointer: coarse)') is used for activation:
| Device | (pointer: coarse) | Plugin active |
| ---------------------------------- | ------------------- | ------------- |
| Phone, tablet | true | yes |
| Desktop with mouse | false | no |
| Laptop with touchscreen + trackpad | false | no |
| iPad with attached keyboard/mouse | true | yes |
We don't rely on ontouchstart in window — it happen to return true on hybrid laptops where touch is secondary and the plugin would break the desktop mouse tool flow on those machines.
How it works
The plugin is a thin coordinator (lib/core/TouchInteraction.js) that bounds everything together:
lib/
├── core/
│ ├── TouchInteraction.js coordinator
│ └── TouchFix.js Safari iOS empty-canvas workaround
├── recognizers/
│ ├── gestureRecognizer.js pure tap/pan/pinch/press/double-tap state machine
│ └── buttonRecognizer.js gesture wiring for palette and context pad
├── patches/
│ ├── toolMode.js single-gesture connect / lasso / space
│ ├── toolEventPatcher.js fix NaN coords from touchend originalEvent
│ ├── hoverTracking.js drag-time hover for touch (HoverFix is mouse-only)
│ └── mouseSuppression.js block synthesized mouse events during touch
├── bridges/
│ └── minimapTouchBridge.js mouse-event synthesis for diagram-js-minimap
├── util/
│ ├── eventHelpers.js event wrappers for diagram-js compatibility
│ ├── device.js (pointer: coarse) detection
│ └── constants.js zoom bounds, suppression timing, event names
└── styles/
└── touch-interaction.css touch-action: none + active-tool cursorCaveats
Touch-primary devices only
The plugin is a no-op on mouse-primary devices, including hybrid touchscreen
laptops where the trackpad is the primary input. The patches it installs would
break the desktop mouse tool flow if enabled there. To force-enable for
testing, use Chrome DevTools mobile emulation, which sets (pointer: coarse).
Monkey-patch surface
The plugin patches the following diagram-js services. All patches are scoped
to the Modeler instance, never global. Search the source for // PATCH: to
find each one with its rationale.
| Patched symbol | Reason |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| globalConnect.start | Replaced with a flag-setter so connect uses single-gesture flow instead of desktop two-step. |
| lassoTool.activateSelection | Same: single-gesture lasso. |
| spaceTool.activateSelection | Same: single-gesture space tool. |
| resizeHandles.makeDraggable | Original bails on isPrimaryButton(event), which returns false for TouchEvent. Replacement skips the check on touch. |
| dragging.init | Injects keepSelection: true for resize drags to keep handle DOM nodes alive mid-drag. |
| connect.start | Substitutes a synthetic touchstart for the null event passed by globalConnect.start, so Dragging binds touch handlers. |
These patches target private implementation details of diagram-js. They are
documented and scoped, but an upstream change to any patched method or its
call sites could silently break the corresponding gesture. Verified against
[email protected] (the version shipped with [email protected]).
diagram-js mouse-only assumptions
Eight assumptions in diagram-js needed workarounds:
isPrimaryButton(event)checksevent.button === 0. TouchEvent has no.button, so the check always fails and events are silently dropped. Fix:asPrimaryButtonEvent()inutil/eventHelpers.js.toPoint(event)falls back toevent.clientX/clientYontouchend, whereevent.touchesis empty, producing NaN coordinates. Fix:patches/toolEventPatcher.jssubstitutes valid coordinates fromchangedTouches.isTouchEvent(event)usesinstanceof TouchEvent. Plain object wrappers fail this check. Fix:createSyntheticTouchEvent()inutil/eventHelpers.jsconstructs realTouchEventinstances.element.outis mapped frommouseout, which never fires on touch. Fix:patches/hoverTracking.jsreimplements hover/out detection for touch drags.- The two-step desktop interaction pattern (palette tap → canvas tap → drag)
does not map to touch.
Fix:
patches/toolMode.jscollapses it to a single gesture (tap palette, then touch-drag). Dragging.stopPropagation()in capture phase prevents bubble-phase gesture recognizers from receiving subsequent events, leaving the recognizer stuck. Fix:gestureRecognizer.jsresets stuck state on a newtouchstart.- Tool-end events pass
originalEvent(atouchendwith emptytouches) to the nextdragging.initcall, producing NaN coordinates. Fix:patches/toolEventPatcher.js. autoActivate: truewithoutkeepSelection: trueremoves decorations mid-drag, which firestouchcancelon the removed DOM node and ends the drag prematurely. Fix:dragging.initpatch incore/TouchInteraction.js.
Browser support
Requires native TouchEvent and the Touch constructor. Tested on Chrome on
Android and Safari on iOS. The minimap touch bridge needs Touch/TouchEvent
constructors; primary canvas gestures use real browser TouchEvents and work
without them.
License
MIT
