UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

314 lines (286 loc) 11.9 kB
/** * 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};