注册了键盘、鼠标和手势交互事件在地图上的响应方法,这些事件只是原始的dom事件,与拖拽、拉框这种组合 dom 事件有差别。
_mouseEvents 包括 ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu']
export const Map = Evented.extend({
// ......
_mouseEvents: ['click', 'dblclick', 'mouseover', 'mouseout', 'contextmenu'],
/** 绑定 dom 交互事件 */
_initEvents(remove) {
this._targets = {};
this._targets[Util.stamp(this._container)] = this;
const onOff = remove ? DomEvent.off : DomEvent.on;
// @event click: MouseEvent
// 当用户点击 (or taps) 地图时触发
// @event dblclick: MouseEvent
// 当用户双击 (or double-taps) 地图时触发
// @event mousedown: MouseEvent
// 当用户按下鼠标按钮的时候触发
// @event mouseup: MouseEvent
// 当用户松开鼠标按钮的时候触发
// @event mouseover: MouseEvent
// 当鼠标悬浮进入地图时候触发
// @event mouseout: MouseEvent
// 当鼠标悬浮移出地图时候触发
// @event mousemove: MouseEvent
// 当暑校在地图上悬浮移动时候触发
// @event contextmenu: MouseEvent
// 当用户在地图上点击鼠标右键时候触发,注册事件后会组织浏览器的默认右键菜单。移动端长按也能触发
// @event keypress: KeyboardEvent
// 当地图是 focused 状态时,响应用户的键盘事件,按下键盘上产生字符值的键时触发(如Alt,Shift,Ctrl,或Meta 不会触发)
// @event keydown: KeyboardEvent
// 当地图是 focused 状态时,用户按下键盘按键时候触发. 和`keypress`不同,keydown 在按下键盘上产生字符值和不产生字符值的键时都会触发
// @event keyup: KeyboardEvent
// 当地图是 focused 状态时,用户松开键盘按键时候触发
onOff(this._container, 'click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup', this._handleDOMEvent, this);
// 监听浏览器窗口大小变化,地图也跟着调整
if (this.options.trackResize) {
if (!remove) {
if (!this._resizeObserver) {
this._resizeObserver = new ResizeObserver(this._onResize.bind(this));
}
this._resizeObserver.observe(this._container);
} else {
this._resizeObserver.disconnect();
}
}
if (Browser.any3d && this.options.transform3DLimit) {
(remove ? this.off : this.on).call(this, 'moveend', this._onMoveEnd);
}
},
/** 处理 dom 事件 */
_handleDOMEvent(e) {
const el = e.target || e.srcElement;
// 如果地图没有初始化或者禁用了事件,则不做处理
if (!this._loaded || el['_leaflet_disable_events'] || (e.type === 'click' && this._isClickDisabled(el))) {
return;
}
const type = e.type;
if (type === 'mousedown') {
// 阻止点击 keyboard-focusable element 时候显示边框
DomUtil.preventOutline(el);
}
// 触发事件
this._fireDOMEvent(e, type);
},
/** 响应 dom 事件 */
_fireDOMEvent(e, type, canvasTargets) {
if (e.type === 'click') {
// Fire a synthetic 'preclick' event which propagates up (mainly for closing popups).
// 触发一个合成的 preclick 事件,这个事件会向上冒泡(主要用于关闭弹出框popups的窗口)。
// @event preclick: MouseEvent
// Fired before mouse click on the map (sometimes useful when you want something to happen on click before any existing click handlers start running).
// 在单击 map 之前触发(如果你想要在点击事件之前处理一些事情,这种方式很有用)
const synth = Util.extend({}, e);
synth.type = 'preclick';
this._fireDOMEvent(synth, synth.type, canvasTargets);
}
// Find the layer the event is propagating from and its parents.
// 找出事件冒泡的初始图层和它的父级。target 的内容是有顺序的(按照目标图层,从下到父级依次)
let targets = this._findEventTargets(e, type);
// 只把注册了监听函数的 canvas target 添加进来
if (canvasTargets) {
const filtered = [];
for (let i = 0; i < canvasTargets.length; i++) {
if (canvasTargets[i].listens(type, true)) {
filtered.push(canvasTargets[i]);
}
}
targets = filtered.concat(targets);
}
// 事件没有目标,就直接返回
if (!targets.length) {
return;
}
// 阻止浏览器默认的右键
if (type === 'contextmenu') {
DomEvent.preventDefault(e);
}
const target = targets[0];
const data = {
originalEvent: e
};
// 不是键盘事件,是鼠标或者手势事件
if (e.type !== 'keypress' && e.type !== 'keydown' && e.type !== 'keyup') {
const isMarker = target.getLatLng && (!target._radius || target._radius <= 10); // 是不是 marker 图层
// 为事件 data 添加 leaflet 封装的属性
data.containerPoint = isMarker ? this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e);
data.layerPoint = this.containerPointToLayerPoint(data.containerPoint);
data.latlng = isMarker ? target.getLatLng() : this.layerPointToLatLng(data.layerPoint);
}
for (let i = 0; i < targets.length; i++) {
// 依次触发各个 target 上的事件,data 中有 leaflet 添加的属性
targets[i].fire(type, data, true);
if (data.originalEvent._stopped || (targets[i].options.bubblingMouseEvents === false && this._mouseEvents.includes(type))) {
// 阻止了冒泡,就不触发后续的事件了
return;
}
}
},
/** 查找事件的目标图层 */
_findEventTargets(e, type) {
let targets = [],
target,
src = e.target || e.srcElement,
dragging = false;
const isHover = type === 'mouseout' || type === 'mouseover';
while (src) {
// 如果 src 绑定过 dom 事件,会在 this._targets 中
// layer 在创建的时候会注册 dom 事件,也会向 map的 _targets 添加属性
target = this._targets[Util.stamp(src)];
if (target && (type === 'click' || type === 'preclick') && this._draggableMoved(target)) {
// Prevent firing click after you just dragged an object.
// 如果是 drag,就阻止点击事件,停止后续的父级循环
dragging = true;
break;
}
if (target && target.listens(type, true)) {
// 是 hover 类型事件,但是鼠标进入或者移出的目标元素不是 src,就停止后续的父级循环
if (isHover && !DomEvent.isExternalTarget(src, e)) {
break;
}
// 将 target 存入数组
targets.push(target);
// 如果是 hover 就停止后续的父级循环,不会响应到父节点上
// 例如 map 和 marker 都注册了 mouseover 事件,当鼠标在 marker 上的时候,map的 mouseover 就不会继续响应了
if (isHover) {
break;
}
}
// 如果循环到了根节点,没有父节点了,就停止
if (src === this._container) {
break;
}
// 设置为父节点继续循环
src = src.parentNode;
}
// 如果 targets 为空且是 dragging 且不是 hover 且 map 已经注册过当前类型的事件,那么 target 就是 map
if (!targets.length && !dragging && !isHover && this.listens(type, true)) {
targets = [this];
}
return targets;
},
/** 判断是不是在 drag */
_draggableMoved(obj) {
obj = obj.dragging && obj.dragging.enabled() ? obj : this;
return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved());
},
/** move 结束,检查下transform的值,如果超限了就重置一下 */
_onMoveEnd() {
const pos = this._getMapPanePos();
if (Math.max(Math.abs(pos.x), Math.abs(pos.y)) >= this.options.transform3DLimit) {
// https://bugzilla.mozilla.org/show_bug.cgi?id=1203873 but Webkit also have
// a pixel offset on very high values, see: https://jsfiddle.net/dg6r5hhb/
this._resetView(this.getCenter(), this.getZoom());
}
},
/** 处理地图 resize */
_onResize() {
Util.cancelAnimFrame(this._resizeRequest);
this._resizeRequest = Util.requestAnimFrame(function () {
this.invalidateSize({debounceMoveend: true});
}, this);
},
// @method invalidateSize(options: Zoom/pan options): this
// @alternative
// @method invalidateSize(animate: Boolean): this
// 检查地图容器的 size 是否改变,如果改变了就更新 map,动态改变 map 的大小后调用这个方法。默认是进行平移处理。
// 如果 options.pan 是 false,panning 不会发生
// 如果 options.debounceMoveend 是 true`, 会延迟 moveend 事件响应,即使连续多次调用该方法也不会响应
invalidateSize(options) {
if (!this._loaded) {
return this;
}
options = Util.extend(
{
animate: false,
pan: true
},
options === true ? {animate: true} : options
);
const oldSize = this.getSize();
this._sizeChanged = true;
this._lastCenter = null;
const newSize = this.getSize(),
oldCenter = oldSize.divideBy(2).round(),
newCenter = newSize.divideBy(2).round(), // 计算新的地图中心
offset = oldCenter.subtract(newCenter); // 计算平移距离
if (!offset.x && !offset.y) {
return this;
}
// 平移地图
if (options.animate && options.pan) {
this.panBy(offset);
} else {
if (options.pan) {
this._rawPanBy(offset);
}
// 触发 move 事件
this.fire('move');
// 200 毫秒内连续多次改变 map size,不会重复触发 moveend 事件
if (options.debounceMoveend) {
clearTimeout(this._sizeTimer);
this._sizeTimer = setTimeout(this.fire.bind(this, 'moveend'), 200);
} else {
this.fire('moveend');
}
}
// @section Map state change events
// @event resize: ResizeEvent
// 触发地图 resize 事件
return this.fire('resize', {
oldSize,
newSize
});
},
/** 根节点上scroll的时候,保证根节点位置一直是(0,0) */
_onScroll() {
this._container.scrollTop = 0;
this._container.scrollLeft = 0;
},
// ......
})