maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
242 lines (206 loc) • 7.87 kB
text/typescript
import {MapMouseEvent, MapTouchEvent, MapWheelEvent} from '../events.ts';
import type {Handler, HandlerResult} from '../handler_manager.ts';
import type {Map} from '../map.ts';
import type Point from '@mapbox/point-geometry';
export class MapEventHandler implements Handler {
_mousedownPos: Point;
_clickTolerance: number;
_map: Map;
constructor(map: Map, options: {
clickTolerance: number;
}) {
this._map = map;
this._clickTolerance = options.clickTolerance;
}
reset(): void {
delete this._mousedownPos;
}
wheel(e: WheelEvent): HandlerResult | void {
// If mapEvent.preventDefault() is called by the user, prevent handlers such as:
// - ScrollZoom
return this._firePreventable(new MapWheelEvent(this._map, e));
}
mousedown(e: MouseEvent, point: Point): HandlerResult | void {
this._mousedownPos = point;
// If mapEvent.preventDefault() is called by the user, prevent handlers such as:
// - MousePan
// - MouseRotate
// - MousePitch
// - DblclickHandler
return this._firePreventable(new MapMouseEvent(e.type, this._map, e));
}
mouseup(e: MouseEvent): void {
this._map.fire(new MapMouseEvent(e.type, this._map, e));
}
click(e: MouseEvent, point: Point): void {
if (this._mousedownPos && this._mousedownPos.dist(point) >= this._clickTolerance) return;
this._map.fire(new MapMouseEvent(e.type, this._map, e));
}
dblclick(e: MouseEvent): HandlerResult | void {
// If mapEvent.preventDefault() is called by the user, prevent handlers such as:
// - DblClickZoom
return this._firePreventable(new MapMouseEvent(e.type, this._map, e));
}
mouseover(e: MouseEvent): void {
this._map.fire(new MapMouseEvent(e.type, this._map, e));
}
mouseout(e: MouseEvent): void {
this._map.fire(new MapMouseEvent(e.type, this._map, e));
}
touchstart(e: TouchEvent): HandlerResult | void {
// If mapEvent.preventDefault() is called by the user, prevent handlers such as:
// - TouchPan
// - TouchZoom
// - TouchRotate
// - TouchPitch
// - TapZoom
// - SwipeZoom
return this._firePreventable(new MapTouchEvent(e.type, this._map, e));
}
touchmove(e: TouchEvent): void {
this._map.fire(new MapTouchEvent(e.type, this._map, e));
}
touchend(e: TouchEvent): void {
this._map.fire(new MapTouchEvent(e.type, this._map, e));
}
touchcancel(e: TouchEvent): void {
this._map.fire(new MapTouchEvent(e.type, this._map, e));
}
_firePreventable(mapEvent: MapMouseEvent | MapTouchEvent | MapWheelEvent): HandlerResult | void {
this._map.fire(mapEvent);
if (mapEvent.defaultPrevented) {
// returning an object marks the handler as active and resets other handlers
return {};
}
}
isEnabled(): boolean {
return true;
}
isActive(): boolean {
return false;
}
enable(): void {}
disable(): void {}
}
/**
* A single finger held in place this long fires a long-press contextmenu.
*/
const LONG_PRESS_DELAY = 500;
export class BlockableMapEventHandler {
_map: Map;
_clickTolerance: number;
_delayContextMenu: boolean;
_ignoreContextMenu: boolean;
_contextMenuEvent: MouseEvent;
_touchActive: boolean;
_longPressTimer: ReturnType<typeof setTimeout>;
_longPressStart: Point;
constructor(map: Map, options: {
clickTolerance: number;
}) {
this._map = map;
this._clickTolerance = options.clickTolerance;
}
reset(): void {
this._delayContextMenu = false;
this._ignoreContextMenu = true;
delete this._contextMenuEvent;
this._clearLongPress();
this._touchActive = false;
}
mousemove(e: MouseEvent): void {
// mousemove map events should not be fired when interaction handlers (pan, rotate, etc) are active
this._map.fire(new MapMouseEvent(e.type, this._map, e));
}
mousedown(): void {
this._delayContextMenu = true;
this._ignoreContextMenu = false;
}
mouseup(): void {
this._delayContextMenu = false;
if (this._contextMenuEvent) {
this._map.fire(new MapMouseEvent('contextmenu', this._map, this._contextMenuEvent));
delete this._contextMenuEvent;
}
}
/**
* Starts the touch long press. Touch devices have no right click, so a long press opens
* the context menu. It is detected with a timer rather than by relying on a native
* contextmenu event, because Android Chrome fires one on a long press but iOS
* Safari does not. A single finger held past the delay without moving fires a
* contextmenu at that point; a pan or a lift cancels it.
* @param e - the touch event
* @param points - the touch points, in map coordinates
* @param mapTouches - the touches that are on the map
*/
touchstart(e: TouchEvent, points: Point[], mapTouches: Touch[]): void {
this._clearLongPress();
this._touchActive = mapTouches.length > 0;
if (mapTouches.length !== 1) return; // single finger only, never during a pinch
this._longPressStart = points[0];
const touch = mapTouches[0];
this._longPressTimer = setTimeout(() => {
this._longPressTimer = undefined;
const originalEvent = new MouseEvent('contextmenu', {clientX: touch.clientX, clientY: touch.clientY, button: 2, bubbles: true, cancelable: true});
this._map.fire(new MapMouseEvent('contextmenu', this._map, originalEvent));
}, LONG_PRESS_DELAY);
}
/**
* Cancels a pending long press once the finger has moved further than the map's
* click tolerance, or once a second finger lands.
* @param e - the touch event
* @param points - the touch points, in map coordinates
* @param mapTouches - the touches that are on the map
*/
touchmove(e: TouchEvent, points: Point[], mapTouches: Touch[]): void {
if (!this._longPressTimer) return;
if (mapTouches.length !== 1 || !this._longPressStart || points[0].dist(this._longPressStart) > this._clickTolerance) {
this._clearLongPress();
}
}
touchend(): void {
this._touchActive = false;
this._clearLongPress();
}
touchcancel(): void {
this._touchActive = false;
this._clearLongPress();
}
_clearLongPress(): void {
if (this._longPressTimer) {
clearTimeout(this._longPressTimer);
this._longPressTimer = undefined;
}
this._longPressStart = undefined;
}
/**
* Handles a contextmenu event. A native one during a touch is suppressed, because the
* long press timer already fires the event and the browser menu would double up.
* @param e - the mouse event
*/
contextmenu(e: MouseEvent): void {
if (this._touchActive) {
e.preventDefault();
return;
}
if (this._delayContextMenu) {
// Mac: contextmenu fired on mousedown; we save it until mouseup for consistency's sake
this._contextMenuEvent = e;
} else if (!this._ignoreContextMenu) {
// Windows: contextmenu fired on mouseup, so fire event now
this._map.fire(new MapMouseEvent(e.type, this._map, e));
}
// prevent browser context menu when necessary
if (this._map.listens('contextmenu')) {
e.preventDefault();
}
}
isEnabled(): boolean {
return true;
}
isActive(): boolean {
return false;
}
enable(): void {}
disable(): void {}
}