UNPKG

10.4 kBJavaScriptView Raw
1import {Point} from '../geometry/Point';
2import * as Util from '../core/Util';
3import * as Browser from '../core/Browser';
4import {addPointerListener, removePointerListener} from './DomEvent.Pointer';
5import {addDoubleTapListener, removeDoubleTapListener} from './DomEvent.DoubleTap';
6import {getScale} from './DomUtil';
7
8/*
9 * @namespace DomEvent
10 * Utility functions to work with the [DOM events](https://developer.mozilla.org/docs/Web/API/Event), used by Leaflet internally.
11 */
12
13// Inspired by John Resig, Dean Edwards and YUI addEvent implementations.
14
15// @function on(el: HTMLElement, types: String, fn: Function, context?: Object): this
16// Adds a listener function (`fn`) to a particular DOM event type of the
17// element `el`. You can optionally specify the context of the listener
18// (object the `this` keyword will point to). You can also pass several
19// space-separated types (e.g. `'click dblclick'`).
20
21// @alternative
22// @function on(el: HTMLElement, eventMap: Object, context?: Object): this
23// Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
24export function on(obj, types, fn, context) {
25
26 if (typeof types === 'object') {
27 for (var type in types) {
28 addOne(obj, type, types[type], fn);
29 }
30 } else {
31 types = Util.splitWords(types);
32
33 for (var i = 0, len = types.length; i < len; i++) {
34 addOne(obj, types[i], fn, context);
35 }
36 }
37
38 return this;
39}
40
41var eventsKey = '_leaflet_events';
42
43// @function off(el: HTMLElement, types: String, fn: Function, context?: Object): this
44// Removes a previously added listener function.
45// Note that if you passed a custom context to on, you must pass the same
46// context to `off` in order to remove the listener.
47
48// @alternative
49// @function off(el: HTMLElement, eventMap: Object, context?: Object): this
50// Removes a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`
51export function off(obj, types, fn, context) {
52
53 if (typeof types === 'object') {
54 for (var type in types) {
55 removeOne(obj, type, types[type], fn);
56 }
57 } else if (types) {
58 types = Util.splitWords(types);
59
60 for (var i = 0, len = types.length; i < len; i++) {
61 removeOne(obj, types[i], fn, context);
62 }
63 } else {
64 for (var j in obj[eventsKey]) {
65 removeOne(obj, j, obj[eventsKey][j]);
66 }
67 delete obj[eventsKey];
68 }
69
70 return this;
71}
72
73function addOne(obj, type, fn, context) {
74 var id = type + Util.stamp(fn) + (context ? '_' + Util.stamp(context) : '');
75
76 if (obj[eventsKey] && obj[eventsKey][id]) { return this; }
77
78 var handler = function (e) {
79 return fn.call(context || obj, e || window.event);
80 };
81
82 var originalHandler = handler;
83
84 if (Browser.pointer && type.indexOf('touch') === 0) {
85 // Needs DomEvent.Pointer.js
86 addPointerListener(obj, type, handler, id);
87
88 } else if (Browser.touch && (type === 'dblclick') && addDoubleTapListener &&
89 !(Browser.pointer && Browser.chrome)) {
90 // Chrome >55 does not need the synthetic dblclicks from addDoubleTapListener
91 // See #5180
92 addDoubleTapListener(obj, handler, id);
93
94 } else if ('addEventListener' in obj) {
95
96 if (type === 'mousewheel') {
97 obj.addEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, false);
98
99 } else if ((type === 'mouseenter') || (type === 'mouseleave')) {
100 handler = function (e) {
101 e = e || window.event;
102 if (isExternalTarget(obj, e)) {
103 originalHandler(e);
104 }
105 };
106 obj.addEventListener(type === 'mouseenter' ? 'mouseover' : 'mouseout', handler, false);
107
108 } else {
109 if (type === 'click' && Browser.android) {
110 handler = function (e) {
111 filterClick(e, originalHandler);
112 };
113 }
114 obj.addEventListener(type, handler, false);
115 }
116
117 } else if ('attachEvent' in obj) {
118 obj.attachEvent('on' + type, handler);
119 }
120
121 obj[eventsKey] = obj[eventsKey] || {};
122 obj[eventsKey][id] = handler;
123}
124
125function removeOne(obj, type, fn, context) {
126
127 var id = type + Util.stamp(fn) + (context ? '_' + Util.stamp(context) : ''),
128 handler = obj[eventsKey] && obj[eventsKey][id];
129
130 if (!handler) { return this; }
131
132 if (Browser.pointer && type.indexOf('touch') === 0) {
133 removePointerListener(obj, type, id);
134
135 } else if (Browser.touch && (type === 'dblclick') && removeDoubleTapListener &&
136 !(Browser.pointer && Browser.chrome)) {
137 removeDoubleTapListener(obj, id);
138
139 } else if ('removeEventListener' in obj) {
140
141 if (type === 'mousewheel') {
142 obj.removeEventListener('onwheel' in obj ? 'wheel' : 'mousewheel', handler, false);
143
144 } else {
145 obj.removeEventListener(
146 type === 'mouseenter' ? 'mouseover' :
147 type === 'mouseleave' ? 'mouseout' : type, handler, false);
148 }
149
150 } else if ('detachEvent' in obj) {
151 obj.detachEvent('on' + type, handler);
152 }
153
154 obj[eventsKey][id] = null;
155}
156
157// @function stopPropagation(ev: DOMEvent): this
158// Stop the given event from propagation to parent elements. Used inside the listener functions:
159// ```js
160// L.DomEvent.on(div, 'click', function (ev) {
161// L.DomEvent.stopPropagation(ev);
162// });
163// ```
164export function stopPropagation(e) {
165
166 if (e.stopPropagation) {
167 e.stopPropagation();
168 } else if (e.originalEvent) { // In case of Leaflet event.
169 e.originalEvent._stopped = true;
170 } else {
171 e.cancelBubble = true;
172 }
173 skipped(e);
174
175 return this;
176}
177
178// @function disableScrollPropagation(el: HTMLElement): this
179// Adds `stopPropagation` to the element's `'mousewheel'` events (plus browser variants).
180export function disableScrollPropagation(el) {
181 addOne(el, 'mousewheel', stopPropagation);
182 return this;
183}
184
185// @function disableClickPropagation(el: HTMLElement): this
186// Adds `stopPropagation` to the element's `'click'`, `'doubleclick'`,
187// `'mousedown'` and `'touchstart'` events (plus browser variants).
188export function disableClickPropagation(el) {
189 on(el, 'mousedown touchstart dblclick', stopPropagation);
190 addOne(el, 'click', fakeStop);
191 return this;
192}
193
194// @function preventDefault(ev: DOMEvent): this
195// Prevents the default action of the DOM Event `ev` from happening (such as
196// following a link in the href of the a element, or doing a POST request
197// with page reload when a `<form>` is submitted).
198// Use it inside listener functions.
199export function preventDefault(e) {
200 if (e.preventDefault) {
201 e.preventDefault();
202 } else {
203 e.returnValue = false;
204 }
205 return this;
206}
207
208// @function stop(ev: DOMEvent): this
209// Does `stopPropagation` and `preventDefault` at the same time.
210export function stop(e) {
211 preventDefault(e);
212 stopPropagation(e);
213 return this;
214}
215
216// @function getMousePosition(ev: DOMEvent, container?: HTMLElement): Point
217// Gets normalized mouse position from a DOM event relative to the
218// `container` (border excluded) or to the whole page if not specified.
219export function getMousePosition(e, container) {
220 if (!container) {
221 return new Point(e.clientX, e.clientY);
222 }
223
224 var scale = getScale(container),
225 offset = scale.boundingClientRect; // left and top values are in page scale (like the event clientX/Y)
226
227 return new Point(
228 // offset.left/top values are in page scale (like clientX/Y),
229 // whereas clientLeft/Top (border width) values are the original values (before CSS scale applies).
230 (e.clientX - offset.left) / scale.x - container.clientLeft,
231 (e.clientY - offset.top) / scale.y - container.clientTop
232 );
233}
234
235// Chrome on Win scrolls double the pixels as in other platforms (see #4538),
236// and Firefox scrolls device pixels, not CSS pixels
237var wheelPxFactor =
238 (Browser.win && Browser.chrome) ? 2 * window.devicePixelRatio :
239 Browser.gecko ? window.devicePixelRatio : 1;
240
241// @function getWheelDelta(ev: DOMEvent): Number
242// Gets normalized wheel delta from a mousewheel DOM event, in vertical
243// pixels scrolled (negative if scrolling down).
244// Events from pointing devices without precise scrolling are mapped to
245// a best guess of 60 pixels.
246export function getWheelDelta(e) {
247 return (Browser.edge) ? e.wheelDeltaY / 2 : // Don't trust window-geometry-based delta
248 (e.deltaY && e.deltaMode === 0) ? -e.deltaY / wheelPxFactor : // Pixels
249 (e.deltaY && e.deltaMode === 1) ? -e.deltaY * 20 : // Lines
250 (e.deltaY && e.deltaMode === 2) ? -e.deltaY * 60 : // Pages
251 (e.deltaX || e.deltaZ) ? 0 : // Skip horizontal/depth wheel events
252 e.wheelDelta ? (e.wheelDeltaY || e.wheelDelta) / 2 : // Legacy IE pixels
253 (e.detail && Math.abs(e.detail) < 32765) ? -e.detail * 20 : // Legacy Moz lines
254 e.detail ? e.detail / -32765 * 60 : // Legacy Moz pages
255 0;
256}
257
258var skipEvents = {};
259
260export function fakeStop(e) {
261 // fakes stopPropagation by setting a special event flag, checked/reset with skipped(e)
262 skipEvents[e.type] = true;
263}
264
265export function skipped(e) {
266 var events = skipEvents[e.type];
267 // reset when checking, as it's only used in map container and propagates outside of the map
268 skipEvents[e.type] = false;
269 return events;
270}
271
272// check if element really left/entered the event target (for mouseenter/mouseleave)
273export function isExternalTarget(el, e) {
274
275 var related = e.relatedTarget;
276
277 if (!related) { return true; }
278
279 try {
280 while (related && (related !== el)) {
281 related = related.parentNode;
282 }
283 } catch (err) {
284 return false;
285 }
286 return (related !== el);
287}
288
289var lastClick;
290
291// this is a horrible workaround for a bug in Android where a single touch triggers two click events
292function filterClick(e, handler) {
293 var timeStamp = (e.timeStamp || (e.originalEvent && e.originalEvent.timeStamp)),
294 elapsed = lastClick && (timeStamp - lastClick);
295
296 // are they closer together than 500ms yet more than 100ms?
297 // Android typically triggers them ~300ms apart while multiple listeners
298 // on the same event should be triggered far faster;
299 // or check if click is simulated on the element, and if it is, reject any non-simulated events
300
301 if ((elapsed && elapsed > 100 && elapsed < 500) || (e.target._simulatedClick && !e._simulated)) {
302 stop(e);
303 return;
304 }
305 lastClick = timeStamp;
306
307 handler(e);
308}
309
310// @function addListener(…): this
311// Alias to [`L.DomEvent.on`](#domevent-on)
312export {on as addListener};
313
314// @function removeListener(…): this
315// Alias to [`L.DomEvent.off`](#domevent-off)
316export {off as removeListener};