-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Modularize support for raw and gestural input events (#636)
* First pass at new EventManager. Provides single API for both raw and gestural events; leverages hammer.js for both. Replaces use of luma.gl's addEvents. Does not yet reimplement support for dragging (i.e. dragging is broken as of this commit). * Consolidate "event" and "gesture" layers into a single layer by using manager.emit(). Alias basic and gestural events as necessary. Add documentation and cleanup. * Refactor wheel-input to be generic wheel input handler module instead of a hammer-specific implementation Wire refactored wheel-input into hammer.js event management in event-manager * Implement pointer/touch/mouse move events as a standalone module rather than extending Hammer input. This enables us to rely on Hammer's feature detection by not specifying an `inputClass`. * Bugfixes from @Pessimistress, address comments from @ibgreen. Break out constants into separate file. Add `destroy()` methods to input classes. * Consolidate event-manager-related classes into utils/events * Remove 300ms click delay Ensure concurrent registration of single and double tap handlers works properly * Clean up wheel-input. * Pass event map directly via constructor.
- Loading branch information
Showing
6 changed files
with
446 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { | ||
Tap, | ||
Press, | ||
Pinch, | ||
Rotate, | ||
Pan, | ||
Swipe | ||
} from 'hammerjs'; | ||
|
||
/** | ||
* Only one set of basic input events will be fired by Hammer.js: | ||
* either pointer, touch, or mouse, depending on system support. | ||
* In order to enable an application to be agnostic of system support, | ||
* alias basic input events into "classes" of events: down, move, and up. | ||
* See `_onBasicInput()` for usage of these aliases. | ||
*/ | ||
/* eslint-disable */ | ||
export const BASIC_EVENT_CLASSES = { | ||
down: ['pointerdown', 'touchstart', 'mousedown'], | ||
move: ['pointermove', 'touchmove', 'mousemove'], | ||
up: ['pointerup', 'touchend', 'mouseup'] | ||
}; | ||
/* eslint-enable */ | ||
|
||
export const BASIC_EVENT_ALIASES = { | ||
pointerdown: BASIC_EVENT_CLASSES.down, | ||
pointermove: BASIC_EVENT_CLASSES.move, | ||
pointerup: BASIC_EVENT_CLASSES.up, | ||
touchstart: BASIC_EVENT_CLASSES.down, | ||
touchmove: BASIC_EVENT_CLASSES.move, | ||
touchend: BASIC_EVENT_CLASSES.up, | ||
mousedown: BASIC_EVENT_CLASSES.down, | ||
mousemove: BASIC_EVENT_CLASSES.move, | ||
mouseup: BASIC_EVENT_CLASSES.up | ||
}; | ||
|
||
/** | ||
* "Gestural" events are those that have semantic meaning beyond the basic input event, | ||
* e.g. a click or tap is a sequence of `down` and `up` events with no `move` event in between. | ||
* Hammer.js handles these with its Recognizer system; | ||
* this block maps event names to the Recognizers required to detect the events. | ||
*/ | ||
export const EVENT_RECOGNIZER_MAP = { | ||
click: 'tap', | ||
tap: 'tap', | ||
doubletap: 'doubletap', | ||
press: 'press', | ||
pinch: 'pinch', | ||
pinchin: 'pinch', | ||
pinchout: 'pinch', | ||
pinchstart: 'pinch', | ||
pinchmove: 'pinch', | ||
pinchend: 'pinch', | ||
pinchcancel: 'pinch', | ||
rotate: 'rotate', | ||
rotatestart: 'rotate', | ||
rotatemove: 'rotate', | ||
rotateend: 'rotate', | ||
rotatecancel: 'rotate', | ||
pan: 'pan', | ||
panstart: 'pan', | ||
panmove: 'pan', | ||
panup: 'pan', | ||
pandown: 'pan', | ||
panleft: 'pan', | ||
panright: 'pan', | ||
panend: 'pan', | ||
pancancel: 'pan', | ||
swipe: 'swipe', | ||
swipeleft: 'swipe', | ||
swiperight: 'swipe', | ||
swipeup: 'swipe', | ||
swipedown: 'swipe' | ||
}; | ||
|
||
export const RECOGNIZERS = [ | ||
[Rotate, {enable: false}], | ||
[Pinch, {enable: false}, ['rotate']], | ||
[Pan, {threshold: 10, enable: false}], | ||
[Swipe, {enable: false}], | ||
[Press, {enable: false}], | ||
[Tap, {event: 'doubletap', taps: 2, enable: false}], | ||
[Tap, {enable: false, interval: 0}] | ||
]; | ||
|
||
/** | ||
* Map gestural events typically provided by browsers | ||
* that are not reported in 'hammer.input' events | ||
* to corresponding Hammer.js gestures. | ||
*/ | ||
export const GESTURE_EVENT_ALIASES = { | ||
click: 'tap' | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import {Manager} from 'hammerjs'; | ||
|
||
import { | ||
BASIC_EVENT_ALIASES, | ||
EVENT_RECOGNIZER_MAP, | ||
RECOGNIZERS, | ||
GESTURE_EVENT_ALIASES | ||
} from './constants'; | ||
import WheelInput from './wheel-input'; | ||
import MoveInput from './move-input'; | ||
|
||
/** | ||
* Single API for subscribing to events about both | ||
* basic input events (e.g. 'mousemove', 'touchstart', 'wheel') | ||
* and gestural input (e.g. 'click', 'tap', 'panstart'). | ||
* Delegates event registration and handling to Hammer.js. | ||
* @param {DOM Element} element DOM element on which event handlers will be registered. | ||
* @param {Object} options Options for instantiation | ||
* @param {Object} options.events Map of {event name: handler} to register on init. | ||
* @param {Object} options.recognizers Gesture recognizers from Hammer.js to register. | ||
* Not yet implemented. | ||
*/ | ||
export default class EventManager { | ||
constructor(element, options) { | ||
// TODO: support overriding default RECOGNIZERS by passing | ||
// recognizers / configs, keyed to event name. | ||
|
||
this._onBasicInput = this._onBasicInput.bind(this); | ||
this.manager = new Manager(element, {recognizers: RECOGNIZERS}) | ||
.on('hammer.input', this._onBasicInput); | ||
|
||
this.aliasedEventHandlers = {}; | ||
|
||
// Handle events not handled by Hammer.js: | ||
// - mouse wheel | ||
// - pointer/touch/mouse move | ||
this._onOtherEvent = this._onOtherEvent.bind(this); | ||
this.wheelInput = new WheelInput(element, this._onOtherEvent); | ||
this.moveInput = new MoveInput(element, this._onOtherEvent); | ||
|
||
// Register all passed events. | ||
const {events} = options; | ||
if (events) { | ||
this.on(events); | ||
} | ||
} | ||
|
||
destroy() { | ||
this.wheelInput.destroy(); | ||
this.moveInput.destroy(); | ||
this.manager.destroy(); | ||
} | ||
|
||
/** | ||
* Register an event handler function to be called on `event`. | ||
* @param {string|Object} event An event name (String) or map of event names to handlers. | ||
* @param {Function} [handler] The function to be called on `event`. | ||
*/ | ||
on(event, handler) { | ||
if (typeof event === 'string') { | ||
// Special handling for gestural events. | ||
const recognizerEvent = EVENT_RECOGNIZER_MAP[event]; | ||
if (recognizerEvent) { | ||
// Enable recognizer for this event. | ||
this.manager.get(recognizerEvent).set({enable: true}); | ||
|
||
// Handle concurrent single and double tap registration as necessary. | ||
this._reconcileSingleAndDoubleTap(recognizerEvent); | ||
|
||
// Alias to a recognized gesture as necessary. | ||
const eventAlias = GESTURE_EVENT_ALIASES[event]; | ||
if (eventAlias && !this.aliasedEventHandlers[event]) { | ||
const aliasedEventHandler = this._aliasEventHandler(event); | ||
this.manager.on(eventAlias, aliasedEventHandler); | ||
// TODO: multiple handlers for the same aliased event will override one another. | ||
// This should be an array of aliased handlers instead. | ||
this.aliasedEventHandlers[event] = aliasedEventHandler; | ||
} | ||
} | ||
|
||
// Register event handler. | ||
this.manager.on(event, handler); | ||
} else { | ||
// If `event` is a map, call `on()` for each entry. | ||
for (const eventName in event) { | ||
this.on(eventName, event[eventName]); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Deregister a previously-registered event handler. | ||
* @param {string|Object} event An event name (String) or map of event names to handlers | ||
* @param {Function} [handler] The function to be called on `event`. | ||
*/ | ||
off(event, handler) { | ||
if (typeof event === 'string') { | ||
// Clean up aliased gesture handler as necessary. | ||
const recognizerEvent = EVENT_RECOGNIZER_MAP[event]; | ||
if (recognizerEvent) { | ||
const eventAlias = GESTURE_EVENT_ALIASES[event]; | ||
if (eventAlias && this.aliasedEventHandlers[event]) { | ||
this.manager.off(eventAlias, this.aliasedEventHandlers[event]); | ||
delete this.aliasedEventHandlers[event]; | ||
} | ||
} | ||
|
||
// Deregister event handler. | ||
this.manager.off(event, handler); | ||
} else { | ||
// If `event` is a map, call `off()` for each entry. | ||
for (const eventName in event) { | ||
this.off(eventName, event[eventName]); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Handle basic events using the 'hammer.input' Hammer.js API: | ||
* Before running Recognizers, Hammer emits a 'hammer.input' event | ||
* with the basic event info. This function emits all basic events | ||
* aliased to the "class" of event received. | ||
* See constants.BASIC_EVENT_CLASSES basic event class definitions. | ||
*/ | ||
_onBasicInput(event) { | ||
const {srcEvent} = event; | ||
const eventAliases = BASIC_EVENT_ALIASES[srcEvent.type]; | ||
if (eventAliases) { | ||
// fire all events aliased to srcEvent.type | ||
eventAliases.forEach(alias => { | ||
const emitEvent = Object.assign({}, event, {type: alias}); | ||
this.manager.emit(alias, emitEvent); | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
* Handle events not supported by Hammer.js, | ||
* and pipe back out through same (Hammer) channel used by other events. | ||
*/ | ||
_onOtherEvent(event) { | ||
const {srcEvent: {type}} = event; | ||
this.manager.emit(type, event); | ||
} | ||
|
||
/** | ||
* Alias one event name to another, | ||
* to support events supported by Hammer.js under a different name. | ||
* See constants.GESTURE_EVENT_ALIASES. | ||
*/ | ||
_aliasEventHandler(eventAlias) { | ||
return event => this.manager.emit(eventAlias, event); | ||
} | ||
|
||
/** | ||
* If single and double tap are both enabled, | ||
* The single tap recognizer must wait for the double tap recognizer | ||
* to fail before resolving. Note that enabling both incurs a slight delay | ||
* on tap/click handler resolution. | ||
*/ | ||
_reconcileSingleAndDoubleTap(event) { | ||
if (event === 'tap' || event === 'doubletap') { | ||
const singletapRecognizer = this.manager.get('tap'); | ||
const doubletapRecognizer = this.manager.get('doubletap'); | ||
if (singletapRecognizer.options.enable && doubletapRecognizer.options.enable) { | ||
singletapRecognizer.requireFailure('doubletap'); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.