Add/remove event handlers based on user setting (#308)

- Disable recognizers in `EventManager.off` if no more handlers are attached
- Let `MapControls` manage its own events by passing in the `EventManager` instance
- Use a new MapControls instance per map
This commit is contained in:
Xiaoji Chen 2017-07-10 12:52:28 -07:00 committed by GitHub
parent 4c0504a841
commit e73cceea5f
6 changed files with 111 additions and 66 deletions

View File

@ -112,9 +112,7 @@ const defaultProps = Object.assign({}, StaticMap.defaultProps, MAPBOX_LIMITS, {
clickRadius: 0,
getCursor: getDefaultCursor,
visibilityConstraints: MAPBOX_LIMITS,
mapControls: new MapControls()
visibilityConstraints: MAPBOX_LIMITS
});
const childContextTypes = {
@ -140,6 +138,10 @@ export default class InteractiveMap extends PureComponent {
// Whether the cursor is over a clickable feature
isHovering: false
};
// If props.mapControls is not provided, fallback to default MapControls instance
// Cannot use defaultProps here because it needs to be per map instance
this._mapControls = props.mapControls || new MapControls();
}
getChildContext() {
@ -151,20 +153,22 @@ export default class InteractiveMap extends PureComponent {
componentDidMount() {
const {eventCanvas} = this.refs;
const {mapControls} = this.props;
// Register event handlers defined by map controls
const events = {};
mapControls.events.forEach(eventName => {
events[eventName] = this._handleEvent;
});
const eventManager = new EventManager(eventCanvas, {events});
const eventManager = new EventManager(eventCanvas);
// Register additional event handlers for click and hover
eventManager.on('mousemove', this._onMouseMove);
eventManager.on('click', this._onMouseClick);
this._eventManager = eventManager;
this._mapControls.setOptions(Object.assign({}, this.props, {
onStateChange: this._onInteractiveStateChange,
eventManager
}));
}
componentWillUpdate(nextProps) {
this._mapControls.setOptions(nextProps);
}
componentWillUnmount() {
@ -174,13 +178,6 @@ export default class InteractiveMap extends PureComponent {
}
}
_handleEvent(event) {
const controlOptions = Object.assign({}, this.props, {
onStateChange: this._onInteractiveStateChange
});
return this.props.mapControls.handleEvent(event, controlOptions);
}
getMap() {
return this._map.getMap();
}

View File

@ -33,7 +33,6 @@ export const BASIC_EVENT_ALIASES = {
* 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',

View File

@ -107,27 +107,32 @@ export default class EventManager {
}
}
/*
* Enable/disable recognizer for the given event
*/
_toggleRecognizer(name, enabled) {
const recognizer = this.manager.get(name);
if (recognizer) {
recognizer.set({enable: enabled});
}
this.wheelInput.toggleIfEventSupported(name, enabled);
this.moveInput.toggleIfEventSupported(name, enabled);
}
/**
* Process the event registration for a single event + handler.
*/
_addEventHandler(event, handler) {
// Special handling for gestural events.
const recognizerEvent = EVENT_RECOGNIZER_MAP[event];
if (recognizerEvent) {
// Enable recognizer for this event.
const recognizer = this.manager.get(recognizerEvent);
recognizer.set({enable: true});
}
this.wheelInput.enableIfEventSupported(event);
this.moveInput.enableIfEventSupported(event);
const wrappedHandler = this._wrapEventHandler(event, handler);
// Alias to a recognized gesture as necessary.
const eventAlias = GESTURE_EVENT_ALIASES[event] || event;
// Get recognizer for this event
const recognizerName = EVENT_RECOGNIZER_MAP[eventAlias] || eventAlias;
// Enable recognizer for this event.
this._toggleRecognizer(recognizerName, true);
// Save wrapped handler
this.eventHandlers.push({event, eventAlias, handler, wrappedHandler});
this.eventHandlers.push({event, eventAlias, recognizerName, handler, wrappedHandler});
this.manager.on(eventAlias, wrappedHandler);
}
@ -136,6 +141,8 @@ export default class EventManager {
* Process the event deregistration for a single event + handler.
*/
_removeEventHandler(event, handler) {
let success = false;
// Find saved handler if any.
for (let i = this.eventHandlers.length; i--;) {
const entry = this.eventHandlers[i];
@ -144,6 +151,21 @@ export default class EventManager {
this.manager.off(entry.eventAlias, entry.wrappedHandler);
// Delete saved handler
this.eventHandlers.splice(i, 1);
success = true;
}
}
if (success) {
// Alias to a recognized gesture as necessary.
const eventAlias = GESTURE_EVENT_ALIASES[event] || event;
// Get recognizer for this event
const recognizerName = EVENT_RECOGNIZER_MAP[eventAlias] || eventAlias;
// Disable recognizer if no more handlers are attached to its events
const isRecognizerUsed = this.eventHandlers.find(
entry => entry.recognizerName === recognizerName
);
if (!isRecognizerUsed) {
this._toggleRecognizer(recognizerName, false);
}
}
}

View File

@ -1,5 +1,5 @@
const MOUSE_EVENTS = ['mousedown', 'mousemove', 'mouseup'];
const MOVE_EVENT_TYPES = ['mousemove', 'pointermove'];
const EVENT_TYPE = 'pointermove';
/**
* Hammer.js swallows 'move' events (for pointer/touch/mouse)
@ -35,9 +35,9 @@ export default class MoveInput {
* Enable this input (begin processing events)
* if the specified event type is among those handled by this input.
*/
enableIfEventSupported(eventType) {
if (MOVE_EVENT_TYPES.indexOf(eventType) >= 0) {
this.options.enable = true;
toggleIfEventSupported(eventType, enabled) {
if (EVENT_TYPE === eventType) {
this.options.enable = enabled;
}
}
@ -62,12 +62,12 @@ export default class MoveInput {
if (!this.pressed) {
// Drag events are emitted by hammer already
// we just need to emit the move event on hover
MOVE_EVENT_TYPES.forEach(type => this.callback({
type,
this.callback({
type: EVENT_TYPE,
srcEvent: event,
pointerType: 'mouse',
target: event.target
}));
});
}
break;
case 'mouseup':

View File

@ -53,9 +53,9 @@ export default class WheelInput {
* Enable this input (begin processing events)
* if the specified event type is among those handled by this input.
*/
enableIfEventSupported(eventType) {
toggleIfEventSupported(eventType, enabled) {
if (eventType === EVENT_TYPE) {
this.options.enable = true;
this.options.enable = enabled;
}
}
@ -64,6 +64,7 @@ export default class WheelInput {
if (!this.options.enable) {
return;
}
event.preventDefault();
let value = event.deltaY;
if (window.WheelEvent) {

View File

@ -25,16 +25,12 @@ const PITCH_MOUSE_THRESHOLD = 5;
const PITCH_ACCEL = 1.2;
const ZOOM_ACCEL = 0.01;
const SUBSCRIBED_EVENTS = [
'panstart',
'panmove',
'panend',
'pinchstart',
'pinch',
'pinchend',
'doubletap',
'wheel'
];
const EVENT_TYPES = {
WHEEL: ['wheel'],
PAN: ['panstart', 'panmove', 'panend'],
PINCH: ['pinchstart', 'pinchmove', 'pinchend'],
DOUBLE_TAP: ['doubletap']
};
export default class MapControls {
/**
@ -42,19 +38,18 @@ export default class MapControls {
* A class that handles events and updates mercator style viewport parameters
*/
constructor() {
this.events = SUBSCRIBED_EVENTS;
this._state = {
isDragging: false
};
this.handleEvent = this.handleEvent.bind(this);
}
/**
* Callback for events
* @param {hammer.Event} event
*/
handleEvent(event, options) {
this.mapState = new MapState(Object.assign({}, options, this._state));
this.setOptions(options);
handleEvent(event) {
this.mapState = new MapState(Object.assign({}, this.mapStateProps, this._state));
switch (event.type) {
case 'panstart':
@ -116,20 +111,37 @@ export default class MapControls {
/**
* Extract interactivity options
*/
setOptions({
// TODO(deprecate): remove this when `onChangeViewport` gets deprecated
onChangeViewport,
onViewportChange,
onStateChange,
scrollZoom = true,
dragPan = true,
dragRotate = true,
doubleClickZoom = true,
touchZoomRotate = true
}) {
setOptions(options) {
const {
// TODO(deprecate): remove this when `onChangeViewport` gets deprecated
onChangeViewport,
onViewportChange,
onStateChange = this.onStateChange,
eventManager = this.eventManager,
scrollZoom = true,
dragPan = true,
dragRotate = true,
doubleClickZoom = true,
touchZoomRotate = true
} = options;
// TODO(deprecate): remove this check when `onChangeViewport` gets deprecated
this.onViewportChange = onViewportChange || onChangeViewport;
this.onStateChange = onStateChange;
this.mapStateProps = options;
if (this.eventManager !== eventManager) {
// EventManager has changed
this.eventManager = eventManager;
this._events = {};
}
// Register/unregister events
this.toggleEvents(EVENT_TYPES.WHEEL, scrollZoom);
this.toggleEvents(EVENT_TYPES.PAN, dragPan || dragRotate);
this.toggleEvents(EVENT_TYPES.PINCH, touchZoomRotate);
this.toggleEvents(EVENT_TYPES.DOUBLE_TAP, doubleClickZoom);
// Interaction toggles
this.scrollZoom = scrollZoom;
this.dragPan = dragPan;
this.dragRotate = dragRotate;
@ -137,6 +149,21 @@ export default class MapControls {
this.touchZoomRotate = touchZoomRotate;
}
toggleEvents(eventNames, enabled) {
if (this.eventManager) {
eventNames.forEach(eventName => {
if (this._events[eventName] !== enabled) {
this._events[eventName] = enabled;
if (enabled) {
this.eventManager.on(eventName, this.handleEvent);
} else {
this.eventManager.off(eventName, this.handleEvent);
}
}
});
}
}
/* Event handlers */
// Default handler for the `panstart` event.
_onPanStart(event) {
@ -204,7 +231,6 @@ export default class MapControls {
if (!this.scrollZoom) {
return false;
}
event.srcEvent.preventDefault();
const pos = this.getCenter(event);
const {delta} = event;