capybara/frontend/src/Touchpad.js

191 lines
6.7 KiB
JavaScript
Raw Normal View History

const HOLDING_THRESHOLD_MS = 300;
const SCROLL_X_DAMPENING = 0.03;
const SCROLL_Y_DAMPENING = 0.09;
class TouchpadController {
constructor(connection) {
this.currentMoveX = 0;
this.currentMoveY = 0;
this.lastMoveX = 0;
this.lastMoveY = 0;
this.shouldResetLastMove = false;
this.isInHoldingMode = false;
this.ongoingTouches = {};
this.holdModeTimeout = null;
this.touchpadDiv = null;
this.connection = connection;
}
getButtonCode(button) {
let buttonCode = 0;
if (button === "right") buttonCode = 1;
return buttonCode;
}
// takes a gesture name and a touch list
// adds the gesture name to the touch and returns false if that touch already had that gesture
_gesture(gestureName, touchList) {
for (let i = 0; i < touchList.length; i++) {
const touch = this.ongoingTouches[touchList[i].identifier];
if (touch.gestured.includes(gestureName)) return false;
touch.gestured.push(gestureName);
}
return true;
}
_sendRelativeMouseMovement(dx, dy) {
if (dx === 0 && dy === 0)
return false;
this.connection.sendMessage("r", [dx, dy]);
return true;
}
_sendRelativeMouseScroll(dx, dy) {
if (dx === 0 && dy === 0)
return false;
this.connection.sendMessage("s", [dx, dy]);
return true;
}
_sendMouseButtonDown(button="left") {
this.connection.sendMessage("d", [this.getButtonCode(button)]);
}
_sendMouseButtonUp(button="left") {
this.connection.sendMessage("u", [this.getButtonCode(button)]);
}
_sendSingleClick(button="left") {
this.connection.sendMessage("c", [this.getButtonCode(button)]);
}
mountOn(element) {
if (this.touchpadDiv)
return; // Already mounted
this.touchpadDiv = document.createElement("div");
this.touchpadDiv.classList.add("touchpad");
element.appendChild(this.touchpadDiv);
this.touchpadDiv.addEventListener("touchmove", this.onTouchMove.bind(this));
this.touchpadDiv.addEventListener("touchend", this.onTouchEnd.bind(this));
this.touchpadDiv.addEventListener("touchstart", this.onTouchStart.bind(this));
}
unmount() {
if (!this.touchpadDiv)
return; // Not mounted - don't do anything
this.touchpadDiv.parentElement.removeChild(this.touchpadDiv);
this.touchpadDiv = null;
}
onTouchMove(event) {
const touches = event.changedTouches;
event.preventDefault();
const targetTouch = touches[0];
this.currentMoveX = targetTouch.pageX;
this.currentMoveY = targetTouch.pageY;
// When ending a touch and starting a new one in another part of the touchpad,
// the cursor "rubber bands" to that position.
// To solve this, the "last move" parameters are reset when a touch is ended
// (see onTouchEnd())
if (this.shouldResetLastMove) {
this.shouldResetLastMove = false;
this.lastMoveX = this.currentMoveX;
this.lastMoveY = this.currentMoveY;
}
const deltaX = this.currentMoveX - this.lastMoveX;
const deltaY = this.currentMoveY - this.lastMoveY;
this.lastMoveX = this.currentMoveX;
this.lastMoveY = this.currentMoveY;
// if two touches moved at the same time, assume scrolling intent
// if _sendRelativeMouseScroll or _sendRelativeMouseMovement return true, it means that the delta values are non-zero and a packet has been sent to the server
// in that case, the touches will be marked as moved
let shouldMarkTouchesAsMoved = false;
if (touches.length === 2) {
if (
this._sendRelativeMouseScroll(deltaX * SCROLL_X_DAMPENING, deltaY * SCROLL_Y_DAMPENING)
) {
shouldMarkTouchesAsMoved = true;
}
} else if (touches.length === 3 && this._gesture("GESTURE_RIGHT_CLICK", touches)) {
shouldMarkTouchesAsMoved = true;
this._sendSingleClick("right");
} else {
if (
this._sendRelativeMouseMovement(deltaX, deltaY)
) {
shouldMarkTouchesAsMoved = true;
}
}
if (shouldMarkTouchesAsMoved) {
for (let i = 0; i < touches.length; i++) {
this.ongoingTouches[touches[i].identifier].hasMoved = true;
}
}
}
onTouchEnd(event) {
const changedTouches = event.changedTouches;
this.shouldResetLastMove = true;
if (changedTouches.length === 1) {
// This is a single tap - left click
if (!this.ongoingTouches[changedTouches[0].identifier].hasMoved) {
this._sendSingleClick("left");
// We were in "holding mode" and now that touch event has ended,
// thus we have to stop holding the left click button
} else if (this.isInHoldingMode) {
this._sendMouseButtonUp("left");
this.isInHoldingMode = false;
}
}
// remove all ended touches
for (let i = 0; i < changedTouches.length; i++) {
const touch = changedTouches[i];
this.ongoingTouches[touch.identifier] = null;
delete this.ongoingTouches[touch.identifier];
}
}
onTouchStart(event) {
const changedTouches = event.changedTouches;
// Clear the hold mode time out if another touch begins
if (this.holdModeTimeout)
clearTimeout(this.holdModeTimeout);
// If the touch is still unmoved and held for a certain amount of time,
// we will enter "holding mode" which keeps the mouse button held while dragging
// allowing you to, for example, select text or drag a scrollbar
if (changedTouches.length === 1) {
const targetTouch = changedTouches[0];
this.holdModeTimeout = setTimeout(() => {
if (this.ongoingTouches[targetTouch.identifier] && !this.ongoingTouches[targetTouch.identifier].hasMoved) {
this.isInHoldingMode = true;
this._sendMouseButtonDown("left");
}
}, HOLDING_THRESHOLD_MS);
}
for (let i = 0; i < changedTouches.length; i++) {
const touch = changedTouches[i];
this.ongoingTouches[touch.identifier] = {
identifier: touch.identifier,
clientX: touch.clientX,
clientY: touch.clientY,
hasMoved: false,
gestured: []
};
}
}
}
export default TouchpadController;