123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- "use strict";
- const { Ci, Cu } = require("chrome");
- const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
- var systemAppOrigin = (function () {
- let systemOrigin = "_";
- try {
- systemOrigin = Services.io.newURI(
- Services.prefs.getCharPref("b2g.system_manifest_url"), null, null)
- .prePath;
- } catch (e) {
- // Fall back to default value
- }
- return systemOrigin;
- })();
- var threshold = Services.prefs.getIntPref("ui.dragThresholdX", 25);
- var delay = Services.prefs.getIntPref("ui.click_hold_context_menus.delay", 500);
- function SimulatorCore(simulatorTarget) {
- this.simulatorTarget = simulatorTarget;
- }
- /**
- * Simulate touch events for platforms where they aren't generally available.
- */
- SimulatorCore.prototype = {
- events: [
- "mousedown",
- "mousemove",
- "mouseup",
- "touchstart",
- "touchend",
- "mouseenter",
- "mouseover",
- "mouseout",
- "mouseleave"
- ],
- contextMenuTimeout: null,
- simulatorTarget: null,
- enabled: false,
- start() {
- if (this.enabled) {
- // Simulator is already started
- return;
- }
- this.events.forEach(evt => {
- // Only listen trusted events to prevent messing with
- // event dispatched manually within content documents
- this.simulatorTarget.addEventListener(evt, this, true, false);
- });
- this.enabled = true;
- },
- stop() {
- if (!this.enabled) {
- // Simulator isn't running
- return;
- }
- this.events.forEach(evt => {
- this.simulatorTarget.removeEventListener(evt, this, true);
- });
- this.enabled = false;
- },
- handleEvent(evt) {
- // The gaia system window use an hybrid system even on the device which is
- // a mix of mouse/touch events. So let's not cancel *all* mouse events
- // if it is the current target.
- let content = this.getContent(evt.target);
- if (!content) {
- return;
- }
- let isSystemWindow = content.location.toString()
- .startsWith(systemAppOrigin);
- // App touchstart & touchend should also be dispatched on the system app
- // to match on-device behavior.
- if (evt.type.startsWith("touch") && !isSystemWindow) {
- let sysFrame = content.realFrameElement;
- if (!sysFrame) {
- return;
- }
- let sysDocument = sysFrame.ownerDocument;
- let sysWindow = sysDocument.defaultView;
- let touchEvent = sysDocument.createEvent("touchevent");
- let touch = evt.touches[0] || evt.changedTouches[0];
- let point = sysDocument.createTouch(sysWindow, sysFrame, 0,
- touch.pageX, touch.pageY,
- touch.screenX, touch.screenY,
- touch.clientX, touch.clientY,
- 1, 1, 0, 0);
- let touches = sysDocument.createTouchList(point);
- let targetTouches = touches;
- let changedTouches = touches;
- touchEvent.initTouchEvent(evt.type, true, true, sysWindow, 0,
- false, false, false, false,
- touches, targetTouches, changedTouches);
- sysFrame.dispatchEvent(touchEvent);
- return;
- }
- // Ignore all but real mouse event coming from physical mouse
- // (especially ignore mouse event being dispatched from a touch event)
- if (evt.button ||
- evt.mozInputSource != Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE ||
- evt.isSynthesized) {
- return;
- }
- let eventTarget = this.target;
- let type = "";
- switch (evt.type) {
- case "mouseenter":
- case "mouseover":
- case "mouseout":
- case "mouseleave":
- // Don't propagate events which are not related to touch events
- evt.stopPropagation();
- break;
- case "mousedown":
- this.target = evt.target;
- this.contextMenuTimeout = this.sendContextMenu(evt);
- this.cancelClick = false;
- this.startX = evt.pageX;
- this.startY = evt.pageY;
- // Capture events so if a different window show up the events
- // won't be dispatched to something else.
- evt.target.setCapture(false);
- type = "touchstart";
- break;
- case "mousemove":
- if (!eventTarget) {
- // Don't propagate mousemove event when touchstart event isn't fired
- evt.stopPropagation();
- return;
- }
- if (!this.cancelClick) {
- if (Math.abs(this.startX - evt.pageX) > threshold ||
- Math.abs(this.startY - evt.pageY) > threshold) {
- this.cancelClick = true;
- content.clearTimeout(this.contextMenuTimeout);
- }
- }
- type = "touchmove";
- break;
- case "mouseup":
- if (!eventTarget) {
- return;
- }
- this.target = null;
- content.clearTimeout(this.contextMenuTimeout);
- type = "touchend";
- // Only register click listener after mouseup to ensure
- // catching only real user click. (Especially ignore click
- // being dispatched on form submit)
- if (evt.detail == 1) {
- this.simulatorTarget.addEventListener("click", this, true, false);
- }
- break;
- case "click":
- // Mouse events has been cancelled so dispatch a sequence
- // of events to where touchend has been fired
- evt.preventDefault();
- evt.stopImmediatePropagation();
- this.simulatorTarget.removeEventListener("click", this, true, false);
- if (this.cancelClick) {
- return;
- }
- content.setTimeout(function dispatchMouseEvents(self) {
- try {
- self.fireMouseEvent("mousedown", evt);
- self.fireMouseEvent("mousemove", evt);
- self.fireMouseEvent("mouseup", evt);
- } catch (e) {
- console.error("Exception in touch event helper: " + e);
- }
- }, this.getDelayBeforeMouseEvent(evt), this);
- return;
- }
- let target = eventTarget || this.target;
- if (target && type) {
- this.sendTouchEvent(evt, target, type);
- }
- if (!isSystemWindow) {
- evt.preventDefault();
- evt.stopImmediatePropagation();
- }
- },
- fireMouseEvent(type, evt) {
- let content = this.getContent(evt.target);
- let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils);
- utils.sendMouseEvent(type, evt.clientX, evt.clientY, 0, 1, 0, true, 0,
- Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
- },
- sendContextMenu({ target, clientX, clientY, screenX, screenY }) {
- let view = target.ownerDocument.defaultView;
- let { MouseEvent } = view;
- let evt = new MouseEvent("contextmenu", {
- bubbles: true,
- cancelable: true,
- view,
- screenX,
- screenY,
- clientX,
- clientY,
- });
- let content = this.getContent(target);
- let timeout = content.setTimeout((function contextMenu() {
- target.dispatchEvent(evt);
- this.cancelClick = true;
- }).bind(this), delay);
- return timeout;
- },
- sendTouchEvent(evt, target, name) {
- function clone(obj) {
- return Cu.cloneInto(obj, target);
- }
- // When running OOP b2g desktop, we need to send the touch events
- // using the mozbrowser api on the unwrapped frame.
- if (target.localName == "iframe" && target.mozbrowser === true) {
- if (name == "touchstart") {
- this.touchstartTime = Date.now();
- } else if (name == "touchend") {
- // If we have a "fast" tap, don't send a click as both will be turned
- // into a click and that breaks eg. checkboxes.
- if (Date.now() - this.touchstartTime < delay) {
- this.cancelClick = true;
- }
- }
- let unwrapped = XPCNativeWrapper.unwrap(target);
- unwrapped.sendTouchEvent(name, clone([0]), // event type, id
- clone([evt.clientX]), // x
- clone([evt.clientY]), // y
- clone([1]), clone([1]), // rx, ry
- clone([0]), clone([0]), // rotation, force
- 1); // count
- return;
- }
- let document = target.ownerDocument;
- let content = this.getContent(target);
- if (!content) {
- return;
- }
- let touchEvent = document.createEvent("touchevent");
- let point = document.createTouch(content, target, 0,
- evt.pageX, evt.pageY,
- evt.screenX, evt.screenY,
- evt.clientX, evt.clientY,
- 1, 1, 0, 0);
- let touches = document.createTouchList(point);
- let targetTouches = touches;
- let changedTouches = touches;
- if (name === "touchend" || name === "touchcancel") {
- // "touchend" and "touchcancel" events should not have the removed touch
- // neither in touches nor in targetTouches
- touches = targetTouches = document.createTouchList();
- }
- touchEvent.initTouchEvent(name, true, true, content, 0,
- false, false, false, false,
- touches, targetTouches, changedTouches);
- target.dispatchEvent(touchEvent);
- },
- getContent(target) {
- let win = (target && target.ownerDocument)
- ? target.ownerDocument.defaultView
- : null;
- return win;
- },
- getDelayBeforeMouseEvent(evt) {
- // On mobile platforms, Firefox inserts a 300ms delay between
- // touch events and accompanying mouse events, except if the
- // content window is not zoomable and the content window is
- // auto-zoomed to device-width.
- // If the preference dom.meta-viewport.enabled is set to false,
- // we couldn't read viewport's information from getViewportInfo().
- // So we always simulate 300ms delay when the
- // dom.meta-viewport.enabled is false.
- let savedMetaViewportEnabled =
- Services.prefs.getBoolPref("dom.meta-viewport.enabled");
- if (!savedMetaViewportEnabled) {
- return 300;
- }
- let content = this.getContent(evt.target);
- if (!content) {
- return 0;
- }
- let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDOMWindowUtils);
- let allowZoom = {},
- minZoom = {},
- maxZoom = {},
- autoSize = {};
- utils.getViewportInfo(content.innerWidth, content.innerHeight, {},
- allowZoom, minZoom, maxZoom, {}, {}, autoSize);
- // FIXME: On Safari and Chrome mobile platform, if the css property
- // touch-action set to none or manipulation would also suppress 300ms
- // delay. But Firefox didn't support this property now, we can't get
- // this value from utils.getVisitedDependentComputedStyle() to check
- // if we should suppress 300ms delay.
- if (!allowZoom.value || // user-scalable = no
- minZoom.value === maxZoom.value || // minimum-scale = maximum-scale
- autoSize.value // width = device-width
- ) {
- return 0;
- } else {
- return 300;
- }
- }
- };
- exports.SimulatorCore = SimulatorCore;
|