123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634 |
- /* 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
- }
|