touch.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. define(["./_base/kernel", "./aspect", "./dom", "./dom-class", "./_base/lang", "./on", "./has", "./mouse", "./domReady", "./_base/window"],
  2. function(dojo, aspect, dom, domClass, lang, on, has, mouse, domReady, win){
  3. // module:
  4. // dojo/touch
  5. var ios4 = has("ios") < 5;
  6. // Detect if platform supports Pointer Events, and if so, the names of the events (pointerdown vs. MSPointerDown).
  7. var hasPointer = has("pointer-events") || has("MSPointer"),
  8. pointer = (function () {
  9. var pointer = {};
  10. for (var type in { down: 1, move: 1, up: 1, cancel: 1, over: 1, out: 1 }) {
  11. pointer[type] = has("MSPointer") ?
  12. "MSPointer" + type.charAt(0).toUpperCase() + type.slice(1) :
  13. "pointer" + type;
  14. }
  15. return pointer;
  16. })();
  17. // Detect if platform supports the webkit touchstart/touchend/... events
  18. var hasTouch = has("touch-events");
  19. // Click generation variables
  20. var clicksInited, clickTracker, useTarget = false, clickTarget, clickX, clickY, clickDx, clickDy, clickTime;
  21. // Time of most recent touchstart, touchmove, or touchend event
  22. var lastTouch;
  23. function dualEvent(mouseType, touchType, pointerType){
  24. // Returns synthetic event that listens for both the specified mouse event and specified touch event.
  25. // But ignore fake mouse events that were generated due to the user touching the screen.
  26. if(hasPointer && pointerType){
  27. // IE10+: MSPointer* events are designed to handle both mouse and touch in a uniform way,
  28. // so just use that regardless of hasTouch.
  29. return function(node, listener){
  30. return on(node, pointerType, listener);
  31. }
  32. }else if(hasTouch){
  33. return function(node, listener){
  34. var handle1 = on(node, touchType, function(evt){
  35. listener.call(this, evt);
  36. // On slow mobile browsers (see https://bugs.dojotoolkit.org/ticket/17634),
  37. // a handler for a touch event may take >1s to run. That time shouldn't
  38. // be included in the calculation for lastTouch.
  39. lastTouch = (new Date()).getTime();
  40. }),
  41. handle2 = on(node, mouseType, function(evt){
  42. if(!lastTouch || (new Date()).getTime() > lastTouch + 1000){
  43. listener.call(this, evt);
  44. }
  45. });
  46. return {
  47. remove: function(){
  48. handle1.remove();
  49. handle2.remove();
  50. }
  51. };
  52. };
  53. }else{
  54. // Avoid creating listeners for touch events on performance sensitive older browsers like IE6
  55. return function(node, listener){
  56. return on(node, mouseType, listener);
  57. }
  58. }
  59. }
  60. function marked(/*DOMNode*/ node){
  61. // Search for node ancestor has been marked with the dojoClick property to indicate special processing.
  62. // Returns marked ancestor.
  63. do{
  64. if(node.dojoClick !== undefined){ return node; }
  65. }while(node = node.parentNode);
  66. }
  67. function doClicks(e, moveType, endType){
  68. // summary:
  69. // Setup touch listeners to generate synthetic clicks immediately (rather than waiting for the browser
  70. // to generate clicks after the double-tap delay) and consistently (regardless of whether event.preventDefault()
  71. // was called in an event listener. Synthetic clicks are generated only if a node or one of its ancestors has
  72. // its dojoClick property set to truthy. If a node receives synthetic clicks because one of its ancestors has its
  73. // dojoClick property set to truthy, you can disable synthetic clicks on this node by setting its own dojoClick property
  74. // to falsy.
  75. var markedNode = marked(e.target);
  76. clickTracker = !e.target.disabled && markedNode && markedNode.dojoClick; // click threshold = true, number, x/y object, or "useTarget"
  77. if(clickTracker){
  78. useTarget = (clickTracker == "useTarget");
  79. clickTarget = (useTarget?markedNode:e.target);
  80. if(useTarget){
  81. // We expect a click, so prevent any other
  82. // default action on "touchpress"
  83. e.preventDefault();
  84. }
  85. clickX = e.changedTouches ? e.changedTouches[0].pageX - win.global.pageXOffset : e.clientX;
  86. clickY = e.changedTouches ? e.changedTouches[0].pageY - win.global.pageYOffset : e.clientY;
  87. clickDx = (typeof clickTracker == "object" ? clickTracker.x : (typeof clickTracker == "number" ? clickTracker : 0)) || 4;
  88. clickDy = (typeof clickTracker == "object" ? clickTracker.y : (typeof clickTracker == "number" ? clickTracker : 0)) || 4;
  89. // add move/end handlers only the first time a node with dojoClick is seen,
  90. // so we don't add too much overhead when dojoClick is never set.
  91. if(!clicksInited){
  92. clicksInited = true;
  93. function updateClickTracker(e){
  94. if(useTarget){
  95. clickTracker = dom.isDescendant(
  96. win.doc.elementFromPoint(
  97. (e.changedTouches ? e.changedTouches[0].pageX - win.global.pageXOffset : e.clientX),
  98. (e.changedTouches ? e.changedTouches[0].pageY - win.global.pageYOffset : e.clientY)),
  99. clickTarget);
  100. }else{
  101. clickTracker = clickTracker &&
  102. (e.changedTouches ? e.changedTouches[0].target : e.target) == clickTarget &&
  103. Math.abs((e.changedTouches ? e.changedTouches[0].pageX - win.global.pageXOffset : e.clientX) - clickX) <= clickDx &&
  104. Math.abs((e.changedTouches ? e.changedTouches[0].pageY - win.global.pageYOffset : e.clientY) - clickY) <= clickDy;
  105. }
  106. }
  107. win.doc.addEventListener(moveType, function(e){
  108. updateClickTracker(e);
  109. if(useTarget){
  110. // prevent native scroll event and ensure touchend is
  111. // fire after touch moves between press and release.
  112. e.preventDefault();
  113. }
  114. }, true);
  115. win.doc.addEventListener(endType, function(e){
  116. updateClickTracker(e);
  117. if(clickTracker){
  118. clickTime = (new Date()).getTime();
  119. var target = (useTarget?clickTarget:e.target);
  120. if(target.tagName === "LABEL"){
  121. // when clicking on a label, forward click to its associated input if any
  122. target = dom.byId(target.getAttribute("for")) || target;
  123. }
  124. //some attributes can be on the Touch object, not on the Event:
  125. //http://www.w3.org/TR/touch-events/#touch-interface
  126. var src = (e.changedTouches) ? e.changedTouches[0] : e;
  127. //create the synthetic event.
  128. //http://www.w3.org/TR/DOM-Level-3-Events/#widl-MouseEvent-initMouseEvent
  129. var clickEvt = document.createEvent("MouseEvents");
  130. clickEvt._dojo_click = true;
  131. clickEvt.initMouseEvent("click",
  132. true, //bubbles
  133. true, //cancelable
  134. e.view,
  135. e.detail,
  136. src.screenX,
  137. src.screenY,
  138. src.clientX,
  139. src.clientY,
  140. e.ctrlKey,
  141. e.altKey,
  142. e.shiftKey,
  143. e.metaKey,
  144. 0, //button
  145. null //related target
  146. );
  147. setTimeout(function(){
  148. on.emit(target, "click", clickEvt);
  149. // refresh clickTime in case app-defined click handler took a long time to run
  150. clickTime = (new Date()).getTime();
  151. }, 0);
  152. }
  153. }, true);
  154. function stopNativeEvents(type){
  155. win.doc.addEventListener(type, function(e){
  156. // Stop native events when we emitted our own click event. Note that the native click may occur
  157. // on a different node than the synthetic click event was generated on. For example,
  158. // click on a menu item, causing the menu to disappear, and then (~300ms later) the browser
  159. // sends a click event to the node that was *underneath* the menu. So stop all native events
  160. // sent shortly after ours, similar to what is done in dualEvent.
  161. // The INPUT.dijitOffScreen test is for offscreen inputs used in dijit/form/Button, on which
  162. // we call click() explicitly, we don't want to stop this event.
  163. if(!e._dojo_click &&
  164. (new Date()).getTime() <= clickTime + 1000 &&
  165. !(e.target.tagName == "INPUT" && domClass.contains(e.target, "dijitOffScreen"))){
  166. e.stopPropagation();
  167. e.stopImmediatePropagation && e.stopImmediatePropagation();
  168. if(type == "click" && (e.target.tagName != "INPUT" || e.target.type == "radio" || e.target.type == "checkbox")
  169. && e.target.tagName != "TEXTAREA" && e.target.tagName != "AUDIO" && e.target.tagName != "VIDEO"){
  170. // preventDefault() breaks textual <input>s on android, keyboard doesn't popup,
  171. // but it is still needed for checkboxes and radio buttons, otherwise in some cases
  172. // the checked state becomes inconsistent with the widget's state
  173. e.preventDefault();
  174. }
  175. }
  176. }, true);
  177. }
  178. stopNativeEvents("click");
  179. // We also stop mousedown/up since these would be sent well after with our "fast" click (300ms),
  180. // which can confuse some dijit widgets.
  181. stopNativeEvents("mousedown");
  182. stopNativeEvents("mouseup");
  183. }
  184. }
  185. }
  186. var hoveredNode;
  187. if(hasPointer){
  188. // MSPointer (IE10+) already has support for over and out, so we just need to init click support
  189. domReady(function(){
  190. win.doc.addEventListener(pointer.down, function(evt){
  191. doClicks(evt, pointer.move, pointer.up);
  192. }, true);
  193. });
  194. }else if(hasTouch){
  195. domReady(function(){
  196. // Keep track of currently hovered node
  197. hoveredNode = win.body(); // currently hovered node
  198. win.doc.addEventListener("touchstart", function(evt){
  199. lastTouch = (new Date()).getTime();
  200. // Precede touchstart event with touch.over event. DnD depends on this.
  201. // Use addEventListener(cb, true) to run cb before any touchstart handlers on node run,
  202. // and to ensure this code runs even if the listener on the node does event.stop().
  203. var oldNode = hoveredNode;
  204. hoveredNode = evt.target;
  205. on.emit(oldNode, "dojotouchout", {
  206. relatedTarget: hoveredNode,
  207. bubbles: true
  208. });
  209. on.emit(hoveredNode, "dojotouchover", {
  210. relatedTarget: oldNode,
  211. bubbles: true
  212. });
  213. doClicks(evt, "touchmove", "touchend"); // init click generation
  214. }, true);
  215. function copyEventProps(evt){
  216. // Make copy of event object and also set bubbles:true. Used when calling on.emit().
  217. var props = lang.delegate(evt, {
  218. bubbles: true
  219. });
  220. if(has("ios") >= 6){
  221. // On iOS6 "touches" became a non-enumerable property, which
  222. // is not hit by for...in. Ditto for the other properties below.
  223. props.touches = evt.touches;
  224. props.altKey = evt.altKey;
  225. props.changedTouches = evt.changedTouches;
  226. props.ctrlKey = evt.ctrlKey;
  227. props.metaKey = evt.metaKey;
  228. props.shiftKey = evt.shiftKey;
  229. props.targetTouches = evt.targetTouches;
  230. }
  231. return props;
  232. }
  233. on(win.doc, "touchmove", function(evt){
  234. lastTouch = (new Date()).getTime();
  235. var newNode = win.doc.elementFromPoint(
  236. evt.pageX - (ios4 ? 0 : win.global.pageXOffset), // iOS 4 expects page coords
  237. evt.pageY - (ios4 ? 0 : win.global.pageYOffset)
  238. );
  239. if(newNode){
  240. // Fire synthetic touchover and touchout events on nodes since the browser won't do it natively.
  241. if(hoveredNode !== newNode){
  242. // touch out on the old node
  243. on.emit(hoveredNode, "dojotouchout", {
  244. relatedTarget: newNode,
  245. bubbles: true
  246. });
  247. // touchover on the new node
  248. on.emit(newNode, "dojotouchover", {
  249. relatedTarget: hoveredNode,
  250. bubbles: true
  251. });
  252. hoveredNode = newNode;
  253. }
  254. // Unlike a listener on "touchmove", on(node, "dojotouchmove", listener) fires when the finger
  255. // drags over the specified node, regardless of which node the touch started on.
  256. if(!on.emit(newNode, "dojotouchmove", copyEventProps(evt))){
  257. // emit returns false when synthetic event "dojotouchmove" is cancelled, so we prevent the
  258. // default behavior of the underlying native event "touchmove".
  259. evt.preventDefault();
  260. }
  261. }
  262. });
  263. // Fire a dojotouchend event on the node where the finger was before it was removed from the screen.
  264. // This is different than the native touchend, which fires on the node where the drag started.
  265. on(win.doc, "touchend", function(evt){
  266. lastTouch = (new Date()).getTime();
  267. var node = win.doc.elementFromPoint(
  268. evt.pageX - (ios4 ? 0 : win.global.pageXOffset), // iOS 4 expects page coords
  269. evt.pageY - (ios4 ? 0 : win.global.pageYOffset)
  270. ) || win.body(); // if out of the screen
  271. on.emit(node, "dojotouchend", copyEventProps(evt));
  272. });
  273. });
  274. }
  275. //device neutral events - touch.press|move|release|cancel/over/out
  276. var touch = {
  277. press: dualEvent("mousedown", "touchstart", pointer.down),
  278. move: dualEvent("mousemove", "dojotouchmove", pointer.move),
  279. release: dualEvent("mouseup", "dojotouchend", pointer.up),
  280. cancel: dualEvent(mouse.leave, "touchcancel", hasPointer ? pointer.cancel : null),
  281. over: dualEvent("mouseover", "dojotouchover", pointer.over),
  282. out: dualEvent("mouseout", "dojotouchout", pointer.out),
  283. enter: mouse._eventHandler(dualEvent("mouseover","dojotouchover", pointer.over)),
  284. leave: mouse._eventHandler(dualEvent("mouseout", "dojotouchout", pointer.out))
  285. };
  286. /*=====
  287. touch = {
  288. // summary:
  289. // This module provides unified touch event handlers by exporting
  290. // press, move, release and cancel which can also run well on desktop.
  291. // Based on http://dvcs.w3.org/hg/webevents/raw-file/tip/touchevents.html
  292. // Also, if the dojoClick property is set to truthy on a DOM node, dojo/touch generates
  293. // click events immediately for this node and its descendants (except for descendants that
  294. // have a dojoClick property set to falsy), to avoid the delay before native browser click events,
  295. // and regardless of whether evt.preventDefault() was called in a touch.press event listener.
  296. //
  297. // example:
  298. // Used with dojo/on
  299. // | define(["dojo/on", "dojo/touch"], function(on, touch){
  300. // | on(node, touch.press, function(e){});
  301. // | on(node, touch.move, function(e){});
  302. // | on(node, touch.release, function(e){});
  303. // | on(node, touch.cancel, function(e){});
  304. // example:
  305. // Used with touch.* directly
  306. // | touch.press(node, function(e){});
  307. // | touch.move(node, function(e){});
  308. // | touch.release(node, function(e){});
  309. // | touch.cancel(node, function(e){});
  310. // example:
  311. // Have dojo/touch generate clicks without delay, with a default move threshold of 4 pixels
  312. // | node.dojoClick = true;
  313. // example:
  314. // Have dojo/touch generate clicks without delay, with a move threshold of 10 pixels horizontally and vertically
  315. // | node.dojoClick = 10;
  316. // example:
  317. // Have dojo/touch generate clicks without delay, with a move threshold of 50 pixels horizontally and 10 pixels vertically
  318. // | node.dojoClick = {x:50, y:5};
  319. // example:
  320. // Disable clicks without delay generated by dojo/touch on a node that has an ancestor with property dojoClick set to truthy
  321. // | node.dojoClick = false;
  322. press: function(node, listener){
  323. // summary:
  324. // Register a listener to 'touchstart'|'mousedown' for the given node
  325. // node: Dom
  326. // Target node to listen to
  327. // listener: Function
  328. // Callback function
  329. // returns:
  330. // A handle which will be used to remove the listener by handle.remove()
  331. },
  332. move: function(node, listener){
  333. // summary:
  334. // Register a listener that fires when the mouse cursor or a finger is dragged over the given node.
  335. // node: Dom
  336. // Target node to listen to
  337. // listener: Function
  338. // Callback function
  339. // returns:
  340. // A handle which will be used to remove the listener by handle.remove()
  341. },
  342. release: function(node, listener){
  343. // summary:
  344. // Register a listener to releasing the mouse button while the cursor is over the given node
  345. // (i.e. "mouseup") or for removing the finger from the screen while touching the given node.
  346. // node: Dom
  347. // Target node to listen to
  348. // listener: Function
  349. // Callback function
  350. // returns:
  351. // A handle which will be used to remove the listener by handle.remove()
  352. },
  353. cancel: function(node, listener){
  354. // summary:
  355. // Register a listener to 'touchcancel'|'mouseleave' for the given node
  356. // node: Dom
  357. // Target node to listen to
  358. // listener: Function
  359. // Callback function
  360. // returns:
  361. // A handle which will be used to remove the listener by handle.remove()
  362. },
  363. over: function(node, listener){
  364. // summary:
  365. // Register a listener to 'mouseover' or touch equivalent for the given node
  366. // node: Dom
  367. // Target node to listen to
  368. // listener: Function
  369. // Callback function
  370. // returns:
  371. // A handle which will be used to remove the listener by handle.remove()
  372. },
  373. out: function(node, listener){
  374. // summary:
  375. // Register a listener to 'mouseout' or touch equivalent for the given node
  376. // node: Dom
  377. // Target node to listen to
  378. // listener: Function
  379. // Callback function
  380. // returns:
  381. // A handle which will be used to remove the listener by handle.remove()
  382. },
  383. enter: function(node, listener){
  384. // summary:
  385. // Register a listener to mouse.enter or touch equivalent for the given node
  386. // node: Dom
  387. // Target node to listen to
  388. // listener: Function
  389. // Callback function
  390. // returns:
  391. // A handle which will be used to remove the listener by handle.remove()
  392. },
  393. leave: function(node, listener){
  394. // summary:
  395. // Register a listener to mouse.leave or touch equivalent for the given node
  396. // node: Dom
  397. // Target node to listen to
  398. // listener: Function
  399. // Callback function
  400. // returns:
  401. // A handle which will be used to remove the listener by handle.remove()
  402. }
  403. };
  404. =====*/
  405. has("extend-dojo") && (dojo.touch = touch);
  406. return touch;
  407. });