180 lines
6.4 KiB
JavaScript
180 lines
6.4 KiB
JavaScript
|
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.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)]);
|
||
|
}
|
||
|
|
||
|
bindTo(element) {
|
||
|
element.addEventListener("touchmove", this.onTouchMove.bind(this));
|
||
|
element.addEventListener("touchend", this.onTouchEnd.bind(this));
|
||
|
element.addEventListener("touchstart", this.onTouchStart.bind(this));
|
||
|
}
|
||
|
|
||
|
onTouchMove(event) {
|
||
|
const touches = event.changedTouches;
|
||
|
event.preventDefault();
|
||
|
event.stopPropagation();
|
||
|
|
||
|
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;
|
||
|
event.preventDefault();
|
||
|
event.stopPropagation();
|
||
|
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;
|
||
|
event.preventDefault();
|
||
|
event.stopPropagation();
|
||
|
|
||
|
// 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;
|