diff --git a/src/components/interactive-map.js b/src/components/interactive-map.js index bd1168fd..0139173a 100644 --- a/src/components/interactive-map.js +++ b/src/components/interactive-map.js @@ -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(); } diff --git a/src/utils/event-manager/constants.js b/src/utils/event-manager/constants.js index 15f17267..7269ada4 100644 --- a/src/utils/event-manager/constants.js +++ b/src/utils/event-manager/constants.js @@ -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', diff --git a/src/utils/event-manager/event-manager.js b/src/utils/event-manager/event-manager.js index 6675b165..002baf4e 100644 --- a/src/utils/event-manager/event-manager.js +++ b/src/utils/event-manager/event-manager.js @@ -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); } } } diff --git a/src/utils/event-manager/move-input.js b/src/utils/event-manager/move-input.js index 650592f7..54e46dd5 100644 --- a/src/utils/event-manager/move-input.js +++ b/src/utils/event-manager/move-input.js @@ -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': diff --git a/src/utils/event-manager/wheel-input.js b/src/utils/event-manager/wheel-input.js index 29051a1c..46311357 100644 --- a/src/utils/event-manager/wheel-input.js +++ b/src/utils/event-manager/wheel-input.js @@ -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) { diff --git a/src/utils/map-controls.js b/src/utils/map-controls.js index 56034885..4bbcacd5 100644 --- a/src/utils/map-controls.js +++ b/src/utils/map-controls.js @@ -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;