Managing user interactions
User interactions via mouse, keyboard and touch devices trigger many different behaviors in the application. Depending on the currently active tools or modes, these events can overlap each other and cause unwanted behavior and side effects.
The UserInteractionManager
is the central place where event listeners are registered and reassignment of mouse/keyboard control
between different components and tools is managed.
UserInteractionManager
can manage the following kind of events:
- Mouse primary click, alternate click and double click
- Mouse movement
- Mouse wheel
- Mouse wheel buttons
- Any mouse action in combination with modifier keys
- More complex interactions consisting of simpler events like openlayers draw or modify
- Keyboard events including modifier keys
The UserInteractionManager
is responsible for:
- registering event listeners in the state
- reassigning event listening depending on active tool or mode
- allowing parallel or exclusive listening to events
- restoring defaults once tools deactivate or close
Guidelines
For user interaction management to work, these guidelines have to be followed when creating listeners for interaction events:
- Register the listener with the
UserInteractionManager
- Whenever the event triggers, ask for permission before reacting to the event
- Unregister listeners once the tool deactivates or closes
Unit tests enforce these guidelines by searching and comparing event listeners and listener registrations in the code base.
1. Register listener
Register listeners when the tool activates or becomes visible.
When working with a class that extendsGirafeHTMLElement
, the helper function registerInteraction()
can be used.
Decide if the listener should be exclusively listening for the event (isExclusive = true
), or if other tools can react as well.
// In GirafeHTMLElement
this.registerInteraction('map.mouseclick', true);
// Outside GirafeHTMLElement
UserInteractionManager.getInstance().registerListener('map.mouseclick', true, 'nameOfTool');
If registering a listener as exklusive, any other listener for this event will be disabled until the tool is closed again
2. Ask for permission when event triggers
Before the event can be processed, each listener must ask for permission with the UserInteractionManager
.
// In GirafeHTMLElement
document.addEventListener('click', (e) => {
if (!this.canExecute('map.mouseclick')) {
return;
}
// React to event
});
// Outside GirafeHTMLElement
document.addEventListener('click', (e) => {
if (!UserInteractionManager.getInstance().canExecute('map.mouseclick', 'nameOfTool')) {
return;
}
// React to event
});
Many openlayers interactions (draw, modify, etc.) provide a condition
option that can be used to make the permission check:
this.modifyInteraction = new Modify({
source: this.linestringSource,
condition: (e) => primaryAction(e) && this.canExecute('map.modify')
});
3. Unregister on close
When working in a GirafeHTMLElement
class, unregistering isn't necessary because the component does it automatically in its hide()
function.
Anywhere else, use:
UserInteractionManager.getInstance().unregister('map.mouseclick', 'nameOfTool');
Event names: GgUserInteractionEvent
To help define and identify events across components and tools of GeoGirafe, the type GgUserInteractionEvent
is provided.
It is a list of strings that describe common mouse and keyboard events including optional modifier keys.
Examples:
'map.mouseclick'
'map.mouseclick.ctrl'
'keydown.P'
'keypress.ctrl.X'
'map.draw'
'map.modify'
'globe.draw'
GgUserInteractionEvent
provides code suggestions and auto complete in IDEs and helps to reduce the chance for typos.
It is easily expandable if new events need to be registered and managed.
The type also contains some more complex interactions that trigger many different mouse events while the user interacts with the map,
like 'map.draw'
or 'map.modify'
.
The UserInteractionManager
handles this by defining gGEventDependencies
: When 'map.draw'
is registered exclusively,
the manager disables 'map.mouseclick'
and 'map.mousedoubleclick'
as well.
Interaction management in the state
The UserInteractionManager
stores listeners in the state by creating and saving GgUserInteractionListener
objects.
These objects identify the listener and describe its behaviour fully.
Every GgUserInteractionListener
object in the state must be unique.
eventName
: String of typeGgUserInteractionEvent
identifying the eventisExclusive
: Decides if this listener is the only one that can react to the event (true
). Iffalse
, multiple tools can listen for the event in paralleltoolName
: Name of component, manager or tool. When working with a component extendingGirafeHTMLElement
,this.name
is used
Typical use cases
Disable map feature selection
If feature selection on the map is interfering with other interactions of your component,
it can be deactivated by registering 'map.select'
as an exclusive listener.
Even if the component does not need feature selection functionality,
the exclusive registration will block the map from reacting to mouse clicks and therefore deactivating feature selection temporarily.
In the component add:
// GirafeHTMLElement
this.registerInteraction('map.select', true);
When the component closes, the GirafeHTMLElement
will automatically unregister the 'map.select'
listener,
which will in turn reactivate feature selection on the map.
Drawing and editing features on the map
First, the events are registered when the component is rendered visible. Usually, drawing and modifying interactions should be registered as exclusive, since having multiple draw interactions active at the same time will lead to unwanted side effects.
registerEvents() {
this.registerInteraction('map.draw', true);
this.registerInteraction('map.modify', true);
}
::: tip
Note that openlayers interactions are complex events that consist of multiple simpler events like mouse click or double click.
The UserInteractionManager
knows about this and will deactivate these simpler events as well.
:::
Then, create your openlayers interactions as usual but add a condition
that checks for execution permission.
primaryAction(e)
refers to the default condition to trigger the drawing interaction.
this.draw = new Draw({
source: this.drawingSource,
type: olTool as Type,
geometryFunction: geomFunction,
condition: (e) => primaryAction(e) && this.canExecute('map.draw'),
});
Don't forget to deactivate or remove the openlayers interaction from the map once the component closes.
// deactivate
this.draw.setActive(false);
// and/or remove
this.map.removeInteraction(this.draw);