/**
|
* Utilities for mouse or touch events.
|
*/
|
|
import Eventful from './Eventful';
|
import env from './env';
|
import { ZRRawEvent } from './types';
|
import {isCanvasEl, transformCoordWithViewport} from './dom';
|
|
const MOUSE_EVENT_REG = /^(?:mouse|pointer|contextmenu|drag|drop)|click/;
|
const _calcOut: number[] = [];
|
const firefoxNotSupportOffsetXY = env.browser.firefox
|
// use offsetX/offsetY for Firefox >= 39
|
// PENDING: consider Firefox for Android and Firefox OS? >= 43
|
&& +(env.browser.version as string).split('.')[0] < 39;
|
|
type FirefoxMouseEvent = {
|
layerX: number
|
layerY: number
|
}
|
|
|
/**
|
* Get the `zrX` and `zrY`, which are relative to the top-left of
|
* the input `el`.
|
* CSS transform (2D & 3D) is supported.
|
*
|
* The strategy to fetch the coords:
|
* + If `calculate` is not set as `true`, users of this method should
|
* ensure that `el` is the same or the same size & location as `e.target`.
|
* Otherwise the result coords are probably not expected. Because we
|
* firstly try to get coords from e.offsetX/e.offsetY.
|
* + If `calculate` is set as `true`, the input `el` can be any element
|
* and we force to calculate the coords based on `el`.
|
* + The input `el` should be positionable (not position:static).
|
*
|
* The force `calculate` can be used in case like:
|
* When mousemove event triggered on ec tooltip, `e.target` is not `el`(zr painter.dom).
|
*
|
* @param el DOM element.
|
* @param e Mouse event or touch event.
|
* @param out Get `out.zrX` and `out.zrY` as the result.
|
* @param calculate Whether to force calculate
|
* the coordinates but not use ones provided by browser.
|
*/
|
export function clientToLocal(
|
el: HTMLElement,
|
e: ZRRawEvent | FirefoxMouseEvent | Touch,
|
out: {zrX?: number, zrY?: number},
|
calculate?: boolean
|
) {
|
out = out || {};
|
|
// According to the W3C Working Draft, offsetX and offsetY should be relative
|
// to the padding edge of the target element. The only browser using this convention
|
// is IE. Webkit uses the border edge, Opera uses the content edge, and FireFox does
|
// not support the properties.
|
// (see http://www.jacklmoore.com/notes/mouse-position/)
|
// In zr painter.dom, padding edge equals to border edge.
|
if (calculate) {
|
calculateZrXY(el, e as ZRRawEvent, out);
|
}
|
// Caution: In FireFox, layerX/layerY Mouse position relative to the closest positioned
|
// ancestor element, so we should make sure el is positioned (e.g., not position:static).
|
// BTW1, Webkit don't return the same results as FF in non-simple cases (like add
|
// zoom-factor, overflow / opacity layers, transforms ...)
|
// BTW2, (ev.offsetY || ev.pageY - $(ev.target).offset().top) is not correct in preserve-3d.
|
// <https://bugs.jquery.com/ticket/8523#comment:14>
|
// BTW3, In ff, offsetX/offsetY is always 0.
|
else if (firefoxNotSupportOffsetXY
|
&& (e as FirefoxMouseEvent).layerX != null
|
&& (e as FirefoxMouseEvent).layerX !== (e as MouseEvent).offsetX
|
) {
|
out.zrX = (e as FirefoxMouseEvent).layerX;
|
out.zrY = (e as FirefoxMouseEvent).layerY;
|
}
|
// For IE6+, chrome, safari, opera, firefox >= 39
|
else if ((e as MouseEvent).offsetX != null) {
|
out.zrX = (e as MouseEvent).offsetX;
|
out.zrY = (e as MouseEvent).offsetY;
|
}
|
// For some other device, e.g., IOS safari.
|
else {
|
calculateZrXY(el, e as ZRRawEvent, out);
|
}
|
|
return out;
|
}
|
|
function calculateZrXY(
|
el: HTMLElement,
|
e: ZRRawEvent,
|
out: {zrX?: number, zrY?: number}
|
) {
|
// BlackBerry 5, iOS 3 (original iPhone) don't have getBoundingRect.
|
if (env.domSupported && el.getBoundingClientRect) {
|
const ex = (e as MouseEvent).clientX;
|
const ey = (e as MouseEvent).clientY;
|
|
if (isCanvasEl(el)) {
|
// Original approach, which do not support CSS transform.
|
// marker can not be locationed in a canvas container
|
// (getBoundingClientRect is always 0). We do not support
|
// that input a pre-created canvas to zr while using css
|
// transform in iOS.
|
const box = el.getBoundingClientRect();
|
out.zrX = ex - box.left;
|
out.zrY = ey - box.top;
|
return;
|
}
|
else {
|
if (transformCoordWithViewport(_calcOut, el, ex, ey)) {
|
out.zrX = _calcOut[0];
|
out.zrY = _calcOut[1];
|
return;
|
}
|
}
|
}
|
out.zrX = out.zrY = 0;
|
}
|
|
/**
|
* Find native event compat for legency IE.
|
* Should be called at the begining of a native event listener.
|
*
|
* @param e Mouse event or touch event or pointer event.
|
* For lagency IE, we use `window.event` is used.
|
* @return The native event.
|
*/
|
export function getNativeEvent(e: ZRRawEvent): ZRRawEvent {
|
return e
|
|| (window.event as any); // For IE
|
}
|
|
/**
|
* Normalize the coordinates of the input event.
|
*
|
* Get the `e.zrX` and `e.zrY`, which are relative to the top-left of
|
* the input `el`.
|
* Get `e.zrDelta` if using mouse wheel.
|
* Get `e.which`, see the comment inside this function.
|
*
|
* Do not calculate repeatly if `zrX` and `zrY` already exist.
|
*
|
* Notice: see comments in `clientToLocal`. check the relationship
|
* between the result coords and the parameters `el` and `calculate`.
|
*
|
* @param el DOM element.
|
* @param e See `getNativeEvent`.
|
* @param calculate Whether to force calculate
|
* the coordinates but not use ones provided by browser.
|
* @return The normalized native UIEvent.
|
*/
|
export function normalizeEvent(
|
el: HTMLElement,
|
e: ZRRawEvent,
|
calculate?: boolean
|
) {
|
|
e = getNativeEvent(e);
|
|
if (e.zrX != null) {
|
return e;
|
}
|
|
const eventType = e.type;
|
const isTouch = eventType && eventType.indexOf('touch') >= 0;
|
|
if (!isTouch) {
|
clientToLocal(el, e, e, calculate);
|
const wheelDelta = getWheelDeltaMayPolyfill(e);
|
// FIXME: IE8- has "wheelDeta" in event "mousewheel" but hat different value (120 times)
|
// with Chrome and Safari. It's not correct for zrender event but we left it as it was.
|
e.zrDelta = wheelDelta ? wheelDelta / 120 : -(e.detail || 0) / 3;
|
}
|
else {
|
const touch = eventType !== 'touchend'
|
? (<TouchEvent>e).targetTouches[0]
|
: (<TouchEvent>e).changedTouches[0];
|
touch && clientToLocal(el, touch, e, calculate);
|
}
|
|
// Add which for click: 1 === left; 2 === middle; 3 === right; otherwise: 0;
|
// See jQuery: https://github.com/jquery/jquery/blob/master/src/event.js
|
// If e.which has been defined, it may be readonly,
|
// see: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/which
|
const button = (<MouseEvent>e).button;
|
if (e.which == null && button !== undefined && MOUSE_EVENT_REG.test(e.type)) {
|
(e as any).which = (button & 1 ? 1 : (button & 2 ? 3 : (button & 4 ? 2 : 0)));
|
}
|
// [Caution]: `e.which` from browser is not always reliable. For example,
|
// when press left button and `mousemove (pointermove)` in Edge, the `e.which`
|
// is 65536 and the `e.button` is -1. But the `mouseup (pointerup)` and
|
// `mousedown (pointerdown)` is the same as Chrome does.
|
|
return e;
|
}
|
|
// TODO: also provide prop "deltaX" "deltaY" in zrender "mousewheel" event.
|
function getWheelDeltaMayPolyfill(e: ZRRawEvent): number {
|
// Although event "wheel" do not has the prop "wheelDelta" in spec,
|
// agent like Chrome and Safari still provide "wheelDelta" like
|
// event "mousewheel" did (perhaps for backward compat).
|
// Since zrender has been using "wheelDeta" in zrender event "mousewheel".
|
// we currently do not break it.
|
// But event "wheel" in firefox do not has "wheelDelta", so we calculate
|
// "wheelDeta" from "deltaX", "deltaY" (which is the props in spec).
|
const rawWheelDelta = (e as any).wheelDelta;
|
// Theroetically `e.wheelDelta` won't be 0 unless some day it has been deprecated
|
// by agent like Chrome or Safari. So we also calculate it if rawWheelDelta is 0.
|
if (rawWheelDelta) {
|
return rawWheelDelta;
|
}
|
|
const deltaX = (e as any).deltaX;
|
const deltaY = (e as any).deltaY;
|
if (deltaX == null || deltaY == null) {
|
return rawWheelDelta;
|
}
|
|
// Test in Chrome and Safari (MacOS):
|
// The sign is corrent.
|
// The abs value is 99% corrent (inconsist case only like 62~63, 125~126 ...)
|
const delta = deltaY !== 0 ? Math.abs(deltaY) : Math.abs(deltaX);
|
const sign = deltaY > 0 ? -1
|
: deltaY < 0 ? 1
|
: deltaX > 0 ? -1
|
: 1;
|
return 3 * delta * sign;
|
}
|
|
|
type AddEventListenerParams = Parameters<typeof HTMLElement.prototype.addEventListener>
|
type RemoveEventListenerParams = Parameters<typeof HTMLElement.prototype.removeEventListener>
|
/**
|
* @param el
|
* @param name
|
* @param handler
|
* @param opt If boolean, means `opt.capture`
|
* @param opt.capture
|
* @param opt.passive
|
*/
|
export function addEventListener(
|
el: HTMLElement | HTMLDocument,
|
name: AddEventListenerParams[0],
|
handler: AddEventListenerParams[1],
|
opt?: AddEventListenerParams[2]
|
) {
|
// Reproduct the console warning:
|
// [Violation] Added non-passive event listener to a scroll-blocking <some> event.
|
// Consider marking event handler as 'passive' to make the page more responsive.
|
// Just set console log level: verbose in chrome dev tool.
|
// then the warning log will be printed when addEventListener called.
|
// See https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
|
// We have not yet found a neat way to using passive. Because in zrender the dom event
|
// listener delegate all of the upper events of element. Some of those events need
|
// to prevent default. For example, the feature `preventDefaultMouseMove` of echarts.
|
// Before passive can be adopted, these issues should be considered:
|
// (1) Whether and how a zrender user specifies an event listener passive. And by default,
|
// passive or not.
|
// (2) How to tread that some zrender event listener is passive, and some is not. If
|
// we use other way but not preventDefault of mousewheel and touchmove, browser
|
// compatibility should be handled.
|
|
// const opts = (env.passiveSupported && name === 'mousewheel')
|
// ? {passive: true}
|
// // By default, the third param of el.addEventListener is `capture: false`.
|
// : void 0;
|
// el.addEventListener(name, handler /* , opts */);
|
el.addEventListener(name, handler, opt);
|
}
|
|
/**
|
* Parameter are the same as `addEventListener`.
|
*
|
* Notice that if a listener is registered twice, one with capture and one without,
|
* remove each one separately. Removal of a capturing listener does not affect a
|
* non-capturing version of the same listener, and vice versa.
|
*/
|
export function removeEventListener(
|
el: HTMLElement | HTMLDocument,
|
name: RemoveEventListenerParams[0],
|
handler: RemoveEventListenerParams[1],
|
opt: RemoveEventListenerParams[2]
|
) {
|
el.removeEventListener(name, handler, opt);
|
}
|
|
/**
|
* preventDefault and stopPropagation.
|
* Notice: do not use this method in zrender. It can only be
|
* used by upper applications if necessary.
|
*
|
* @param {Event} e A mouse or touch event.
|
*/
|
export const stop = function (e: MouseEvent | TouchEvent | PointerEvent) {
|
e.preventDefault();
|
e.stopPropagation();
|
e.cancelBubble = true;
|
};
|
|
/**
|
* This method only works for mouseup and mousedown. The functionality is restricted
|
* for fault tolerance, See the `e.which` compatibility above.
|
*
|
* params can be MouseEvent or ElementEvent
|
*/
|
export function isMiddleOrRightButtonOnMouseUpDown(e: { which: number }) {
|
return e.which === 2 || e.which === 3;
|
}
|
|
// For backward compatibility
|
export {Eventful as Dispatcher};
|