TouchPointer.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /* *
  2. *
  3. * (c) 2010-2019 Torstein Honsi
  4. *
  5. * License: www.highcharts.com/license
  6. *
  7. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  8. *
  9. * */
  10. 'use strict';
  11. import H from './Globals.js';
  12. import U from './Utilities.js';
  13. var extend = U.extend, pick = U.pick;
  14. var charts = H.charts, noop = H.noop, Pointer = H.Pointer;
  15. /* eslint-disable no-invalid-this, valid-jsdoc */
  16. // Support for touch devices
  17. extend(Pointer.prototype, /** @lends Pointer.prototype */ {
  18. /**
  19. * Run translation operations
  20. *
  21. * @private
  22. * @function Highcharts.Pointer#pinchTranslate
  23. *
  24. * @param {Array<*>} pinchDown
  25. *
  26. * @param {Array<*>} touches
  27. *
  28. * @param {*} transform
  29. *
  30. * @param {*} selectionMarker
  31. *
  32. * @param {*} clip
  33. *
  34. * @param {*} lastValidTouch
  35. *
  36. * @return {void}
  37. */
  38. pinchTranslate: function (pinchDown, touches, transform, selectionMarker, clip, lastValidTouch) {
  39. if (this.zoomHor) {
  40. this.pinchTranslateDirection(true, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
  41. }
  42. if (this.zoomVert) {
  43. this.pinchTranslateDirection(false, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
  44. }
  45. },
  46. /**
  47. * Run translation operations for each direction (horizontal and vertical)
  48. * independently.
  49. *
  50. * @private
  51. * @function Highcharts.Pointer#pinchTranslateDirection
  52. *
  53. * @param {boolean} horiz
  54. *
  55. * @param {Array<*>} pinchDown
  56. *
  57. * @param {Array<*>} touches
  58. *
  59. * @param {*} transform
  60. *
  61. * @param {*} selectionMarker
  62. *
  63. * @param {*} clip
  64. *
  65. * @param {*} lastValidTouch
  66. *
  67. * @param {number|undefined} [forcedScale=1]
  68. *
  69. * @return {void}
  70. */
  71. pinchTranslateDirection: function (horiz, pinchDown, touches, transform, selectionMarker, clip, lastValidTouch, forcedScale) {
  72. var chart = this.chart, xy = horiz ? 'x' : 'y', XY = horiz ? 'X' : 'Y', sChartXY = 'chart' + XY, wh = horiz ? 'width' : 'height', plotLeftTop = chart['plot' + (horiz ? 'Left' : 'Top')], selectionWH, selectionXY, clipXY, scale = forcedScale || 1, inverted = chart.inverted, bounds = chart.bounds[horiz ? 'h' : 'v'], singleTouch = pinchDown.length === 1, touch0Start = pinchDown[0][sChartXY], touch0Now = touches[0][sChartXY], touch1Start = !singleTouch && pinchDown[1][sChartXY], touch1Now = !singleTouch && touches[1][sChartXY], outOfBounds, transformScale, scaleKey, setScale = function () {
  73. // Don't zoom if fingers are too close on this axis
  74. if (!singleTouch && Math.abs(touch0Start - touch1Start) > 20) {
  75. scale = forcedScale ||
  76. Math.abs(touch0Now - touch1Now) /
  77. Math.abs(touch0Start - touch1Start);
  78. }
  79. clipXY = ((plotLeftTop - touch0Now) / scale) + touch0Start;
  80. selectionWH = chart['plot' + (horiz ? 'Width' : 'Height')] / scale;
  81. };
  82. // Set the scale, first pass
  83. setScale();
  84. // The clip position (x or y) is altered if out of bounds, the selection
  85. // position is not
  86. selectionXY = clipXY;
  87. // Out of bounds
  88. if (selectionXY < bounds.min) {
  89. selectionXY = bounds.min;
  90. outOfBounds = true;
  91. }
  92. else if (selectionXY + selectionWH > bounds.max) {
  93. selectionXY = bounds.max - selectionWH;
  94. outOfBounds = true;
  95. }
  96. // Is the chart dragged off its bounds, determined by dataMin and
  97. // dataMax?
  98. if (outOfBounds) {
  99. // Modify the touchNow position in order to create an elastic drag
  100. // movement. This indicates to the user that the chart is responsive
  101. // but can't be dragged further.
  102. touch0Now -= 0.8 * (touch0Now - lastValidTouch[xy][0]);
  103. if (!singleTouch) {
  104. touch1Now -= 0.8 * (touch1Now - lastValidTouch[xy][1]);
  105. }
  106. // Set the scale, second pass to adapt to the modified touchNow
  107. // positions
  108. setScale();
  109. }
  110. else {
  111. lastValidTouch[xy] = [touch0Now, touch1Now];
  112. }
  113. // Set geometry for clipping, selection and transformation
  114. if (!inverted) {
  115. clip[xy] = clipXY - plotLeftTop;
  116. clip[wh] = selectionWH;
  117. }
  118. scaleKey = inverted ? (horiz ? 'scaleY' : 'scaleX') : 'scale' + XY;
  119. transformScale = inverted ? 1 / scale : scale;
  120. selectionMarker[wh] = selectionWH;
  121. selectionMarker[xy] = selectionXY;
  122. transform[scaleKey] = scale;
  123. transform['translate' + XY] = (transformScale * plotLeftTop) +
  124. (touch0Now - (transformScale * touch0Start));
  125. },
  126. /**
  127. * Handle touch events with two touches
  128. *
  129. * @private
  130. * @function Highcharts.Pointer#pinch
  131. *
  132. * @param {Highcharts.PointerEventObject} e
  133. *
  134. * @return {void}
  135. */
  136. pinch: function (e) {
  137. var self = this, chart = self.chart, pinchDown = self.pinchDown, touches = e.touches, touchesLength = touches.length, lastValidTouch = self.lastValidTouch, hasZoom = self.hasZoom, selectionMarker = self.selectionMarker, transform = {}, fireClickEvent = touchesLength === 1 && ((self.inClass(e.target, 'highcharts-tracker') &&
  138. chart.runTrackerClick) ||
  139. self.runChartClick), clip = {};
  140. // Don't initiate panning until the user has pinched. This prevents us
  141. // from blocking page scrolling as users scroll down a long page
  142. // (#4210).
  143. if (touchesLength > 1) {
  144. self.initiated = true;
  145. }
  146. // On touch devices, only proceed to trigger click if a handler is
  147. // defined
  148. if (hasZoom && self.initiated && !fireClickEvent) {
  149. e.preventDefault();
  150. }
  151. // Normalize each touch
  152. [].map.call(touches, function (e) {
  153. return self.normalize(e);
  154. });
  155. // Register the touch start position
  156. if (e.type === 'touchstart') {
  157. [].forEach.call(touches, function (e, i) {
  158. pinchDown[i] = { chartX: e.chartX, chartY: e.chartY };
  159. });
  160. lastValidTouch.x = [pinchDown[0].chartX, pinchDown[1] &&
  161. pinchDown[1].chartX];
  162. lastValidTouch.y = [pinchDown[0].chartY, pinchDown[1] &&
  163. pinchDown[1].chartY];
  164. // Identify the data bounds in pixels
  165. chart.axes.forEach(function (axis) {
  166. if (axis.zoomEnabled) {
  167. var bounds = chart.bounds[axis.horiz ? 'h' : 'v'], minPixelPadding = axis.minPixelPadding, min = axis.toPixels(Math.min(pick(axis.options.min, axis.dataMin), axis.dataMin)), max = axis.toPixels(Math.max(pick(axis.options.max, axis.dataMax), axis.dataMax)), absMin = Math.min(min, max), absMax = Math.max(min, max);
  168. // Store the bounds for use in the touchmove handler
  169. bounds.min = Math.min(axis.pos, absMin - minPixelPadding);
  170. bounds.max = Math.max(axis.pos + axis.len, absMax + minPixelPadding);
  171. }
  172. });
  173. self.res = true; // reset on next move
  174. // Optionally move the tooltip on touchmove
  175. }
  176. else if (self.followTouchMove && touchesLength === 1) {
  177. this.runPointActions(self.normalize(e));
  178. // Event type is touchmove, handle panning and pinching
  179. }
  180. else if (pinchDown.length) { // can be 0 when releasing, if touchend
  181. // fires first
  182. // Set the marker
  183. if (!selectionMarker) {
  184. self.selectionMarker = selectionMarker = extend({
  185. destroy: noop,
  186. touch: true
  187. }, chart.plotBox);
  188. }
  189. self.pinchTranslate(pinchDown, touches, transform, selectionMarker, clip, lastValidTouch);
  190. self.hasPinched = hasZoom;
  191. // Scale and translate the groups to provide visual feedback during
  192. // pinching
  193. self.scaleGroups(transform, clip);
  194. if (self.res) {
  195. self.res = false;
  196. this.reset(false, 0);
  197. }
  198. }
  199. },
  200. /**
  201. * General touch handler shared by touchstart and touchmove.
  202. *
  203. * @private
  204. * @function Highcharts.Pointer#touch
  205. *
  206. * @param {Highcharts.PointerEventObject} e
  207. *
  208. * @param {boolean} [start]
  209. *
  210. * @return {void}
  211. */
  212. touch: function (e, start) {
  213. var chart = this.chart, hasMoved, pinchDown, isInside;
  214. if (chart.index !== H.hoverChartIndex) {
  215. this.onContainerMouseLeave({ relatedTarget: true });
  216. }
  217. H.hoverChartIndex = chart.index;
  218. if (e.touches.length === 1) {
  219. e = this.normalize(e);
  220. isInside = chart.isInsidePlot(e.chartX - chart.plotLeft, e.chartY - chart.plotTop);
  221. if (isInside && !chart.openMenu) {
  222. // Run mouse events and display tooltip etc
  223. if (start) {
  224. this.runPointActions(e);
  225. }
  226. // Android fires touchmove events after the touchstart even if
  227. // the finger hasn't moved, or moved only a pixel or two. In iOS
  228. // however, the touchmove doesn't fire unless the finger moves
  229. // more than ~4px. So we emulate this behaviour in Android by
  230. // checking how much it moved, and cancelling on small
  231. // distances. #3450.
  232. if (e.type === 'touchmove') {
  233. pinchDown = this.pinchDown;
  234. hasMoved = pinchDown[0] ? Math.sqrt(// #5266
  235. Math.pow(pinchDown[0].chartX - e.chartX, 2) +
  236. Math.pow(pinchDown[0].chartY - e.chartY, 2)) >= 4 : false;
  237. }
  238. if (pick(hasMoved, true)) {
  239. this.pinch(e);
  240. }
  241. }
  242. else if (start) {
  243. // Hide the tooltip on touching outside the plot area (#1203)
  244. this.reset();
  245. }
  246. }
  247. else if (e.touches.length === 2) {
  248. this.pinch(e);
  249. }
  250. },
  251. /**
  252. * @private
  253. * @function Highcharts.Pointer#onContainerTouchStart
  254. *
  255. * @param {Highcharts.PointerEventObject} e
  256. *
  257. * @return {void}
  258. */
  259. onContainerTouchStart: function (e) {
  260. this.zoomOption(e);
  261. this.touch(e, true);
  262. },
  263. /**
  264. * @private
  265. * @function Highcharts.Pointer#onContainerTouchMove
  266. *
  267. * @param {Highcharts.PointerEventObject} e
  268. *
  269. * @return {void}
  270. */
  271. onContainerTouchMove: function (e) {
  272. this.touch(e);
  273. },
  274. /**
  275. * @private
  276. * @function Highcharts.Pointer#onDocumentTouchEnd
  277. *
  278. * @param {Highcharts.PointerEventObject} e
  279. *
  280. * @return {void}
  281. */
  282. onDocumentTouchEnd: function (e) {
  283. if (charts[H.hoverChartIndex]) {
  284. charts[H.hoverChartIndex].pointer.drop(e);
  285. }
  286. }
  287. });