leaflet
Version:
JavaScript library for mobile-friendly interactive maps
282 lines (227 loc) • 8.93 kB
JavaScript
import {Point} from '../geometry/Point.js';
import * as Util from '../core/Util.js';
import Browser from '../core/Browser.js';
import {addDoubleTapListener, removeDoubleTapListener} from './DomEvent.DoubleTap.js';
import {getScale} from './DomUtil.js';
import * as PointerEvents from './DomEvent.PointerEvents.js';
export {PointerEvents};
/*
* @namespace DomEvent
* Utility functions to work with the [DOM events](https://developer.mozilla.org/docs/Web/API/Event), used by Leaflet internally.
*/
// Inspired by John Resig, Dean Edwards and YUI addEvent implementations.
// @function on(el: HTMLElement, types: String, fn: Function, context?: Object): this
// Adds a listener function (`fn`) to a particular DOM event type of the
// element `el`. You can optionally specify the context of the listener
// (object the `this` keyword will point to). You can also pass several
// space-separated types (e.g. `'click dblclick'`).
// @alternative
// @function on(el: HTMLElement, eventMap: Object, context?: Object): this
// Adds a set of type/listener pairs, e.g. `{click: onClick, pointermove: onPointerMove}`
export function on(obj, types, fn, context) {
if (types && typeof types === 'object') {
for (const [type, listener] of Object.entries(types)) {
addOne(obj, type, listener, fn);
}
} else {
for (const type of Util.splitWords(types)) {
addOne(obj, type, fn, context);
}
}
return this;
}
const eventsKey = '_leaflet_events';
// @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this
// Removes a previously added listener function.
// Note that if you passed a custom context to on, you must pass the same
// context to `off` in order to remove the listener.
// @alternative
// @function off(el: HTMLElement, eventMap: Object, context?: Object): this
// Removes a set of type/listener pairs, e.g. `{click: onClick, pointermove: onPointerMove}`
// @alternative
// @function off(el: HTMLElement, types: String): this
// Removes all previously added listeners of given types.
// @alternative
// @function off(el: HTMLElement): this
// Removes all previously added listeners from given HTMLElement
export function off(obj, types, fn, context) {
if (arguments.length === 1) {
batchRemove(obj);
delete obj[eventsKey];
} else if (types && typeof types === 'object') {
for (const [type, listener] of Object.entries(types)) {
removeOne(obj, type, listener, fn);
}
} else {
types = Util.splitWords(types);
if (arguments.length === 2) {
batchRemove(obj, type => types.includes(type));
} else {
for (const type of types) {
removeOne(obj, type, fn, context);
}
}
}
return this;
}
function batchRemove(obj, filterFn) {
for (const id of Object.keys(obj[eventsKey] ?? {})) {
const type = id.split(/\d/)[0];
if (!filterFn || filterFn(type)) {
removeOne(obj, type, null, null, id);
}
}
}
const pointerSubst = {
pointerenter: 'pointerover',
pointerleave: 'pointerout',
wheel: typeof window === 'undefined' ? false : !('onwheel' in window) && 'mousewheel'
};
function addOne(obj, type, fn, context) {
const id = type + Util.stamp(fn) + (context ? `_${Util.stamp(context)}` : '');
if (obj[eventsKey] && obj[eventsKey][id]) { return this; }
let handler = function (e) {
return fn.call(context || obj, e || window.event);
};
const originalHandler = handler;
if (Browser.touch && (type === 'dblclick')) {
handler = addDoubleTapListener(obj, handler);
} else if ('addEventListener' in obj) {
if (type === 'wheel' || type === 'mousewheel') {
obj.addEventListener(pointerSubst[type] || type, handler, {passive: false});
} else if (type === 'pointerenter' || type === 'pointerleave') {
handler = function (e) {
e ??= window.event;
if (isExternalTarget(obj, e)) {
originalHandler(e);
}
};
obj.addEventListener(pointerSubst[type], handler, false);
} else {
obj.addEventListener(type, originalHandler, false);
}
} else {
obj.attachEvent(`on${type}`, handler);
}
obj[eventsKey] ??= {};
obj[eventsKey][id] = handler;
}
function removeOne(obj, type, fn, context, id) {
id ??= type + Util.stamp(fn) + (context ? `_${Util.stamp(context)}` : '');
const handler = obj[eventsKey] && obj[eventsKey][id];
if (!handler) { return this; }
if (Browser.touch && (type === 'dblclick')) {
removeDoubleTapListener(obj, handler);
} else if ('removeEventListener' in obj) {
obj.removeEventListener(pointerSubst[type] || type, handler, false);
} else {
obj.detachEvent(`on${type}`, handler);
}
obj[eventsKey][id] = null;
}
// @function stopPropagation(ev: DOMEvent): this
// Stop the given event from propagation to parent elements. Used inside the listener functions:
// ```js
// DomEvent.on(div, 'click', DomEvent.stopPropagation);
// ```
export function stopPropagation(e) {
if (e.stopPropagation) {
e.stopPropagation();
} else if (e.originalEvent) { // In case of Leaflet event.
e.originalEvent._stopped = true;
} else {
e.cancelBubble = true;
}
return this;
}
// @function disableScrollPropagation(el: HTMLElement): this
// Adds `stopPropagation` to the element's `'wheel'` events (plus browser variants).
export function disableScrollPropagation(el) {
addOne(el, 'wheel', stopPropagation);
return this;
}
// @function disableClickPropagation(el: HTMLElement): this
// Adds `stopPropagation` to the element's `'click'`, `'dblclick'`, `'contextmenu'`
// and `'pointerdown'` events (plus browser variants).
export function disableClickPropagation(el) {
on(el, 'pointerdown dblclick contextmenu', stopPropagation);
el['_leaflet_disable_click'] = true;
return this;
}
// @function preventDefault(ev: DOMEvent): this
// Prevents the default action of the DOM Event `ev` from happening (such as
// following a link in the href of the a element, or doing a POST request
// with page reload when a `<form>` is submitted).
// Use it inside listener functions.
export function preventDefault(e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
return this;
}
// @function stop(ev: DOMEvent): this
// Does `stopPropagation` and `preventDefault` at the same time.
export function stop(e) {
preventDefault(e);
stopPropagation(e);
return this;
}
// @function getPropagationPath(ev: DOMEvent): Array
// Returns an array containing the `HTMLElement`s that the given DOM event
// should propagate to (if not stopped).
export function getPropagationPath(ev) {
return ev.composedPath();
}
// @function getPointerPosition(ev: DOMEvent, container?: HTMLElement): Point
// Gets normalized pointer position from a DOM event relative to the
// `container` (border excluded) or to the whole page if not specified.
export function getPointerPosition(e, container) {
if (!container) {
return new Point(e.clientX, e.clientY);
}
const scale = getScale(container),
offset = scale.boundingClientRect; // left and top values are in page scale (like the event clientX/Y)
return new Point(
// offset.left/top values are in page scale (like clientX/Y),
// whereas clientLeft/Top (border width) values are the original values (before CSS scale applies).
(e.clientX - offset.left) / scale.x - container.clientLeft,
(e.clientY - offset.top) / scale.y - container.clientTop
);
}
// @function getWheelPxFactor(): Number
// Gets the wheel pixel factor based on the devicePixelRatio
export function getWheelPxFactor() {
// We need double the scroll pixels (see #7403 and #4538) for all Browsers
// except OSX (Mac) -> 3x, Chrome running on Linux 1x
const ratio = window.devicePixelRatio;
return Browser.linux && Browser.chrome ? ratio :
Browser.mac ? ratio * 3 :
ratio > 0 ? 2 * ratio : 1;
}
// @function getWheelDelta(ev: DOMEvent): Number
// Gets normalized wheel delta from a wheel DOM event, in vertical
// pixels scrolled (negative if scrolling down).
// Events from pointing devices without precise scrolling are mapped to
// a best guess of 60 pixels.
export function getWheelDelta(e) {
return (e.deltaY && e.deltaMode === 0) ? -e.deltaY / getWheelPxFactor() : // Pixels
(e.deltaY && e.deltaMode === 1) ? -e.deltaY * 20 : // Lines
(e.deltaY && e.deltaMode === 2) ? -e.deltaY * 60 : // Pages
(e.deltaX || e.deltaZ) ? 0 : // Skip horizontal/depth wheel events
0;
}
// check if element really left/entered the event target (for pointerenter/pointerleave)
export function isExternalTarget(el, e) {
let related = e.relatedTarget;
if (!related) { return true; }
try {
while (related && (related !== el)) {
related = related.parentNode;
}
} catch (err) {
return false;
}
return (related !== el);
}