addHandler是向 map 注册各种交互事件的方法,每一个交互事件都会在通过 Class.addInitHook 向map的构造函数加addHandler钩子,map初始化的时候就会执行钩子函数,进而调用Handler.addHooks方法,实现在地图上绑定交互事件的监听函数。
export const Map = Evented.extend({
// ......
// 注册地图交互事件
addHandler(name, HandlerClass) {
if (!HandlerClass) {
return this;
}
// 初始化实例
const handler = (this[name] = new HandlerClass(this));
this._handlers.push(handler);
if (this.options[name]) {
// 如果存在,就启用,在dom元素上注册监听事件
handler.enable();
}
return this;
},
// 清理地图交互事件
_clearHandlers() {
for (let i = 0, len = this._handlers.length; i < len; i++) {
this._handlers[i].disable();
}
},
// ......
})
拖拽事件
拖拽交互可以让map具备拖拽平移的交互效果。
import {Map} from '../Map';
import {Handler} from '../../core/Handler';
import {Draggable} from '../../dom/Draggable';
import * as Util from '../../core/Util';
import * as DomUtil from '../../dom/DomUtil';
import {toLatLngBounds as latLngBounds} from '../../geo/LatLngBounds';
import {toBounds} from '../../geometry/Bounds';
Map.mergeOptions({
// 是否可以通过 鼠标或者触摸 平移
dragging: true,
// 如果启用,平移地图将有一个惯性效果
// 在触摸设备上变现不错。
inertia: true,
// 惯性衰减系数, in pixels/second².
inertiaDeceleration: 3400, // px/s^2
// 惯性最大速度
inertiaMaxSpeed: Infinity, // px/s
// 三次贝塞尔曲线的一个参数,数字越小,曲线越弯
easeLinearity: 0.2,
// TODO refactor, move to CRS
// @option worldCopyJump: Boolean = false
// With this option enabled, the map tracks when you pan to another "copy"
// of the world and seamlessly jumps to the original one so that all overlays
// like markers and vector layers are still visible.
worldCopyJump: false,
// @option maxBoundsViscosity: Number = 0.0
// If `maxBounds` is set, this option will control how solid the bounds
// are when dragging the map around. The default value of `0.0` allows the
// user to drag outside the bounds at normal speed, higher values will
// slow down map dragging outside bounds, and `1.0` makes the bounds fully
// solid, preventing the user from dragging outside the bounds.
maxBoundsViscosity: 0.0
});
export const Drag = Handler.extend({
addHooks() {
if (!this._draggable) {
const map = this._map;
this._draggable = new Draggable(map._mapPane, map._container);
this._draggable.on({
dragstart: this._onDragStart,
drag: this._onDrag,
dragend: this._onDragEnd
}, this);
this._draggable.on('predrag', this._onPreDragLimit, this);
if (map.options.worldCopyJump) {
this._draggable.on('predrag', this._onPreDragWrap, this);
map.on('zoomend', this._onZoomEnd, this);
map.whenReady(this._onZoomEnd, this);
}
}
DomUtil.addClass(this._map._container, 'leaflet-grab leaflet-touch-drag');
this._draggable.enable();
this._positions = [];
this._times = [];
},
removeHooks() {
DomUtil.removeClass(this._map._container, 'leaflet-grab');
DomUtil.removeClass(this._map._container, 'leaflet-touch-drag');
this._draggable.disable();
},
moved() {
return this._draggable && this._draggable._moved;
},
moving() {
return this._draggable && this._draggable._moving;
},
_onDragStart() {
const map = this._map;
map._stop();
if (this._map.options.maxBounds && this._map.options.maxBoundsViscosity) {
const bounds = latLngBounds(this._map.options.maxBounds);
this._offsetLimit = toBounds(
this._map.latLngToContainerPoint(bounds.getNorthWest()).multiplyBy(-1),
this._map.latLngToContainerPoint(bounds.getSouthEast()).multiplyBy(-1)
.add(this._map.getSize()));
this._viscosity = Math.min(1.0, Math.max(0.0, this._map.options.maxBoundsViscosity));
} else {
this._offsetLimit = null;
}
map
.fire('movestart')
.fire('dragstart');
if (map.options.inertia) {
this._positions = [];
this._times = [];
}
},
_onDrag(e) {
if (this._map.options.inertia) {
const time = this._lastTime = +new Date(),
pos = this._lastPos = this._draggable._absPos || this._draggable._newPos;
this._positions.push(pos);
this._times.push(time);
this._prunePositions(time);
}
this._map.fire('move', e).fire('drag', e);
},
_prunePositions(time) {
while (this._positions.length > 1 && time - this._times[0] > 50) {
this._positions.shift();
this._times.shift();
}
},
_onZoomEnd() {
const pxCenter = this._map.getSize().divideBy(2),
pxWorldCenter = this._map.latLngToLayerPoint([0, 0]);
this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x;
this._worldWidth = this._map.getPixelWorldBounds().getSize().x;
},
_viscousLimit(value, threshold) {
return value - (value - threshold) * this._viscosity;
},
_onPreDragLimit() {
if (!this._viscosity || !this._offsetLimit) { return; }
const offset = this._draggable._newPos.subtract(this._draggable._startPos);
const limit = this._offsetLimit;
if (offset.x < limit.min.x) { offset.x = this._viscousLimit(offset.x, limit.min.x); }
if (offset.y < limit.min.y) { offset.y = this._viscousLimit(offset.y, limit.min.y); }
if (offset.x > limit.max.x) { offset.x = this._viscousLimit(offset.x, limit.max.x); }
if (offset.y > limit.max.y) { offset.y = this._viscousLimit(offset.y, limit.max.y); }
this._draggable._newPos = this._draggable._startPos.add(offset);
},
_onPreDragWrap() {
// TODO refactor to be able to adjust map pane position after zoom
const worldWidth = this._worldWidth,
halfWidth = Math.round(worldWidth / 2),
dx = this._initialWorldOffset,
x = this._draggable._newPos.x,
newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx,
newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx,
newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;
this._draggable._absPos = this._draggable._newPos.clone();
this._draggable._newPos.x = newX;
},
_onDragEnd(e) {
const map = this._map,
options = map.options,
noInertia = !options.inertia || e.noInertia || this._times.length < 2;
map.fire('dragend', e);
if (noInertia) {
map.fire('moveend');
} else {
this._prunePositions(+new Date());
const direction = this._lastPos.subtract(this._positions[0]),
duration = (this._lastTime - this._times[0]) / 1000,
ease = options.easeLinearity,
speedVector = direction.multiplyBy(ease / duration),
speed = speedVector.distanceTo([0, 0]),
limitedSpeed = Math.min(options.inertiaMaxSpeed, speed),
limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed),
decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease);
let offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round();
if (!offset.x && !offset.y) {
map.fire('moveend');
} else {
offset = map._limitOffset(offset, map.options.maxBounds);
//
Util.requestAnimFrame(() => {
map.panBy(offset, {
duration: decelerationDuration,
easeLinearity: ease,
noMoveStart: true,
animate: true
});
});
}
}
}
});
// @section Handlers
// @property dragging: Handler
// Map dragging handler (by both mouse and touch).
Map.addInitHook('addHandler', 'dragging', Drag);
键盘交互事件
import {Map} from '../Map';
import {Handler} from '../../core/Handler';
import {on, off, stop} from '../../dom/DomEvent';
import {toPoint} from '../../geometry/Point';
/*
* L.Map.Keyboard is handling keyboard interaction with the map, enabled by default.
*/
// @namespace Map
// @section Keyboard Navigation Options
Map.mergeOptions({
// @option keyboard: Boolean = true
// Makes the map focusable and allows users to navigate the map with keyboard
// arrows and `+`/`-` keys.
keyboard: true,
// @option keyboardPanDelta: Number = 80
// Amount of pixels to pan when pressing an arrow key.
keyboardPanDelta: 80
});
export const Keyboard = Handler.extend({
keyCodes: {
left: [37],
right: [39],
down: [40],
up: [38],
zoomIn: [187, 107, 61, 171],
zoomOut: [189, 109, 54, 173]
},
initialize(map) {
this._map = map;
this._setPanDelta(map.options.keyboardPanDelta);
this._setZoomDelta(map.options.zoomDelta);
},
addHooks() {
const container = this._map._container;
// make the container focusable by tabbing
if (container.tabIndex <= 0) {
container.tabIndex = '0';
}
on(container, {
focus: this._onFocus,
blur: this._onBlur,
mousedown: this._onMouseDown
}, this);
this._map.on({
focus: this._addHooks,
blur: this._removeHooks
}, this);
},
removeHooks() {
this._removeHooks();
off(this._map._container, {
focus: this._onFocus,
blur: this._onBlur,
mousedown: this._onMouseDown
}, this);
this._map.off({
focus: this._addHooks,
blur: this._removeHooks
}, this);
},
_onMouseDown() {
if (this._focused) { return; }
const body = document.body,
docEl = document.documentElement,
top = body.scrollTop || docEl.scrollTop,
left = body.scrollLeft || docEl.scrollLeft;
this._map._container.focus();
window.scrollTo(left, top);
},
_onFocus() {
this._focused = true;
this._map.fire('focus');
},
_onBlur() {
this._focused = false;
this._map.fire('blur');
},
_setPanDelta(panDelta) {
const keys = this._panKeys = {},
codes = this.keyCodes;
let i, len;
for (i = 0, len = codes.left.length; i < len; i++) {
keys[codes.left[i]] = [-1 * panDelta, 0];
}
for (i = 0, len = codes.right.length; i < len; i++) {
keys[codes.right[i]] = [panDelta, 0];
}
for (i = 0, len = codes.down.length; i < len; i++) {
keys[codes.down[i]] = [0, panDelta];
}
for (i = 0, len = codes.up.length; i < len; i++) {
keys[codes.up[i]] = [0, -1 * panDelta];
}
},
_setZoomDelta(zoomDelta) {
const keys = this._zoomKeys = {},
codes = this.keyCodes;
let i, len;
for (i = 0, len = codes.zoomIn.length; i < len; i++) {
keys[codes.zoomIn[i]] = zoomDelta;
}
for (i = 0, len = codes.zoomOut.length; i < len; i++) {
keys[codes.zoomOut[i]] = -zoomDelta;
}
},
_addHooks() {
on(document, 'keydown', this._onKeyDown, this);
},
_removeHooks() {
off(document, 'keydown', this._onKeyDown, this);
},
_onKeyDown(e) {
if (e.altKey || e.ctrlKey || e.metaKey) { return; }
const key = e.keyCode,
map = this._map;
let offset;
if (key in this._panKeys) {
if (!map._panAnim || !map._panAnim._inProgress) {
offset = this._panKeys[key];
if (e.shiftKey) {
offset = toPoint(offset).multiplyBy(3);
}
if (map.options.maxBounds) {
offset = map._limitOffset(toPoint(offset), map.options.maxBounds);
}
if (map.options.worldCopyJump) {
const newLatLng = map.wrapLatLng(map.unproject(map.project(map.getCenter()).add(offset)));
map.panTo(newLatLng);
} else {
map.panBy(offset);
}
}
} else if (key in this._zoomKeys) {
map.setZoom(map.getZoom() + (e.shiftKey ? 3 : 1) * this._zoomKeys[key]);
} else if (key === 27 && map._popup && map._popup.options.closeOnEscapeKey) {
map.closePopup();
} else {
return;
}
stop(e);
}
});
// @section Handlers
// @section Handlers
// @property keyboard: Handler
// Keyboard navigation handler.
Map.addInitHook('addHandler', 'keyboard', Keyboard);
鼠标滚轮缩放
import {Map} from '../Map';
import {Handler} from '../../core/Handler';
import * as DomEvent from '../../dom/DomEvent';
/*
* L.Handler.ScrollWheelZoom is used by L.Map to enable mouse scroll wheel zoom on the map.
*/
// @namespace Map
// @section Interaction Options
Map.mergeOptions({
// @section Mouse wheel options
// @option scrollWheelZoom: Boolean|String = true
// Whether the map can be zoomed by using the mouse wheel. If passed `'center'`,
// it will zoom to the center of the view regardless of where the mouse was.
scrollWheelZoom: true,
// @option wheelDebounceTime: Number = 40
// Limits the rate at which a wheel can fire (in milliseconds). By default
// user can't zoom via wheel more often than once per 40 ms.
wheelDebounceTime: 40,
// @option wheelPxPerZoomLevel: Number = 60
// How many scroll pixels (as reported by [L.DomEvent.getWheelDelta](#domevent-getwheeldelta))
// mean a change of one full zoom level. Smaller values will make wheel-zooming
// faster (and vice versa).
wheelPxPerZoomLevel: 60
});
export const ScrollWheelZoom = Handler.extend({
addHooks() {
DomEvent.on(this._map._container, 'wheel', this._onWheelScroll, this);
this._delta = 0;
},
removeHooks() {
DomEvent.off(this._map._container, 'wheel', this._onWheelScroll, this);
},
_onWheelScroll(e) {
const delta = DomEvent.getWheelDelta(e);
const debounce = this._map.options.wheelDebounceTime;
this._delta += delta;
this._lastMousePos = this._map.mouseEventToContainerPoint(e);
if (!this._startTime) {
this._startTime = +new Date();
}
const left = Math.max(debounce - (+new Date() - this._startTime), 0);
clearTimeout(this._timer);
this._timer = setTimeout(this._performZoom.bind(this), left);
DomEvent.stop(e);
},
_performZoom() {
const map = this._map,
zoom = map.getZoom(),
snap = this._map.options.zoomSnap || 0;
map._stop(); // stop panning and fly animations if any
// map the delta with a sigmoid function to -4..4 range leaning on -1..1
const d2 = this._delta / (this._map.options.wheelPxPerZoomLevel * 4),
d3 = 4 * Math.log(2 / (1 + Math.exp(-Math.abs(d2)))) / Math.LN2,
d4 = snap ? Math.ceil(d3 / snap) * snap : d3,
delta = map._limitZoom(zoom + (this._delta > 0 ? d4 : -d4)) - zoom;
this._delta = 0;
this._startTime = null;
if (!delta) { return; }
if (map.options.scrollWheelZoom === 'center') {
map.setZoom(zoom + delta);
} else {
map.setZoomAround(this._lastMousePos, zoom + delta);
}
}
});
// @section Handlers
// @property scrollWheelZoom: Handler
// Scroll wheel zoom handler.
Map.addInitHook('addHandler', 'scrollWheelZoom', ScrollWheelZoom);
双击缩放事件
import {Map} from '../Map';
import {Handler} from '../../core/Handler';
/*
* L.Handler.DoubleClickZoom is used to handle double-click zoom on the map, enabled by default.
*/
// @namespace Map
// @section Interaction Options
Map.mergeOptions({
// @option doubleClickZoom: Boolean|String = true
// Whether the map can be zoomed in by double clicking on it and
// zoomed out by double clicking while holding shift. If passed
// `'center'`, double-click zoom will zoom to the center of the
// view regardless of where the mouse was.
doubleClickZoom: true
});
export const DoubleClickZoom = Handler.extend({
addHooks() {
this._map.on('dblclick', this._onDoubleClick, this);
},
removeHooks() {
this._map.off('dblclick', this._onDoubleClick, this);
},
_onDoubleClick(e) {
const map = this._map,
oldZoom = map.getZoom(),
delta = map.options.zoomDelta,
zoom = e.originalEvent.shiftKey ? oldZoom - delta : oldZoom + delta;
if (map.options.doubleClickZoom === 'center') {
map.setZoom(zoom);
} else {
map.setZoomAround(e.containerPoint, zoom);
}
}
});
// @section Handlers
//
// Map properties include interaction handlers that allow you to control
// interaction behavior in runtime, enabling or disabling certain features such
// as dragging or touch zoom (see `Handler` methods). For example:
//
// ```js
// map.doubleClickZoom.disable();
// ```
//
// @property doubleClickZoom: Handler
// Double click zoom handler.
Map.addInitHook('addHandler', 'doubleClickZoom', DoubleClickZoom);
shift按键+拖拽缩放
import {Map} from '../Map';
import {Handler} from '../../core/Handler';
import * as DomUtil from '../../dom/DomUtil';
import * as DomEvent from '../../dom/DomEvent';
import {LatLngBounds} from '../../geo/LatLngBounds';
import {Bounds} from '../../geometry/Bounds';
/*
* L.Handler.BoxZoom is used to add shift-drag zoom interaction to the map
* (zoom to a selected bounding box), enabled by default.
*/
// @namespace Map
// @section Interaction Options
Map.mergeOptions({
// @option boxZoom: Boolean = true
// Whether the map can be zoomed to a rectangular area specified by
// dragging the mouse while pressing the shift key.
boxZoom: true
});
export const BoxZoom = Handler.extend({
initialize(map) {
this._map = map;
this._container = map._container;
this._pane = map._panes.overlayPane;
this._resetStateTimeout = 0;
map.on('unload', this._destroy, this);
},
addHooks() {
DomEvent.on(this._container, 'mousedown', this._onMouseDown, this);
},
removeHooks() {
DomEvent.off(this._container, 'mousedown', this._onMouseDown, this);
},
moved() {
return this._moved;
},
_destroy() {
DomUtil.remove(this._pane);
delete this._pane;
},
_resetState() {
this._resetStateTimeout = 0;
this._moved = false;
},
_clearDeferredResetState() {
if (this._resetStateTimeout !== 0) {
clearTimeout(this._resetStateTimeout);
this._resetStateTimeout = 0;
}
},
_onMouseDown(e) {
if (!e.shiftKey || ((e.which !== 1) && (e.button !== 1))) { return false; }
// Clear the deferred resetState if it hasn't executed yet, otherwise it
// will interrupt the interaction and orphan a box element in the container.
this._clearDeferredResetState();
this._resetState();
DomUtil.disableTextSelection();
DomUtil.disableImageDrag();
this._startPoint = this._map.mouseEventToContainerPoint(e);
DomEvent.on(document, {
contextmenu: DomEvent.stop,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
keydown: this._onKeyDown
}, this);
},
_onMouseMove(e) {
if (!this._moved) {
this._moved = true;
this._box = DomUtil.create('div', 'leaflet-zoom-box', this._container);
DomUtil.addClass(this._container, 'leaflet-crosshair');
this._map.fire('boxzoomstart');
}
this._point = this._map.mouseEventToContainerPoint(e);
const bounds = new Bounds(this._point, this._startPoint),
size = bounds.getSize();
DomUtil.setPosition(this._box, bounds.min);
this._box.style.width = `${size.x}px`;
this._box.style.height = `${size.y}px`;
},
_finish() {
if (this._moved) {
DomUtil.remove(this._box);
DomUtil.removeClass(this._container, 'leaflet-crosshair');
}
DomUtil.enableTextSelection();
DomUtil.enableImageDrag();
DomEvent.off(document, {
contextmenu: DomEvent.stop,
mousemove: this._onMouseMove,
mouseup: this._onMouseUp,
keydown: this._onKeyDown
}, this);
},
_onMouseUp(e) {
if ((e.which !== 1) && (e.button !== 1)) { return; }
this._finish();
if (!this._moved) { return; }
// Postpone to next JS tick so internal click event handling
// still see it as "moved".
this._clearDeferredResetState();
this._resetStateTimeout = setTimeout(this._resetState.bind(this), 0);
const bounds = new LatLngBounds(
this._map.containerPointToLatLng(this._startPoint),
this._map.containerPointToLatLng(this._point));
this._map
.fitBounds(bounds)
.fire('boxzoomend', {boxZoomBounds: bounds});
},
_onKeyDown(e) {
if (e.keyCode === 27) {
this._finish();
this._clearDeferredResetState();
this._resetState();
}
}
});
// @section Handlers
// @property boxZoom: Handler
// Box (shift-drag with mouse) zoom handler.
Map.addInitHook('addHandler', 'boxZoom', BoxZoom);
移动端双指缩放
import {Map} from '../Map';
import {Handler} from '../../core/Handler';
import * as DomEvent from '../../dom/DomEvent';
import * as Util from '../../core/Util';
import * as DomUtil from '../../dom/DomUtil';
import Browser from '../../core/Browser';
/*
* L.Handler.TouchZoom is used by L.Map to add pinch zoom on supported mobile browsers.
*/
// @namespace Map
// @section Interaction Options
Map.mergeOptions({
// @section Touch interaction options
// @option touchZoom: Boolean|String = *
// Whether the map can be zoomed by touch-dragging with two fingers. If
// passed `'center'`, it will zoom to the center of the view regardless of
// where the touch events (fingers) were. Enabled for touch-capable web
// browsers.
touchZoom: Browser.touch,
// @option bounceAtZoomLimits: Boolean = true
// Set it to false if you don't want the map to zoom beyond min/max zoom
// and then bounce back when pinch-zooming.
bounceAtZoomLimits: true
});
export const TouchZoom = Handler.extend({
addHooks() {
DomUtil.addClass(this._map._container, 'leaflet-touch-zoom');
DomEvent.on(this._map._container, 'touchstart', this._onTouchStart, this);
},
removeHooks() {
DomUtil.removeClass(this._map._container, 'leaflet-touch-zoom');
DomEvent.off(this._map._container, 'touchstart', this._onTouchStart, this);
},
_onTouchStart(e) {
const map = this._map;
if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming) { return; }
const p1 = map.mouseEventToContainerPoint(e.touches[0]),
p2 = map.mouseEventToContainerPoint(e.touches[1]);
this._centerPoint = map.getSize()._divideBy(2);
this._startLatLng = map.containerPointToLatLng(this._centerPoint);
if (map.options.touchZoom !== 'center') {
this._pinchStartLatLng = map.containerPointToLatLng(p1.add(p2)._divideBy(2));
}
this._startDist = p1.distanceTo(p2);
this._startZoom = map.getZoom();
this._moved = false;
this._zooming = true;
map._stop();
DomEvent.on(document, 'touchmove', this._onTouchMove, this);
DomEvent.on(document, 'touchend touchcancel', this._onTouchEnd, this);
DomEvent.preventDefault(e);
},
_onTouchMove(e) {
if (!e.touches || e.touches.length !== 2 || !this._zooming) { return; }
const map = this._map,
p1 = map.mouseEventToContainerPoint(e.touches[0]),
p2 = map.mouseEventToContainerPoint(e.touches[1]),
scale = p1.distanceTo(p2) / this._startDist;
this._zoom = map.getScaleZoom(scale, this._startZoom);
if (!map.options.bounceAtZoomLimits && (
(this._zoom < map.getMinZoom() && scale < 1) ||
(this._zoom > map.getMaxZoom() && scale > 1))) {
this._zoom = map._limitZoom(this._zoom);
}
if (map.options.touchZoom === 'center') {
this._center = this._startLatLng;
if (scale === 1) { return; }
} else {
// Get delta from pinch to center, so centerLatLng is delta applied to initial pinchLatLng
const delta = p1._add(p2)._divideBy(2)._subtract(this._centerPoint);
if (scale === 1 && delta.x === 0 && delta.y === 0) { return; }
this._center = map.unproject(map.project(this._pinchStartLatLng, this._zoom).subtract(delta), this._zoom);
}
if (!this._moved) {
map._moveStart(true, false);
this._moved = true;
}
Util.cancelAnimFrame(this._animRequest);
const moveFn = map._move.bind(map, this._center, this._zoom, {pinch: true, round: false}, undefined);
this._animRequest = Util.requestAnimFrame(moveFn, this, true);
DomEvent.preventDefault(e);
},
_onTouchEnd() {
if (!this._moved || !this._zooming) {
this._zooming = false;
return;
}
this._zooming = false;
Util.cancelAnimFrame(this._animRequest);
DomEvent.off(document, 'touchmove', this._onTouchMove, this);
DomEvent.off(document, 'touchend touchcancel', this._onTouchEnd, this);
// Pinch updates GridLayers' levels only when zoomSnap is off, so zoomSnap becomes noUpdate.
if (this._map.options.zoomAnimation) {
this._map._animateZoom(this._center, this._map._limitZoom(this._zoom), true, this._map.options.zoomSnap);
} else {
this._map._resetView(this._center, this._map._limitZoom(this._zoom));
}
}
});
// @section Handlers
// @property touchZoom: Handler
// Touch zoom handler.
Map.addInitHook('addHandler', 'touchZoom', TouchZoom);
长按模拟右键交互
用于通过“长按”模拟鼠标右键,目前在手机端的 Safar 上不会触发。
import {Map} from '../Map';
import {Handler} from '../../core/Handler';
import * as DomEvent from '../../dom/DomEvent';
import {Point} from '../../geometry/Point';
import Browser from '../../core/Browser';
const tapHoldDelay = 600;
Map.mergeOptions({
// Enables simulation of `contextmenu` event, default is `true` for mobile Safari.
tapHold: Browser.touchNative && Browser.safari && Browser.mobile,
// 用户在触控过程中可以移动手指的最大像素数,才能被认为是一次有效的点击。
tapTolerance: 15
});
export const TapHold = Handler.extend({
addHooks() {
// 注册按下事件
DomEvent.on(this._map._container, 'touchstart', this._onDown, this);
},
removeHooks() {
// 解除按下事件
DomEvent.off(this._map._container, 'touchstart', this._onDown, this);
},
// 按下事件,记录事件
_onDown(e) {
clearTimeout(this._holdTimeout);
if (e.touches.length !== 1) { return; }
const first = e.touches[0];
this._startPos = this._newPos = new Point(first.clientX, first.clientY);
// 0.6 秒之后触注册松开事件(如果0.6秒之内已经松开了,就不会执行注册的松开事件)
this._holdTimeout = setTimeout((() => {
// 清理事件
this._cancel();
// 判断是不是点击
if (!this._isTapValid()) { return; }
// 松开的时候,阻止默认的事件 https://w3c.github.io/touch-events/#mouse-events
DomEvent.on(document, 'touchend', DomEvent.preventDefault);
// 松开或取消的时候,解绑注册注册在松开和取消上的事件,
DomEvent.on(document, 'touchend touchcancel', this._cancelClickPrevent);
// 触发模拟的鼠标右键操作
this._simulateEvent('contextmenu', first);
}), tapHoldDelay);
// 清理事件
DomEvent.on(document, 'touchend touchcancel contextmenu', this._cancel, this);
DomEvent.on(document, 'touchmove', this._onMove, this);
},
// 取消阻止
_cancelClickPrevent: function _cancelClickPrevent() {
DomEvent.off(document, 'touchend', DomEvent.preventDefault);
DomEvent.off(document, 'touchend touchcancel', _cancelClickPrevent);
},
// 取消注册的左右事件
_cancel() {
clearTimeout(this._holdTimeout);
DomEvent.off(document, 'touchend touchcancel contextmenu', this._cancel, this);
DomEvent.off(document, 'touchmove', this._onMove, this);
},
_onMove(e) {
const first = e.touches[0];
this._newPos = new Point(first.clientX, first.clientY);
},
// 判断是不是点击,超过限差就是平移了
_isTapValid() {
return this._newPos.distanceTo(this._startPos) <= this._map.options.tapTolerance;
},
// 触发模拟的鼠标事件
_simulateEvent(type, e) {
const simulatedEvent = new MouseEvent(type, {
bubbles: true,
cancelable: true,
view: window,
// detail: 1,
screenX: e.screenX,
screenY: e.screenY,
clientX: e.clientX,
clientY: e.clientY,
// button: 2,
// buttons: 2
});
simulatedEvent._simulated = true;
// dispatchEvent
e.target.dispatchEvent(simulatedEvent);
}
});
// @section Handlers
// @property tapHold: Handler
// Long tap handler to simulate `contextmenu` event (useful in mobile Safari).
Map.addInitHook('addHandler', 'tapHold', TapHold);