simulator-core.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const { Ci, Cu } = require("chrome");
  6. const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
  7. var systemAppOrigin = (function () {
  8. let systemOrigin = "_";
  9. try {
  10. systemOrigin = Services.io.newURI(
  11. Services.prefs.getCharPref("b2g.system_manifest_url"), null, null)
  12. .prePath;
  13. } catch (e) {
  14. // Fall back to default value
  15. }
  16. return systemOrigin;
  17. })();
  18. var threshold = Services.prefs.getIntPref("ui.dragThresholdX", 25);
  19. var delay = Services.prefs.getIntPref("ui.click_hold_context_menus.delay", 500);
  20. function SimulatorCore(simulatorTarget) {
  21. this.simulatorTarget = simulatorTarget;
  22. }
  23. /**
  24. * Simulate touch events for platforms where they aren't generally available.
  25. */
  26. SimulatorCore.prototype = {
  27. events: [
  28. "mousedown",
  29. "mousemove",
  30. "mouseup",
  31. "touchstart",
  32. "touchend",
  33. "mouseenter",
  34. "mouseover",
  35. "mouseout",
  36. "mouseleave"
  37. ],
  38. contextMenuTimeout: null,
  39. simulatorTarget: null,
  40. enabled: false,
  41. start() {
  42. if (this.enabled) {
  43. // Simulator is already started
  44. return;
  45. }
  46. this.events.forEach(evt => {
  47. // Only listen trusted events to prevent messing with
  48. // event dispatched manually within content documents
  49. this.simulatorTarget.addEventListener(evt, this, true, false);
  50. });
  51. this.enabled = true;
  52. },
  53. stop() {
  54. if (!this.enabled) {
  55. // Simulator isn't running
  56. return;
  57. }
  58. this.events.forEach(evt => {
  59. this.simulatorTarget.removeEventListener(evt, this, true);
  60. });
  61. this.enabled = false;
  62. },
  63. handleEvent(evt) {
  64. // The gaia system window use an hybrid system even on the device which is
  65. // a mix of mouse/touch events. So let's not cancel *all* mouse events
  66. // if it is the current target.
  67. let content = this.getContent(evt.target);
  68. if (!content) {
  69. return;
  70. }
  71. let isSystemWindow = content.location.toString()
  72. .startsWith(systemAppOrigin);
  73. // App touchstart & touchend should also be dispatched on the system app
  74. // to match on-device behavior.
  75. if (evt.type.startsWith("touch") && !isSystemWindow) {
  76. let sysFrame = content.realFrameElement;
  77. if (!sysFrame) {
  78. return;
  79. }
  80. let sysDocument = sysFrame.ownerDocument;
  81. let sysWindow = sysDocument.defaultView;
  82. let touchEvent = sysDocument.createEvent("touchevent");
  83. let touch = evt.touches[0] || evt.changedTouches[0];
  84. let point = sysDocument.createTouch(sysWindow, sysFrame, 0,
  85. touch.pageX, touch.pageY,
  86. touch.screenX, touch.screenY,
  87. touch.clientX, touch.clientY,
  88. 1, 1, 0, 0);
  89. let touches = sysDocument.createTouchList(point);
  90. let targetTouches = touches;
  91. let changedTouches = touches;
  92. touchEvent.initTouchEvent(evt.type, true, true, sysWindow, 0,
  93. false, false, false, false,
  94. touches, targetTouches, changedTouches);
  95. sysFrame.dispatchEvent(touchEvent);
  96. return;
  97. }
  98. // Ignore all but real mouse event coming from physical mouse
  99. // (especially ignore mouse event being dispatched from a touch event)
  100. if (evt.button ||
  101. evt.mozInputSource != Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE ||
  102. evt.isSynthesized) {
  103. return;
  104. }
  105. let eventTarget = this.target;
  106. let type = "";
  107. switch (evt.type) {
  108. case "mouseenter":
  109. case "mouseover":
  110. case "mouseout":
  111. case "mouseleave":
  112. // Don't propagate events which are not related to touch events
  113. evt.stopPropagation();
  114. break;
  115. case "mousedown":
  116. this.target = evt.target;
  117. this.contextMenuTimeout = this.sendContextMenu(evt);
  118. this.cancelClick = false;
  119. this.startX = evt.pageX;
  120. this.startY = evt.pageY;
  121. // Capture events so if a different window show up the events
  122. // won't be dispatched to something else.
  123. evt.target.setCapture(false);
  124. type = "touchstart";
  125. break;
  126. case "mousemove":
  127. if (!eventTarget) {
  128. // Don't propagate mousemove event when touchstart event isn't fired
  129. evt.stopPropagation();
  130. return;
  131. }
  132. if (!this.cancelClick) {
  133. if (Math.abs(this.startX - evt.pageX) > threshold ||
  134. Math.abs(this.startY - evt.pageY) > threshold) {
  135. this.cancelClick = true;
  136. content.clearTimeout(this.contextMenuTimeout);
  137. }
  138. }
  139. type = "touchmove";
  140. break;
  141. case "mouseup":
  142. if (!eventTarget) {
  143. return;
  144. }
  145. this.target = null;
  146. content.clearTimeout(this.contextMenuTimeout);
  147. type = "touchend";
  148. // Only register click listener after mouseup to ensure
  149. // catching only real user click. (Especially ignore click
  150. // being dispatched on form submit)
  151. if (evt.detail == 1) {
  152. this.simulatorTarget.addEventListener("click", this, true, false);
  153. }
  154. break;
  155. case "click":
  156. // Mouse events has been cancelled so dispatch a sequence
  157. // of events to where touchend has been fired
  158. evt.preventDefault();
  159. evt.stopImmediatePropagation();
  160. this.simulatorTarget.removeEventListener("click", this, true, false);
  161. if (this.cancelClick) {
  162. return;
  163. }
  164. content.setTimeout(function dispatchMouseEvents(self) {
  165. try {
  166. self.fireMouseEvent("mousedown", evt);
  167. self.fireMouseEvent("mousemove", evt);
  168. self.fireMouseEvent("mouseup", evt);
  169. } catch (e) {
  170. console.error("Exception in touch event helper: " + e);
  171. }
  172. }, this.getDelayBeforeMouseEvent(evt), this);
  173. return;
  174. }
  175. let target = eventTarget || this.target;
  176. if (target && type) {
  177. this.sendTouchEvent(evt, target, type);
  178. }
  179. if (!isSystemWindow) {
  180. evt.preventDefault();
  181. evt.stopImmediatePropagation();
  182. }
  183. },
  184. fireMouseEvent(type, evt) {
  185. let content = this.getContent(evt.target);
  186. let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
  187. .getInterface(Ci.nsIDOMWindowUtils);
  188. utils.sendMouseEvent(type, evt.clientX, evt.clientY, 0, 1, 0, true, 0,
  189. Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
  190. },
  191. sendContextMenu({ target, clientX, clientY, screenX, screenY }) {
  192. let view = target.ownerDocument.defaultView;
  193. let { MouseEvent } = view;
  194. let evt = new MouseEvent("contextmenu", {
  195. bubbles: true,
  196. cancelable: true,
  197. view,
  198. screenX,
  199. screenY,
  200. clientX,
  201. clientY,
  202. });
  203. let content = this.getContent(target);
  204. let timeout = content.setTimeout((function contextMenu() {
  205. target.dispatchEvent(evt);
  206. this.cancelClick = true;
  207. }).bind(this), delay);
  208. return timeout;
  209. },
  210. sendTouchEvent(evt, target, name) {
  211. function clone(obj) {
  212. return Cu.cloneInto(obj, target);
  213. }
  214. // When running OOP b2g desktop, we need to send the touch events
  215. // using the mozbrowser api on the unwrapped frame.
  216. if (target.localName == "iframe" && target.mozbrowser === true) {
  217. if (name == "touchstart") {
  218. this.touchstartTime = Date.now();
  219. } else if (name == "touchend") {
  220. // If we have a "fast" tap, don't send a click as both will be turned
  221. // into a click and that breaks eg. checkboxes.
  222. if (Date.now() - this.touchstartTime < delay) {
  223. this.cancelClick = true;
  224. }
  225. }
  226. let unwrapped = XPCNativeWrapper.unwrap(target);
  227. unwrapped.sendTouchEvent(name, clone([0]), // event type, id
  228. clone([evt.clientX]), // x
  229. clone([evt.clientY]), // y
  230. clone([1]), clone([1]), // rx, ry
  231. clone([0]), clone([0]), // rotation, force
  232. 1); // count
  233. return;
  234. }
  235. let document = target.ownerDocument;
  236. let content = this.getContent(target);
  237. if (!content) {
  238. return;
  239. }
  240. let touchEvent = document.createEvent("touchevent");
  241. let point = document.createTouch(content, target, 0,
  242. evt.pageX, evt.pageY,
  243. evt.screenX, evt.screenY,
  244. evt.clientX, evt.clientY,
  245. 1, 1, 0, 0);
  246. let touches = document.createTouchList(point);
  247. let targetTouches = touches;
  248. let changedTouches = touches;
  249. if (name === "touchend" || name === "touchcancel") {
  250. // "touchend" and "touchcancel" events should not have the removed touch
  251. // neither in touches nor in targetTouches
  252. touches = targetTouches = document.createTouchList();
  253. }
  254. touchEvent.initTouchEvent(name, true, true, content, 0,
  255. false, false, false, false,
  256. touches, targetTouches, changedTouches);
  257. target.dispatchEvent(touchEvent);
  258. },
  259. getContent(target) {
  260. let win = (target && target.ownerDocument)
  261. ? target.ownerDocument.defaultView
  262. : null;
  263. return win;
  264. },
  265. getDelayBeforeMouseEvent(evt) {
  266. // On mobile platforms, Firefox inserts a 300ms delay between
  267. // touch events and accompanying mouse events, except if the
  268. // content window is not zoomable and the content window is
  269. // auto-zoomed to device-width.
  270. // If the preference dom.meta-viewport.enabled is set to false,
  271. // we couldn't read viewport's information from getViewportInfo().
  272. // So we always simulate 300ms delay when the
  273. // dom.meta-viewport.enabled is false.
  274. let savedMetaViewportEnabled =
  275. Services.prefs.getBoolPref("dom.meta-viewport.enabled");
  276. if (!savedMetaViewportEnabled) {
  277. return 300;
  278. }
  279. let content = this.getContent(evt.target);
  280. if (!content) {
  281. return 0;
  282. }
  283. let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
  284. .getInterface(Ci.nsIDOMWindowUtils);
  285. let allowZoom = {},
  286. minZoom = {},
  287. maxZoom = {},
  288. autoSize = {};
  289. utils.getViewportInfo(content.innerWidth, content.innerHeight, {},
  290. allowZoom, minZoom, maxZoom, {}, {}, autoSize);
  291. // FIXME: On Safari and Chrome mobile platform, if the css property
  292. // touch-action set to none or manipulation would also suppress 300ms
  293. // delay. But Firefox didn't support this property now, we can't get
  294. // this value from utils.getVisitedDependentComputedStyle() to check
  295. // if we should suppress 300ms delay.
  296. if (!allowZoom.value || // user-scalable = no
  297. minZoom.value === maxZoom.value || // minimum-scale = maximum-scale
  298. autoSize.value // width = device-width
  299. ) {
  300. return 0;
  301. } else {
  302. return 300;
  303. }
  304. }
  305. };
  306. exports.SimulatorCore = SimulatorCore;