touch-simulator.js (6890B)
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 5 "use strict"; 6 7 loader.lazyRequireGetter( 8 this, 9 "PICKER_TYPES", 10 "resource://devtools/shared/picker-constants.js" 11 ); 12 13 var isClickHoldEnabled = Services.prefs.getBoolPref( 14 "ui.click_hold_context_menus" 15 ); 16 var clickHoldDelay = Services.prefs.getIntPref( 17 "ui.click_hold_context_menus.delay", 18 500 19 ); 20 21 const EVENTS_TO_HANDLE = [ 22 "mousedown", 23 "mousemove", 24 "mouseup", 25 "mouseenter", 26 "mouseover", 27 "mouseout", 28 "mouseleave", 29 ]; 30 31 const kStateHover = 0x00000004; // ElementState::HOVER 32 33 /** 34 * Simulate touch events for platforms where they aren't generally available. 35 */ 36 class TouchSimulator { 37 /** 38 * @param {WindowGlobalTargetActor} windowTarget: The window object we'll use 39 * to listen for click and touch events to handle. 40 */ 41 constructor(windowTarget) { 42 this.windowTarget = windowTarget; 43 this.simulatorTarget = windowTarget.chromeEventHandler; 44 this._currentPickerMap = new Map(); 45 this.previousScreenY = 0; 46 } 47 48 enabled = false; 49 50 start() { 51 if (this.enabled) { 52 // Simulator is already started 53 return; 54 } 55 56 EVENTS_TO_HANDLE.forEach(evt => { 57 // Only listen trusted events to prevent messing with 58 // event dispatched manually within content documents 59 this.simulatorTarget.addEventListener(evt, this, true, false); 60 }); 61 62 this.enabled = true; 63 } 64 65 stop() { 66 if (!this.enabled) { 67 // Simulator isn't running 68 return; 69 } 70 EVENTS_TO_HANDLE.forEach(evt => { 71 this.simulatorTarget.removeEventListener(evt, this, true); 72 }); 73 this.enabled = false; 74 } 75 76 _isPicking() { 77 const types = Object.values(PICKER_TYPES); 78 return types.some(type => this._currentPickerMap.get(type)); 79 } 80 81 /** 82 * Set the state value for one of DevTools pickers (either eyedropper or 83 * element picker). 84 * If any content picker is currently active, we should not be emulating 85 * touch events. Otherwise it is ok to emulate touch events. 86 * In theory only one picker can ever be active at a time, but tracking the 87 * different pickers independantly avoids race issues in the client code. 88 * 89 * @param {boolean} state 90 * True if the picker is currently active, false otherwise. 91 * @param {string} pickerType 92 * One of PICKER_TYPES. 93 */ 94 setElementPickerState(state, pickerType) { 95 if (!Object.values(PICKER_TYPES).includes(pickerType)) { 96 throw new Error( 97 "Unsupported type in setElementPickerState: " + pickerType 98 ); 99 } 100 this._currentPickerMap.set(pickerType, state); 101 } 102 103 // eslint-disable-next-line complexity 104 handleEvent(evt) { 105 // Bail out if devtools is in pick mode in the same tab. 106 if (this._isPicking()) { 107 return; 108 } 109 110 const content = this.getContent(evt.target); 111 if (!content) { 112 return; 113 } 114 115 // Ignore all but real mouse event coming from physical mouse 116 // (especially ignore mouse event being dispatched from a touch event) 117 if ( 118 evt.button || 119 evt.inputSource != evt.MOZ_SOURCE_MOUSE || 120 evt.isSynthesized 121 ) { 122 return; 123 } 124 125 const eventTarget = this.target; 126 let type = ""; 127 switch (evt.type) { 128 case "mouseenter": 129 case "mouseover": 130 case "mouseout": 131 case "mouseleave": 132 // Don't propagate events which are not related to touch events 133 evt.stopPropagation(); 134 evt.preventDefault(); 135 136 // We don't want to trigger any visual changes to elements whose content can 137 // be modified via hover states. We can avoid this by removing the element's 138 // content state. 139 InspectorUtils.removeContentState(evt.target, kStateHover); 140 break; 141 142 case "mousedown": 143 this.target = evt.target; 144 145 // If the click-hold feature is enabled, start a timeout to convert long clicks 146 // into contextmenu events. 147 // Just don't do it if the event occurred on a scrollbar. 148 if (isClickHoldEnabled && !evt.originalTarget.closest("scrollbar")) { 149 this._contextMenuTimeout = this.sendContextMenu(evt); 150 } 151 152 this.previousScreenY = evt.screenY; 153 154 type = "touchstart"; 155 break; 156 157 case "mousemove": { 158 if (!eventTarget) { 159 // Don't propagate mousemove event when touchstart event isn't fired 160 evt.stopPropagation(); 161 return; 162 } 163 164 type = "touchmove"; 165 const deltaY = evt.screenY - this.previousScreenY; 166 this.previousScreenY = evt.screenY; 167 this.windowTarget.emit("contentScrolled", deltaY); 168 break; 169 } 170 171 case "mouseup": 172 if (!eventTarget) { 173 return; 174 } 175 this.target = null; 176 177 content.clearTimeout(this._contextMenuTimeout); 178 type = "touchend"; 179 180 // Only register click listener after mouseup to ensure 181 // catching only real user click. (Especially ignore click 182 // being dispatched on form submit) 183 if (evt.detail == 1) { 184 this.simulatorTarget.addEventListener("click", this, { 185 capture: true, 186 once: true, 187 }); 188 } 189 break; 190 } 191 192 const target = eventTarget || this.target; 193 if (target && type) { 194 this.sendTouchEvent(content, evt.clientX, evt.clientY, type); 195 } 196 197 evt.preventDefault(); 198 evt.stopImmediatePropagation(); 199 } 200 201 sendContextMenu({ target, clientX, clientY, screenX, screenY }) { 202 const view = target.ownerGlobal; 203 const evt = new view.PointerEvent("contextmenu", { 204 bubbles: true, 205 cancelable: true, 206 view, 207 screenX, 208 screenY, 209 clientX, 210 clientY, 211 }); 212 const content = this.getContent(target); 213 const timeout = content.setTimeout(() => { 214 target.dispatchEvent(evt); 215 }, clickHoldDelay); 216 217 return timeout; 218 } 219 220 /** 221 * Sends a touch action on a given target element. 222 * 223 * @param {Window} win 224 * The target window. 225 * @param {number} clientX 226 * The `x` screen coordinate relative to the viewport origin. 227 * @param {number} clientY 228 * The `y` screen coordinate relative to the viewport origin. 229 * @param {string} type 230 * The type of the touch event. 231 */ 232 sendTouchEvent(win, clientX, clientY, type) { 233 const utils = win.windowUtils; 234 utils.sendTouchEvent( 235 type, 236 [0], 237 [clientX], 238 [clientY], 239 [0], 240 [0], 241 [0], 242 [0], 243 [0], 244 [0], 245 [0], 246 0, 247 utils.ASYNC_ENABLED 248 ); 249 return true; 250 } 251 252 getContent(target) { 253 const win = target?.ownerDocument ? target.ownerGlobal : null; 254 return win; 255 } 256 } 257 258 exports.TouchSimulator = TouchSimulator;