var _event = require("../core/event"); var addEventListener = _event.addEventListener; var removeEventListener = _event.removeEventListener; var normalizeEvent = _event.normalizeEvent; var getNativeEvent = _event.getNativeEvent; var zrUtil = require("../core/util"); var Eventful = require("../mixin/Eventful"); var env = require("../core/env"); /* global document */ var TOUCH_CLICK_DELAY = 300; var globalEventSupported = env.domSupported; var localNativeListenerNames = function () { var mouseHandlerNames = ['click', 'dblclick', 'mousewheel', 'mouseout', 'mouseup', 'mousedown', 'mousemove', 'contextmenu']; var touchHandlerNames = ['touchstart', 'touchend', 'touchmove']; var pointerEventNameMap = { pointerdown: 1, pointerup: 1, pointermove: 1, pointerout: 1 }; var pointerHandlerNames = zrUtil.map(mouseHandlerNames, function (name) { var nm = name.replace('mouse', 'pointer'); return pointerEventNameMap.hasOwnProperty(nm) ? nm : name; }); return { mouse: mouseHandlerNames, touch: touchHandlerNames, pointer: pointerHandlerNames }; }(); var globalNativeListenerNames = { mouse: ['mousemove', 'mouseup'], pointer: ['pointermove', 'pointerup'] }; function eventNameFix(name) { return name === 'mousewheel' && env.browser.firefox ? 'DOMMouseScroll' : name; } function isPointerFromTouch(event) { var pointerType = event.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 * 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) { 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) { event && (event.zrByTouch = true); } // function markTriggeredFromLocal(event) { // event && (event.__zrIsFromLocal = true); // } // function isTriggeredFromLocal(instance, event) { // return !!(event && event.__zrIsFromLocal); // } function normalizeGlobalEvent(instance, event) { // 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, new FakeGlobalEvent(instance, event), true); } /** * Detect whether the given el is in `painterRoot`. */ function isLocalEl(instance, el) { var elTmp = el; var isLocal = false; while (elTmp && elTmp.nodeType !== 9 && !(isLocal = elTmp.domBelongToZr || elTmp !== el && elTmp === instance.painterRoot)) { elTmp = elTmp.parentNode; } return isLocal; } /** * Make a fake event but not change the original event, * becuase the global event probably be used by other * listeners not belonging to zrender. * @class */ function FakeGlobalEvent(instance, event) { this.type = event.type; this.target = this.currentTarget = instance.dom; this.pointerType = event.pointerType; // Necessray for the force calculation of zrX, zrY this.clientX = event.clientX; this.clientY = event.clientY; // Because we do not mount global listeners to touch events, // we do not copy `targetTouches` and `changedTouches` here. } var fakeGlobalEventProto = FakeGlobalEvent.prototype; // we make the default methods on the event do nothing, // otherwise it is dangerous. See more details in // [Drag outside] in `Handler.js`. fakeGlobalEventProto.stopPropagation = fakeGlobalEventProto.stopImmediatePropagation = fakeGlobalEventProto.preventDefault = zrUtil.noop; /** * Local DOM Handlers * @this {HandlerProxy} */ var localDOMHandlers = { mousedown: function (event) { event = normalizeEvent(this.dom, event); this._mayPointerCapture = [event.zrX, event.zrY]; this.trigger('mousedown', event); }, mousemove: function (event) { event = normalizeEvent(this.dom, event); var downPoint = this._mayPointerCapture; if (downPoint && (event.zrX !== downPoint[0] || event.zrY !== downPoint[1])) { togglePointerCapture(this, true); } this.trigger('mousemove', event); }, mouseup: function (event) { event = normalizeEvent(this.dom, event); togglePointerCapture(this, false); this.trigger('mouseup', event); }, mouseout: function (event) { event = normalizeEvent(this.dom, event); // 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'; } // 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). var element = event.toElement || event.relatedTarget; event.zrIsToLocalDOM = isLocalEl(this, element); this.trigger('mouseout', event); }, touchstart: function (event) { // 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: function (event) { 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: function (event) { 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: function (event) { localDOMHandlers.mousedown.call(this, event); // if (useMSGuesture(this, event)) { // this._msGesture.addPointer(event.pointerId); // } }, pointermove: function (event) { // FIXME // pointermove is so sensitive that it always triggered when // tap(click) on touch screen, which affect some judgement in // upper application. So, we dont support mousemove on MS touch // device yet. if (!isPointerFromTouch(event)) { localDOMHandlers.mousemove.call(this, event); } }, pointerup: function (event) { localDOMHandlers.mouseup.call(this, event); }, pointerout: function (event) { // 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', 'mousewheel', '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! * * @this {HandlerProxy} */ var globalDOMHandlers = { pointermove: function (event) { // FIXME // pointermove is so sensitive that it always triggered when // tap(click) on touch screen, which affect some judgement in // upper application. So, we dont support mousemove on MS touch // device yet. if (!isPointerFromTouch(event)) { globalDOMHandlers.mousemove.call(this, event); } }, pointerup: function (event) { globalDOMHandlers.mouseup.call(this, event); }, mousemove: function (event) { this.trigger('mousemove', event); }, mouseup: function (event) { var pointerCaptureReleasing = this._pointerCapturing; togglePointerCapture(this, false); this.trigger('mouseup', event); if (pointerCaptureReleasing) { event.zrEventControl = 'only_globalout'; this.trigger('mouseout', event); } } }; /** * @param {HandlerProxy} instance * @param {DOMHandlerScope} scope */ function mountLocalDOMEventListeners(instance, scope) { var 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 dont support touch on IE on win7. // See // 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) { event = getNativeEvent(event); if (!scope.touching) { // markTriggeredFromLocal(event); domHandlers[nativeEventName].call(instance, event); } }); }); } } /** * @param {HandlerProxy} instance * @param {DOMHandlerScope} scope */ function mountGlobalDOMEventListeners(instance, scope) { // 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) // 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) { function nativeEventListener(event) { 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)) { 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, nativeEventName, listener, opt) { scope.mounted[nativeEventName] = listener; scope.listenerOpts[nativeEventName] = opt; addEventListener(scope.domTarget, eventNameFix(nativeEventName), listener, opt); } function unmountDOMEventListeners(scope) { var mounted = scope.mounted; for (var nativeEventName in mounted) { if (mounted.hasOwnProperty(nativeEventName)) { removeEventListener(scope.domTarget, eventNameFix(nativeEventName), mounted[nativeEventName], scope.listenerOpts[nativeEventName]); } } scope.mounted = {}; } /** * See [Drag Outside] in `Handler.js`. * @implement * @param {boolean} isPointerCapturing Should never be `null`/`undefined`. * `true`: start to capture pointer if it is not capturing. * `false`: end the capture if it is capturing. */ function togglePointerCapture(instance, isPointerCapturing) { instance._mayPointerCapture = null; if (globalEventSupported && instance._pointerCapturing ^ isPointerCapturing) { instance._pointerCapturing = isPointerCapturing; var globalHandlerScope = instance._globalHandlerScope; isPointerCapturing ? mountGlobalDOMEventListeners(instance, globalHandlerScope) : unmountDOMEventListeners(globalHandlerScope); } } /** * @inner * @class */ function DOMHandlerScope(domTarget, domHandlers) { this.domTarget = domTarget; this.domHandlers = domHandlers; // Key: eventName, value: mounted handler funcitons. // Used for unmount. this.mounted = {}; this.listenerOpts = {}; this.touchTimer = null; this.touching = false; } /** * @public * @class */ function HandlerDomProxy(dom, painterRoot) { Eventful.call(this); this.dom = dom; this.painterRoot = painterRoot; this._localHandlerScope = new DOMHandlerScope(dom, localDOMHandlers); if (globalEventSupported) { this._globalHandlerScope = new DOMHandlerScope(document, globalDOMHandlers); } /** * @type {boolean} */ this._pointerCapturing = false; /** * @type {Array.} [x, y] or null. */ this._mayPointerCapture = null; mountLocalDOMEventListeners(this, this._localHandlerScope); } var handlerDomProxyProto = HandlerDomProxy.prototype; handlerDomProxyProto.dispose = function () { unmountDOMEventListeners(this._localHandlerScope); if (globalEventSupported) { unmountDOMEventListeners(this._globalHandlerScope); } }; handlerDomProxyProto.setCursor = function (cursorStyle) { this.dom.style && (this.dom.style.cursor = cursorStyle || 'default'); }; zrUtil.mixin(HandlerDomProxy, Eventful); var _default = HandlerDomProxy; module.exports = _default;