event.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. var Eventful = require("../mixin/Eventful");
  2. exports.Dispatcher = Eventful;
  3. var env = require("./env");
  4. var _fourPointsTransform = require("./fourPointsTransform");
  5. var buildTransformer = _fourPointsTransform.buildTransformer;
  6. /**
  7. * Utilities for mouse or touch events.
  8. */
  9. var isDomLevel2 = typeof window !== 'undefined' && !!window.addEventListener;
  10. var MOUSE_EVENT_REG = /^(?:mouse|pointer|contextmenu|drag|drop)|click/;
  11. var EVENT_SAVED_PROP = '___zrEVENTSAVED';
  12. var _calcOut = [];
  13. /**
  14. * Get the `zrX` and `zrY`, which are relative to the top-left of
  15. * the input `el`.
  16. * CSS transform (2D & 3D) is supported.
  17. *
  18. * The strategy to fetch the coords:
  19. * + If `calculate` is not set as `true`, users of this method should
  20. * ensure that `el` is the same or the same size & location as `e.target`.
  21. * Otherwise the result coords are probably not expected. Because we
  22. * firstly try to get coords from e.offsetX/e.offsetY.
  23. * + If `calculate` is set as `true`, the input `el` can be any element
  24. * and we force to calculate the coords based on `el`.
  25. * + The input `el` should be positionable (not position:static).
  26. *
  27. * The force `calculate` can be used in case like:
  28. * When mousemove event triggered on ec tooltip, `e.target` is not `el`(zr painter.dom).
  29. *
  30. * @param {HTMLElement} el DOM element.
  31. * @param {Event} e Mouse event or touch event.
  32. * @param {Object} out Get `out.zrX` and `out.zrY` as the result.
  33. * @param {boolean} [calculate=false] Whether to force calculate
  34. * the coordinates but not use ones provided by browser.
  35. */
  36. function clientToLocal(el, e, out, calculate) {
  37. out = out || {}; // According to the W3C Working Draft, offsetX and offsetY should be relative
  38. // to the padding edge of the target element. The only browser using this convention
  39. // is IE. Webkit uses the border edge, Opera uses the content edge, and FireFox does
  40. // not support the properties.
  41. // (see http://www.jacklmoore.com/notes/mouse-position/)
  42. // In zr painter.dom, padding edge equals to border edge.
  43. if (calculate || !env.canvasSupported) {
  44. calculateZrXY(el, e, out);
  45. } // Caution: In FireFox, layerX/layerY Mouse position relative to the closest positioned
  46. // ancestor element, so we should make sure el is positioned (e.g., not position:static).
  47. // BTW1, Webkit don't return the same results as FF in non-simple cases (like add
  48. // zoom-factor, overflow / opacity layers, transforms ...)
  49. // BTW2, (ev.offsetY || ev.pageY - $(ev.target).offset().top) is not correct in preserve-3d.
  50. // <https://bugs.jquery.com/ticket/8523#comment:14>
  51. // BTW3, In ff, offsetX/offsetY is always 0.
  52. else if (env.browser.firefox && e.layerX != null && e.layerX !== e.offsetX) {
  53. out.zrX = e.layerX;
  54. out.zrY = e.layerY;
  55. } // For IE6+, chrome, safari, opera. (When will ff support offsetX?)
  56. else if (e.offsetX != null) {
  57. out.zrX = e.offsetX;
  58. out.zrY = e.offsetY;
  59. } // For some other device, e.g., IOS safari.
  60. else {
  61. calculateZrXY(el, e, out);
  62. }
  63. return out;
  64. }
  65. function calculateZrXY(el, e, out) {
  66. // BlackBerry 5, iOS 3 (original iPhone) don't have getBoundingRect.
  67. if (el.getBoundingClientRect && env.domSupported) {
  68. var ex = e.clientX;
  69. var ey = e.clientY;
  70. if (el.nodeName.toUpperCase() === 'CANVAS') {
  71. // Original approach, which do not support CSS transform.
  72. // marker can not be locationed in a canvas container
  73. // (getBoundingClientRect is always 0). We do not support
  74. // that input a pre-created canvas to zr while using css
  75. // transform in iOS.
  76. var box = el.getBoundingClientRect();
  77. out.zrX = ex - box.left;
  78. out.zrY = ey - box.top;
  79. return;
  80. } else {
  81. var saved = el[EVENT_SAVED_PROP] || (el[EVENT_SAVED_PROP] = {});
  82. var transformer = preparePointerTransformer(prepareCoordMarkers(el, saved), saved);
  83. if (transformer) {
  84. transformer(_calcOut, ex, ey);
  85. out.zrX = _calcOut[0];
  86. out.zrY = _calcOut[1];
  87. return;
  88. }
  89. }
  90. }
  91. out.zrX = out.zrY = 0;
  92. }
  93. function prepareCoordMarkers(el, saved) {
  94. var markers = saved.markers;
  95. if (markers) {
  96. return markers;
  97. }
  98. markers = saved.markers = [];
  99. var propLR = ['left', 'right'];
  100. var propTB = ['top', 'bottom'];
  101. for (var i = 0; i < 4; i++) {
  102. var marker = document.createElement('div');
  103. var stl = marker.style;
  104. var idxLR = i % 2;
  105. var idxTB = (i >> 1) % 2;
  106. stl.cssText = ['position:absolute', 'visibility: hidden', 'padding: 0', 'margin: 0', 'border-width: 0', 'width:0', 'height:0', // 'width: 5px',
  107. // 'height: 5px',
  108. propLR[idxLR] + ':0', propTB[idxTB] + ':0', propLR[1 - idxLR] + ':auto', propTB[1 - idxTB] + ':auto', ''].join('!important;');
  109. el.appendChild(marker);
  110. markers.push(marker);
  111. }
  112. return markers;
  113. }
  114. function preparePointerTransformer(markers, saved) {
  115. var transformer = saved.transformer;
  116. var oldSrcCoords = saved.srcCoords;
  117. var useOld = true;
  118. var srcCoords = [];
  119. var destCoords = [];
  120. for (var i = 0; i < 4; i++) {
  121. var rect = markers[i].getBoundingClientRect();
  122. var ii = 2 * i;
  123. var x = rect.left;
  124. var y = rect.top;
  125. srcCoords.push(x, y);
  126. useOld &= oldSrcCoords && x === oldSrcCoords[ii] && y === oldSrcCoords[ii + 1];
  127. destCoords.push(markers[i].offsetLeft, markers[i].offsetTop);
  128. } // Cache to avoid time consuming of `buildTransformer`.
  129. return useOld ? transformer : (saved.srcCoords = srcCoords, saved.transformer = buildTransformer(srcCoords, destCoords));
  130. }
  131. /**
  132. * Find native event compat for legency IE.
  133. * Should be called at the begining of a native event listener.
  134. *
  135. * @param {Event} [e] Mouse event or touch event or pointer event.
  136. * For lagency IE, we use `window.event` is used.
  137. * @return {Event} The native event.
  138. */
  139. function getNativeEvent(e) {
  140. return e || window.event;
  141. }
  142. /**
  143. * Normalize the coordinates of the input event.
  144. *
  145. * Get the `e.zrX` and `e.zrY`, which are relative to the top-left of
  146. * the input `el`.
  147. * Get `e.zrDelta` if using mouse wheel.
  148. * Get `e.which`, see the comment inside this function.
  149. *
  150. * Do not calculate repeatly if `zrX` and `zrY` already exist.
  151. *
  152. * Notice: see comments in `clientToLocal`. check the relationship
  153. * between the result coords and the parameters `el` and `calculate`.
  154. *
  155. * @param {HTMLElement} el DOM element.
  156. * @param {Event} [e] See `getNativeEvent`.
  157. * @param {boolean} [calculate=false] Whether to force calculate
  158. * the coordinates but not use ones provided by browser.
  159. * @return {UIEvent} The normalized native UIEvent.
  160. */
  161. function normalizeEvent(el, e, calculate) {
  162. e = getNativeEvent(e);
  163. if (e.zrX != null) {
  164. return e;
  165. }
  166. var eventType = e.type;
  167. var isTouch = eventType && eventType.indexOf('touch') >= 0;
  168. if (!isTouch) {
  169. clientToLocal(el, e, e, calculate);
  170. e.zrDelta = e.wheelDelta ? e.wheelDelta / 120 : -(e.detail || 0) / 3;
  171. } else {
  172. var touch = eventType !== 'touchend' ? e.targetTouches[0] : e.changedTouches[0];
  173. touch && clientToLocal(el, touch, e, calculate);
  174. } // Add which for click: 1 === left; 2 === middle; 3 === right; otherwise: 0;
  175. // See jQuery: https://github.com/jquery/jquery/blob/master/src/event.js
  176. // If e.which has been defined, it may be readonly,
  177. // see: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/which
  178. var button = e.button;
  179. if (e.which == null && button !== undefined && MOUSE_EVENT_REG.test(e.type)) {
  180. e.which = button & 1 ? 1 : button & 2 ? 3 : button & 4 ? 2 : 0;
  181. } // [Caution]: `e.which` from browser is not always reliable. For example,
  182. // when press left button and `mousemove (pointermove)` in Edge, the `e.which`
  183. // is 65536 and the `e.button` is -1. But the `mouseup (pointerup)` and
  184. // `mousedown (pointerdown)` is the same as Chrome does.
  185. return e;
  186. }
  187. /**
  188. * @param {HTMLElement} el
  189. * @param {string} name
  190. * @param {Function} handler
  191. * @param {Object|boolean} opt If boolean, means `opt.capture`
  192. * @param {boolean} [opt.capture=false]
  193. * @param {boolean} [opt.passive=false]
  194. */
  195. function addEventListener(el, name, handler, opt) {
  196. if (isDomLevel2) {
  197. // Reproduct the console warning:
  198. // [Violation] Added non-passive event listener to a scroll-blocking <some> event.
  199. // Consider marking event handler as 'passive' to make the page more responsive.
  200. // Just set console log level: verbose in chrome dev tool.
  201. // then the warning log will be printed when addEventListener called.
  202. // See https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
  203. // We have not yet found a neat way to using passive. Because in zrender the dom event
  204. // listener delegate all of the upper events of element. Some of those events need
  205. // to prevent default. For example, the feature `preventDefaultMouseMove` of echarts.
  206. // Before passive can be adopted, these issues should be considered:
  207. // (1) Whether and how a zrender user specifies an event listener passive. And by default,
  208. // passive or not.
  209. // (2) How to tread that some zrender event listener is passive, and some is not. If
  210. // we use other way but not preventDefault of mousewheel and touchmove, browser
  211. // compatibility should be handled.
  212. // var opts = (env.passiveSupported && name === 'mousewheel')
  213. // ? {passive: true}
  214. // // By default, the third param of el.addEventListener is `capture: false`.
  215. // : void 0;
  216. // el.addEventListener(name, handler /* , opts */);
  217. el.addEventListener(name, handler, opt);
  218. } else {
  219. // For simplicity, do not implement `setCapture` for IE9-.
  220. el.attachEvent('on' + name, handler);
  221. }
  222. }
  223. /**
  224. * Parameter are the same as `addEventListener`.
  225. *
  226. * Notice that if a listener is registered twice, one with capture and one without,
  227. * remove each one separately. Removal of a capturing listener does not affect a
  228. * non-capturing version of the same listener, and vice versa.
  229. */
  230. function removeEventListener(el, name, handler, opt) {
  231. if (isDomLevel2) {
  232. el.removeEventListener(name, handler, opt);
  233. } else {
  234. el.detachEvent('on' + name, handler);
  235. }
  236. }
  237. /**
  238. * preventDefault and stopPropagation.
  239. * Notice: do not use this method in zrender. It can only be
  240. * used by upper applications if necessary.
  241. *
  242. * @param {Event} e A mouse or touch event.
  243. */
  244. var stop = isDomLevel2 ? function (e) {
  245. e.preventDefault();
  246. e.stopPropagation();
  247. e.cancelBubble = true;
  248. } : function (e) {
  249. e.returnValue = false;
  250. e.cancelBubble = true;
  251. };
  252. /**
  253. * This method only works for mouseup and mousedown. The functionality is restricted
  254. * for fault tolerance, See the `e.which` compatibility above.
  255. *
  256. * @param {MouseEvent} e
  257. * @return {boolean}
  258. */
  259. function isMiddleOrRightButtonOnMouseUpDown(e) {
  260. return e.which === 2 || e.which === 3;
  261. }
  262. /**
  263. * To be removed.
  264. * @deprecated
  265. */
  266. function notLeftMouse(e) {
  267. // If e.which is undefined, considered as left mouse event.
  268. return e.which > 1;
  269. } // For backward compatibility
  270. exports.clientToLocal = clientToLocal;
  271. exports.getNativeEvent = getNativeEvent;
  272. exports.normalizeEvent = normalizeEvent;
  273. exports.addEventListener = addEventListener;
  274. exports.removeEventListener = removeEventListener;
  275. exports.stop = stop;
  276. exports.isMiddleOrRightButtonOnMouseUpDown = isMiddleOrRightButtonOnMouseUpDown;
  277. exports.notLeftMouse = notLeftMouse;