UNPKG

zrender

Version:

A lightweight graphic library providing 2d draw for Apache ECharts

634 lines (520 loc) 22.2 kB
/* global document */ import { addEventListener, removeEventListener, normalizeEvent, getNativeEvent } from '../core/event'; import * as zrUtil from '../core/util'; import Eventful from '../core/Eventful'; import env from '../core/env'; import { Dictionary, ZRRawEvent, ZRRawMouseEvent } from '../core/types'; import { VectorArray } from '../core/vector'; import Handler from '../Handler'; type DomHandlersMap = Dictionary<(this: HandlerDomProxy, event: ZRRawEvent) => void> type DomExtended = Node & { domBelongToZr: boolean } const TOUCH_CLICK_DELAY = 300; const globalEventSupported = env.domSupported; const localNativeListenerNames = (function () { const mouseHandlerNames = [ 'click', 'dblclick', 'mousewheel', 'wheel', 'mouseout', 'mouseup', 'mousedown', 'mousemove', 'contextmenu' ]; const touchHandlerNames = [ 'touchstart', 'touchend', 'touchmove' ]; const pointerEventNameMap = { pointerdown: 1, pointerup: 1, pointermove: 1, pointerout: 1 }; const pointerHandlerNames = zrUtil.map(mouseHandlerNames, function (name) { const nm = name.replace('mouse', 'pointer'); return pointerEventNameMap.hasOwnProperty(nm) ? nm : name; }); return { mouse: mouseHandlerNames, touch: touchHandlerNames, pointer: pointerHandlerNames }; })(); const globalNativeListenerNames = { mouse: ['mousemove', 'mouseup'], pointer: ['pointermove', 'pointerup'] }; let wheelEventSupported = false; // Although firfox has 'DOMMouseScroll' event and do not has 'mousewheel' event, // the 'DOMMouseScroll' event do not performe the same behavior on touch pad device // (like on Mac) ('DOMMouseScroll' will be triggered only if a big wheel delta). // So we should not use it. // function eventNameFix(name: string) { // return (name === 'mousewheel' && env.browser.firefox) ? 'DOMMouseScroll' : name; // } function isPointerFromTouch(event: ZRRawEvent) { const pointerType = (event as any).pointerType; return pointerType === 'pen' || pointerType === 'touch'; } // function useMSGuesture(handlerProxy, event) { // return isPointerFromTouch(event) && !!handlerProxy._msGesture; // } // function onMSGestureChange(proxy, event) { // if (event.translationX || event.translationY) { // // mousemove is carried by MSGesture to reduce the sensitivity. // proxy.handler.dispatchToElement(event.target, 'mousemove', event); // } // if (event.scale !== 1) { // event.pinchX = event.offsetX; // event.pinchY = event.offsetY; // event.pinchScale = event.scale; // proxy.handler.dispatchToElement(event.target, 'pinch', event); // } // } /** * Prevent mouse event from being dispatched after Touch Events action * @see <https://github.com/deltakosh/handjs/blob/master/src/hand.base.js> * 1. Mobile browsers dispatch mouse events 300ms after touchend. * 2. Chrome for Android dispatch mousedown for long-touch about 650ms * Result: Blocking Mouse Events for 700ms. * * @param {DOMHandlerScope} scope */ function setTouchTimer(scope: DOMHandlerScope) { scope.touching = true; if (scope.touchTimer != null) { clearTimeout(scope.touchTimer); scope.touchTimer = null; } scope.touchTimer = setTimeout(function () { scope.touching = false; scope.touchTimer = null; }, 700); } // Mark touch, which is useful in distinguish touch and // mouse event in upper applicatoin. function markTouch(event: ZRRawEvent) { event && (event.zrByTouch = true); } // function markTriggeredFromLocal(event) { // event && (event.__zrIsFromLocal = true); // } // function isTriggeredFromLocal(instance, event) { // return !!(event && event.__zrIsFromLocal); // } function normalizeGlobalEvent(instance: HandlerDomProxy, event: ZRRawEvent) { // offsetX, offsetY still need to be calculated. They are necessary in the event // handlers of the upper applications. Set `true` to force calculate them. return normalizeEvent( instance.dom, // TODO ANY TYPE new FakeGlobalEvent(instance, event) as any as ZRRawEvent, true ); } /** * Detect whether the given el is in `painterRoot`. */ function isLocalEl(instance: HandlerDomProxy, el: Node) { let elTmp = el; let isLocal = false; while (elTmp && elTmp.nodeType !== 9 && !( isLocal = (elTmp as DomExtended).domBelongToZr || (elTmp !== el && elTmp === instance.painterRoot) ) ) { elTmp = elTmp.parentNode; } return isLocal; } /** * Make a fake event but not change the original event, * because the global event probably be used by other * listeners not belonging to zrender. * @class */ class FakeGlobalEvent { type: string target: HTMLElement currentTarget: HTMLElement pointerType: string clientX: number clientY: number constructor(instance: HandlerDomProxy, event: ZRRawEvent) { this.type = event.type; this.target = this.currentTarget = instance.dom; this.pointerType = (event as any).pointerType; // Necessray for the force calculation of zrX, zrY this.clientX = (event as ZRRawMouseEvent).clientX; this.clientY = (event as ZRRawMouseEvent).clientY; // Because we do not mount global listeners to touch events, // we do not copy `targetTouches` and `changedTouches` here. } // we make the default methods on the event do nothing, // otherwise it is dangerous. See more details in // [DRAG_OUTSIDE] in `Handler.js`. stopPropagation = zrUtil.noop stopImmediatePropagation = zrUtil.noop preventDefault = zrUtil.noop } /** * Local DOM Handlers * @this {HandlerProxy} */ const localDOMHandlers: DomHandlersMap = { mousedown(event: ZRRawEvent) { event = normalizeEvent(this.dom, event); this.__mayPointerCapture = [event.zrX, event.zrY]; this.trigger('mousedown', event); }, mousemove(event: ZRRawEvent) { event = normalizeEvent(this.dom, event); const downPoint = this.__mayPointerCapture; if (downPoint && (event.zrX !== downPoint[0] || event.zrY !== downPoint[1])) { this.__togglePointerCapture(true); } this.trigger('mousemove', event); }, mouseup(event: ZRRawEvent) { event = normalizeEvent(this.dom, event); this.__togglePointerCapture(false); this.trigger('mouseup', event); }, mouseout(event: ZRRawEvent) { event = normalizeEvent(this.dom, event); // There might be some doms created by upper layer application // at the same level of painter.getViewportRoot() (e.g., tooltip // dom created by echarts), where 'globalout' event should not // be triggered when mouse enters these doms. (But 'mouseout' // should be triggered at the original hovered element as usual). const element = (event as any).toElement || (event as ZRRawMouseEvent).relatedTarget; // For SVG rendering, there are SVG elements inside `this.dom`. // (especially in decal case). Should not to handle those "mouseout".. if (!isLocalEl(this, element)) { // Similarly to the browser did on `document` and touch event, // `globalout` will be delayed to final pointer cature release. if (this.__pointerCapturing) { event.zrEventControl = 'no_globalout'; } this.trigger('mouseout', event); } }, wheel(event: ZRRawEvent) { // Morden agent has supported event `wheel` instead of `mousewheel`. // About the polyfill of the props "delta", see "arc/core/event.ts". // Firefox only support `wheel` rather than `mousewheel`. Although firfox has been supporting // event `DOMMouseScroll`, it do not act the same behavior as `wheel` on touch pad device // like on Mac, where `DOMMouseScroll` will be triggered only if a big wheel delta occurs, // and it results in no chance to "preventDefault". So we should not use `DOMMouseScroll`. wheelEventSupported = true; event = normalizeEvent(this.dom, event); // Follow the definition of the previous version, the zrender event name is still 'mousewheel'. this.trigger('mousewheel', event); }, mousewheel(event: ZRRawEvent) { // IE8- and some other lagacy agent do not support event `wheel`, so we still listen // to the legacy event `mouseevent`. // Typically if event `wheel` is supported and the handler has been mounted on a // DOM element, the legacy `mousewheel` event will not be triggered (Chrome and Safari). // But we still do this guard to avoid to duplicated handle. if (wheelEventSupported) { return; } event = normalizeEvent(this.dom, event); this.trigger('mousewheel', event); }, touchstart(event: ZRRawEvent) { // Default mouse behaviour should not be disabled here. // For example, page may needs to be slided. event = normalizeEvent(this.dom, event); markTouch(event); this.__lastTouchMoment = new Date(); this.handler.processGesture(event, 'start'); // For consistent event listener for both touch device and mouse device, // we simulate "mouseover-->mousedown" in touch device. So we trigger // `mousemove` here (to trigger `mouseover` inside), and then trigger // `mousedown`. localDOMHandlers.mousemove.call(this, event); localDOMHandlers.mousedown.call(this, event); }, touchmove(event: ZRRawEvent) { event = normalizeEvent(this.dom, event); markTouch(event); this.handler.processGesture(event, 'change'); // Mouse move should always be triggered no matter whether // there is gestrue event, because mouse move and pinch may // be used at the same time. localDOMHandlers.mousemove.call(this, event); }, touchend(event: ZRRawEvent) { event = normalizeEvent(this.dom, event); markTouch(event); this.handler.processGesture(event, 'end'); localDOMHandlers.mouseup.call(this, event); // Do not trigger `mouseout` here, in spite of `mousemove`(`mouseover`) is // triggered in `touchstart`. This seems to be illogical, but by this mechanism, // we can conveniently implement "hover style" in both PC and touch device just // by listening to `mouseover` to add "hover style" and listening to `mouseout` // to remove "hover style" on an element, without any additional code for // compatibility. (`mouseout` will not be triggered in `touchend`, so "hover // style" will remain for user view) // click event should always be triggered no matter whether // there is gestrue event. System click can not be prevented. if (+new Date() - (+this.__lastTouchMoment) < TOUCH_CLICK_DELAY) { localDOMHandlers.click.call(this, event); } }, pointerdown(event: ZRRawEvent) { localDOMHandlers.mousedown.call(this, event); // if (useMSGuesture(this, event)) { // this._msGesture.addPointer(event.pointerId); // } }, pointermove(event: ZRRawEvent) { // FIXME // pointermove is so sensitive that it always triggered when // tap(click) on touch screen, which affect some judgement in // upper application. So, we don't support mousemove on MS touch // device yet. if (!isPointerFromTouch(event)) { localDOMHandlers.mousemove.call(this, event); } }, pointerup(event: ZRRawEvent) { localDOMHandlers.mouseup.call(this, event); }, pointerout(event: ZRRawEvent) { // pointerout will be triggered when tap on touch screen // (IE11+/Edge on MS Surface) after click event triggered, // which is inconsistent with the mousout behavior we defined // in touchend. So we unify them. // (check localDOMHandlers.touchend for detailed explanation) if (!isPointerFromTouch(event)) { localDOMHandlers.mouseout.call(this, event); } } }; /** * Othere DOM UI Event handlers for zr dom. * @this {HandlerProxy} */ zrUtil.each(['click', 'dblclick', 'contextmenu'], function (name) { localDOMHandlers[name] = function (event) { event = normalizeEvent(this.dom, event); this.trigger(name, event); }; }); /** * DOM UI Event handlers for global page. * * [Caution]: * those handlers should both support in capture phase and bubble phase! */ const globalDOMHandlers: DomHandlersMap = { pointermove: function (event: ZRRawEvent) { // FIXME // pointermove is so sensitive that it always triggered when // tap(click) on touch screen, which affect some judgement in // upper application. So, we don't support mousemove on MS touch // device yet. if (!isPointerFromTouch(event)) { globalDOMHandlers.mousemove.call(this, event); } }, pointerup: function (event: ZRRawEvent) { globalDOMHandlers.mouseup.call(this, event); }, mousemove: function (event: ZRRawEvent) { this.trigger('mousemove', event); }, mouseup: function (event: ZRRawEvent) { const pointerCaptureReleasing = this.__pointerCapturing; this.__togglePointerCapture(false); this.trigger('mouseup', event); if (pointerCaptureReleasing) { event.zrEventControl = 'only_globalout'; this.trigger('mouseout', event); } } }; function mountLocalDOMEventListeners(instance: HandlerDomProxy, scope: DOMHandlerScope) { const domHandlers = scope.domHandlers; if (env.pointerEventsSupported) { // Only IE11+/Edge // 1. On devices that both enable touch and mouse (e.g., MS Surface and lenovo X240), // IE11+/Edge do not trigger touch event, but trigger pointer event and mouse event // at the same time. // 2. On MS Surface, it probablely only trigger mousedown but no mouseup when tap on // screen, which do not occurs in pointer event. // So we use pointer event to both detect touch gesture and mouse behavior. zrUtil.each(localNativeListenerNames.pointer, function (nativeEventName) { mountSingleDOMEventListener(scope, nativeEventName, function (event) { // markTriggeredFromLocal(event); domHandlers[nativeEventName].call(instance, event); }); }); // FIXME // Note: MS Gesture require CSS touch-action set. But touch-action is not reliable, // which does not prevent defuault behavior occasionally (which may cause view port // zoomed in but use can not zoom it back). And event.preventDefault() does not work. // So we have to not to use MSGesture and not to support touchmove and pinch on MS // touch screen. And we only support click behavior on MS touch screen now. // MS Gesture Event is only supported on IE11+/Edge and on Windows 8+. // We don't support touch on IE on win7. // See <https://msdn.microsoft.com/en-us/library/dn433243(v=vs.85).aspx> // if (typeof MSGesture === 'function') { // (this._msGesture = new MSGesture()).target = dom; // jshint ignore:line // dom.addEventListener('MSGestureChange', onMSGestureChange); // } } else { if (env.touchEventsSupported) { zrUtil.each(localNativeListenerNames.touch, function (nativeEventName) { mountSingleDOMEventListener(scope, nativeEventName, function (event) { // markTriggeredFromLocal(event); domHandlers[nativeEventName].call(instance, event); setTouchTimer(scope); }); }); // Handler of 'mouseout' event is needed in touch mode, which will be mounted below. // addEventListener(root, 'mouseout', this._mouseoutHandler); } // 1. Considering some devices that both enable touch and mouse event (like on MS Surface // and lenovo X240, @see #2350), we make mouse event be always listened, otherwise // mouse event can not be handle in those devices. // 2. On MS Surface, Chrome will trigger both touch event and mouse event. How to prevent // mouseevent after touch event triggered, see `setTouchTimer`. zrUtil.each(localNativeListenerNames.mouse, function (nativeEventName) { mountSingleDOMEventListener(scope, nativeEventName, function (event: ZRRawEvent) { event = getNativeEvent(event); if (!scope.touching) { // markTriggeredFromLocal(event); domHandlers[nativeEventName].call(instance, event); } }); }); } } function mountGlobalDOMEventListeners(instance: HandlerDomProxy, scope: DOMHandlerScope) { // Only IE11+/Edge. See the comment in `mountLocalDOMEventListeners`. if (env.pointerEventsSupported) { zrUtil.each(globalNativeListenerNames.pointer, mount); } // Touch event has implemented "drag outside" so we do not mount global listener for touch event. // (see https://www.w3.org/TR/touch-events/#the-touchmove-event) (see also `DRAG_OUTSIDE`). // We do not consider "both-support-touch-and-mouse device" for this feature (see the comment of // `mountLocalDOMEventListeners`) to avoid bugs util some requirements come. else if (!env.touchEventsSupported) { zrUtil.each(globalNativeListenerNames.mouse, mount); } function mount(nativeEventName: string) { function nativeEventListener(event: ZRRawEvent) { event = getNativeEvent(event); // See the reason in [DRAG_OUTSIDE] in `Handler.js` // This checking supports both `useCapture` or not. // PENDING: if there is performance issue in some devices, // we probably can not use `useCapture` and change a easier // to judes whether local (mark). if (!isLocalEl(instance, event.target as Node)) { event = normalizeGlobalEvent(instance, event); scope.domHandlers[nativeEventName].call(instance, event); } } mountSingleDOMEventListener( scope, nativeEventName, nativeEventListener, {capture: true} // See [DRAG_OUTSIDE] in `Handler.js` ); } } function mountSingleDOMEventListener( scope: DOMHandlerScope, nativeEventName: string, listener: EventListener, opt?: boolean | AddEventListenerOptions ) { scope.mounted[nativeEventName] = listener; scope.listenerOpts[nativeEventName] = opt; addEventListener(scope.domTarget, nativeEventName, listener, opt); } function unmountDOMEventListeners(scope: DOMHandlerScope) { const mounted = scope.mounted; for (let nativeEventName in mounted) { if (mounted.hasOwnProperty(nativeEventName)) { removeEventListener( scope.domTarget, nativeEventName, mounted[nativeEventName], scope.listenerOpts[nativeEventName] ); } } scope.mounted = {}; } class DOMHandlerScope { domTarget: HTMLElement | HTMLDocument domHandlers: DomHandlersMap // Key: eventName, value: mounted handler functions. // Used for unmount. mounted: Dictionary<EventListener> = {}; listenerOpts: Dictionary<boolean | AddEventListenerOptions> = {}; touchTimer: ReturnType<typeof setTimeout>; touching = false; constructor( domTarget: HTMLElement | HTMLDocument, domHandlers: DomHandlersMap ) { this.domTarget = domTarget; this.domHandlers = domHandlers; } } export default class HandlerDomProxy extends Eventful { dom: HTMLElement painterRoot: HTMLElement handler: Handler private _localHandlerScope: DOMHandlerScope private _globalHandlerScope: DOMHandlerScope __lastTouchMoment: Date // See [DRAG_OUTSIDE] in `Handler.ts`. __pointerCapturing = false // [x, y] __mayPointerCapture: VectorArray constructor(dom: HTMLElement, painterRoot: HTMLElement) { super(); this.dom = dom; this.painterRoot = painterRoot; this._localHandlerScope = new DOMHandlerScope(dom, localDOMHandlers); if (globalEventSupported) { this._globalHandlerScope = new DOMHandlerScope(document, globalDOMHandlers); } mountLocalDOMEventListeners(this, this._localHandlerScope); } dispose() { unmountDOMEventListeners(this._localHandlerScope); if (globalEventSupported) { unmountDOMEventListeners(this._globalHandlerScope); } } setCursor(cursorStyle: string) { this.dom.style && (this.dom.style.cursor = cursorStyle || 'default'); } /** * See [DRAG_OUTSIDE] in `Handler.js`. * @implement * @param isPointerCapturing Should never be `null`/`undefined`. * `true`: start to capture pointer if it is not capturing. * `false`: end the capture if it is capturing. */ __togglePointerCapture(isPointerCapturing?: boolean) { this.__mayPointerCapture = null; if (globalEventSupported && ((+this.__pointerCapturing) ^ (+isPointerCapturing)) ) { this.__pointerCapturing = isPointerCapturing; const globalHandlerScope = this._globalHandlerScope; isPointerCapturing ? mountGlobalDOMEventListeners(this, globalHandlerScope) : unmountDOMEventListeners(globalHandlerScope); } } } export interface HandlerProxyInterface extends Eventful { handler: Handler dispose: () => void setCursor: (cursorStyle?: string) => void }