leaflet
Version:
JavaScript library for mobile-friendly interactive maps
309 lines (241 loc) • 10.1 kB
JavaScript
/*
* @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.
var eventsKey = '_leaflet_events';
L.DomEvent = {
// @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, mousemove: onMouseMove}`
on: function (obj, types, fn, context) {
if (typeof types === 'object') {
for (var type in types) {
this._on(obj, type, types[type], fn);
}
} else {
types = L.Util.splitWords(types);
for (var i = 0, len = types.length; i < len; i++) {
this._on(obj, types[i], fn, context);
}
}
return this;
},
// @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this
// Removes a previously added listener function. If no function is specified,
// it will remove all the listeners of that particular DOM event from the element.
// 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, mousemove: onMouseMove}`
off: function (obj, types, fn, context) {
if (typeof types === 'object') {
for (var type in types) {
this._off(obj, type, types[type], fn);
}
} else {
types = L.Util.splitWords(types);
for (var i = 0, len = types.length; i < len; i++) {
this._off(obj, types[i], fn, context);
}
}
return this;
},
_on: function (obj, type, fn, context) {
var id = type + L.stamp(fn) + (context ? '_' + L.stamp(context) : '');
if (obj[eventsKey] && obj[eventsKey][id]) { return this; }
var handler = function (e) {
return fn.call(context || obj, e || window.event);
};
var originalHandler = handler;
if (L.Browser.pointer && type.indexOf('touch') === 0) {
this.addPointerListener(obj, type, handler, id);
} else if (L.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener) {
this.addDoubleTapListener(obj, handler, id);
} else if ('addEventListener' in obj) {
if (type === 'mousewheel') {
obj.addEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, false);
} else if ((type === 'mouseenter') || (type === 'mouseleave')) {
handler = function (e) {
e = e || window.event;
if (L.DomEvent._isExternalTarget(obj, e)) {
originalHandler(e);
}
};
obj.addEventListener(type === 'mouseenter' ? 'mouseover' : 'mouseout', handler, false);
} else {
if (type === 'click' && L.Browser.android) {
handler = function (e) {
return L.DomEvent._filterClick(e, originalHandler);
};
}
obj.addEventListener(type, handler, false);
}
} else if ('attachEvent' in obj) {
obj.attachEvent('on' + type, handler);
}
obj[eventsKey] = obj[eventsKey] || {};
obj[eventsKey][id] = handler;
return this;
},
_off: function (obj, type, fn, context) {
var id = type + L.stamp(fn) + (context ? '_' + L.stamp(context) : ''),
handler = obj[eventsKey] && obj[eventsKey][id];
if (!handler) { return this; }
if (L.Browser.pointer && type.indexOf('touch') === 0) {
this.removePointerListener(obj, type, id);
} else if (L.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) {
this.removeDoubleTapListener(obj, id);
} else if ('removeEventListener' in obj) {
if (type === 'mousewheel') {
obj.removeEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, false);
} else {
obj.removeEventListener(
type === 'mouseenter' ? 'mouseover' :
type === 'mouseleave' ? 'mouseout' : type, handler, false);
}
} else if ('detachEvent' in obj) {
obj.detachEvent('on' + type, handler);
}
obj[eventsKey][id] = null;
return this;
},
// @function stopPropagation(ev: DOMEvent): this
// Stop the given event from propagation to parent elements. Used inside the listener functions:
// ```js
// L.DomEvent.on(div, 'click', function (ev) {
// L.DomEvent.stopPropagation(ev);
// });
// ```
stopPropagation: function (e) {
if (e.stopPropagation) {
e.stopPropagation();
} else if (e.originalEvent) { // In case of Leaflet event.
e.originalEvent._stopped = true;
} else {
e.cancelBubble = true;
}
L.DomEvent._skipped(e);
return this;
},
// @function disableScrollPropagation(el: HTMLElement): this
// Adds `stopPropagation` to the element's `'mousewheel'` events (plus browser variants).
disableScrollPropagation: function (el) {
return L.DomEvent.on(el, 'mousewheel', L.DomEvent.stopPropagation);
},
// @function disableClickPropagation(el: HTMLElement): this
// Adds `stopPropagation` to the element's `'click'`, `'doubleclick'`,
// `'mousedown'` and `'touchstart'` events (plus browser variants).
disableClickPropagation: function (el) {
var stop = L.DomEvent.stopPropagation;
L.DomEvent.on(el, L.Draggable.START.join(' '), stop);
return L.DomEvent.on(el, {
click: L.DomEvent._fakeStop,
dblclick: stop
});
},
// @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.
preventDefault: function (e) {
if (e.preventDefault) {
e.preventDefault();
} else {
e.returnValue = false;
}
return this;
},
// @function stop(ev): this
// Does `stopPropagation` and `preventDefault` at the same time.
stop: function (e) {
return L.DomEvent
.preventDefault(e)
.stopPropagation(e);
},
// @function getMousePosition(ev: DOMEvent, container?: HTMLElement): Point
// Gets normalized mouse position from a DOM event relative to the
// `container` or to the whole page if not specified.
getMousePosition: function (e, container) {
if (!container) {
return new L.Point(e.clientX, e.clientY);
}
var rect = container.getBoundingClientRect();
return new L.Point(
e.clientX - rect.left - container.clientLeft,
e.clientY - rect.top - container.clientTop);
},
// Chrome on Win scrolls double the pixels as in other platforms (see #4538),
// and Firefox scrolls device pixels, not CSS pixels
_wheelPxFactor: (L.Browser.win && L.Browser.chrome) ? 2 :
L.Browser.gecko ? window.devicePixelRatio :
1,
// @function getWheelDelta(ev: DOMEvent): Number
// Gets normalized wheel delta from a mousewheel 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.
getWheelDelta: function (e) {
return (L.Browser.edge) ? e.wheelDeltaY / 2 : // Don't trust window-geometry-based delta
(e.deltaY && e.deltaMode === 0) ? -e.deltaY / L.DomEvent._wheelPxFactor : // 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
e.wheelDelta ? (e.wheelDeltaY || e.wheelDelta) / 2 : // Legacy IE pixels
(e.detail && Math.abs(e.detail) < 32765) ? -e.detail * 20 : // Legacy Moz lines
e.detail ? e.detail / -32765 * 60 : // Legacy Moz pages
0;
},
_skipEvents: {},
_fakeStop: function (e) {
// fakes stopPropagation by setting a special event flag, checked/reset with L.DomEvent._skipped(e)
L.DomEvent._skipEvents[e.type] = true;
},
_skipped: function (e) {
var skipped = this._skipEvents[e.type];
// reset when checking, as it's only used in map container and propagates outside of the map
this._skipEvents[e.type] = false;
return skipped;
},
// check if element really left/entered the event target (for mouseenter/mouseleave)
_isExternalTarget: function (el, e) {
var related = e.relatedTarget;
if (!related) { return true; }
try {
while (related && (related !== el)) {
related = related.parentNode;
}
} catch (err) {
return false;
}
return (related !== el);
},
// this is a horrible workaround for a bug in Android where a single touch triggers two click events
_filterClick: function (e, handler) {
var timeStamp = (e.timeStamp || (e.originalEvent && e.originalEvent.timeStamp)),
elapsed = L.DomEvent._lastClick && (timeStamp - L.DomEvent._lastClick);
// are they closer together than 500ms yet more than 100ms?
// Android typically triggers them ~300ms apart while multiple listeners
// on the same event should be triggered far faster;
// or check if click is simulated on the element, and if it is, reject any non-simulated events
if ((elapsed && elapsed > 100 && elapsed < 500) || (e.target._simulatedClick && !e._simulated)) {
L.DomEvent.stop(e);
return;
}
L.DomEvent._lastClick = timeStamp;
handler(e);
}
};
// @function addListener(…): this
// Alias to [`L.DomEvent.on`](#domevent-on)
L.DomEvent.addListener = L.DomEvent.on;
// @function removeListener(…): this
// Alias to [`L.DomEvent.off`](#domevent-off)
L.DomEvent.removeListener = L.DomEvent.off;