ci-trap-web
Version:
Lightweight mouse and touch event tracker library for browsers.
501 lines (448 loc) • 12.6 kB
JavaScript
//------------------------------------------------------------------------------
// Copyright (C) 2023- Cursor Insight Ltd.
//
// All rights reserved.
//------------------------------------------------------------------------------
// Handler implementation to manage incoming DOM events.
//------------------------------------------------------------------------------
import simpleAutoBind from './simpleAutoBind';
import eventEmitterMixin from './eventEmitterMixin';
import {
TOUCH_ENABLED,
POINTER_ENABLED,
VISIBILITY_CHANGE_ENABLED,
FREEZE_ENABLED,
MOUSE_MOVE_MESSAGE_TYPE,
TOUCH_START_MESSAGE_TYPE,
TOUCH_MOVE_MESSAGE_TYPE,
TOUCH_END_MESSAGE_TYPE,
MOUSE_DOWN_MESSAGE_TYPE,
MOUSE_UP_MESSAGE_TYPE,
BLUR_WINDOW_MESSAGE_TYPE,
FOCUS_WINDOW_MESSAGE_TYPE,
WHEEL_MESSAGE_TYPE,
SCROLL_MESSAGE_TYPE,
REQUEST_ANIMATION_FRAME_MESSAGE_TYPE,
PAGE_STATE_INACTIVE,
PAGE_STATE_ACTIVE,
} from './constants';
import TimeUtils from './timeUtils';
const EVENT_HANDLER_OPTIONS = {
passive: true,
capture: true,
};
class Handlers {
constructor() {
simpleAutoBind(this);
// DOM elements to which these handlers are already mounted to
this._registeredElements = [];
// Also capture coalesced events
this._captureCoalescedEvents = true;
this._requestAnimationFrameId = undefined;
}
// Mount global handlers -- that are not specific to any element.
mountGlobal() {
if (this._registeredElements.length > 0) { return; }
if (VISIBILITY_CHANGE_ENABLED) {
document.addEventListener(
'visibilitychange',
this.handleVisibilityChange,
);
} else {
window.addEventListener('pagehide', this.handlePageHide);
}
if (FREEZE_ENABLED) {
document.addEventListener('freeze', this.handleFreeze);
}
window.addEventListener('blur', this.handleBlur);
window.addEventListener('focus', this.handleFocus);
window.addEventListener('scroll', this.handleScroll);
}
// Unmount global handlers -- that are not specific to any element.
umountGlobal() {
if (this._registeredElements.length > 0) { return; }
if (VISIBILITY_CHANGE_ENABLED) {
document.removeEventListener(
'visibilitychange',
this.handleVisibilityChange,
);
} else {
window.removeEventListener('pagehide', this.handlePageHide);
}
if (FREEZE_ENABLED) {
document.removeEventListener('freeze', this.handleFreeze);
}
window.removeEventListener('blur', this.handleBlur);
window.removeEventListener('focus', this.handleFocus);
window.removeEventListener('scroll', this.handleScroll);
}
// Mount handlers to given DOM element
mount(element) {
if (this._registeredElements.includes(element)) { return; }
// First register global handlers (e.g. `visibilitychange`)
this.mountGlobal();
if (TOUCH_ENABLED) {
element.addEventListener(
'touchstart',
this.handleTouchStart,
EVENT_HANDLER_OPTIONS,
);
element.addEventListener(
'touchmove',
this.handleTouchMove,
EVENT_HANDLER_OPTIONS,
);
element.addEventListener(
'touchend',
this.handleTouchEnd,
EVENT_HANDLER_OPTIONS,
);
} else if (POINTER_ENABLED) {
element.addEventListener(
'pointermove',
this.handlePointerMove,
EVENT_HANDLER_OPTIONS,
);
element.addEventListener(
'pointerdown',
this.handlePointerDown,
EVENT_HANDLER_OPTIONS,
);
element.addEventListener(
'pointerup',
this.handlePointerUp,
EVENT_HANDLER_OPTIONS,
);
} else {
element.addEventListener(
'mousemove',
this.handleMouseMove,
EVENT_HANDLER_OPTIONS,
);
element.addEventListener(
'mousedown',
this.handleMouseDown,
EVENT_HANDLER_OPTIONS,
);
element.addEventListener(
'mouseup',
this.handleMouseUp,
EVENT_HANDLER_OPTIONS,
);
}
element.addEventListener('wheel', this.handleWheel, EVENT_HANDLER_OPTIONS);
// Insert `element` into `_registeredElements`
this._registeredElements.push(element);
}
// Unmount handlers from given DOM element
umount(element) {
if (!this._registeredElements.includes(element)) { return; }
if (TOUCH_ENABLED) {
element.removeEventListener(
'touchstart',
this.handleTouchStart,
EVENT_HANDLER_OPTIONS,
);
element.removeEventListener(
'touchmove',
this.handleTouchMove,
EVENT_HANDLER_OPTIONS,
);
element.removeEventListener(
'touchend',
this.handleTouchEnd,
EVENT_HANDLER_OPTIONS,
);
} else if (POINTER_ENABLED) {
element.removeEventListener(
'pointermove',
this.handlePointerMove,
EVENT_HANDLER_OPTIONS,
);
element.removeEventListener(
'pointerdown',
this.handlePointerDown,
EVENT_HANDLER_OPTIONS,
);
element.removeEventListener(
'pointerup',
this.handlePointerUp,
EVENT_HANDLER_OPTIONS,
);
} else {
element.removeEventListener(
'mousemove',
this.handleMouseMove,
EVENT_HANDLER_OPTIONS,
);
element.removeEventListener(
'mousedown',
this.handleMouseDown,
EVENT_HANDLER_OPTIONS,
);
element.removeEventListener(
'mouseup',
this.handleMouseUp,
EVENT_HANDLER_OPTIONS,
);
}
element.removeEventListener(
'wheel',
this.handleWheel,
EVENT_HANDLER_OPTIONS,
);
// Remove `element` from `_registeredElements`
const idx = this._registeredElements.indexOf(element);
this._registeredElements.splice(idx, 1);
// Last, unregister global handlers
this.umountGlobal();
}
push(...props) {
this.emit('message', props);
}
// Submit events explicitly
requestSubmission() {
this.emit('requestSubmission');
}
// Set the captureCoalescedEvents option
captureCoalescedEvents(value) {
this._captureCoalescedEvents = value;
}
// `pointermove` and `mousemove` event handler
handlePointerMove(event) {
let events = [];
if (this._captureCoalescedEvents && event.getCoalescedEvents) {
events = event.getCoalescedEvents();
}
if (events.length === 0) {
this.handleSinglePointerMove(event, false);
} else {
events.forEach((coalescedEvent, index) => {
this.handleSinglePointerMove(
coalescedEvent,
index !== events.length - 1,
);
});
}
}
handleSinglePointerMove(event, coalesced) {
switch (event.pointerType) {
case 'mouse':
this.handleMouseMove(event, coalesced);
break;
case 'touch':
this.push(
TOUCH_MOVE_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
event.pointerId,
event.screenX,
event.screenY,
coalesced ? 1 : 0,
);
break;
default:
break;
}
}
// `pointerdown` event handler
handlePointerDown(event) {
switch (event.pointerType) {
case 'mouse':
this.handleMouseDown(event);
break;
case 'touch':
this.push(
TOUCH_START_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
event.pointerId,
event.screenX,
event.screenY,
);
break;
default:
break;
}
}
// `pointerup` and `mouseup` event handler
handlePointerUp(event) {
switch (event.pointerType) {
case 'mouse':
this.handleMouseUp(event);
break;
case 'touch':
this.push(
TOUCH_END_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
event.pointerId,
event.screenX,
event.screenY,
);
break;
default:
break;
}
}
// `mousemove` event handler
handleMouseMove(event, coalesced) {
this.push(
MOUSE_MOVE_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
event.screenX,
event.screenY,
event.buttons,
coalesced ? 1 : 0,
);
}
// `mousedown` event handler
handleMouseDown(event) {
this.push(
MOUSE_DOWN_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
event.screenX,
event.screenY,
event.buttons,
event.button,
);
}
// `mouseup` event handler
handleMouseUp(event) {
this.push(
MOUSE_UP_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
event.screenX,
event.screenY,
event.buttons,
event.button,
);
}
// `touchstart` event handler
handleTouchStart(event) {
this.iterateTouches(event, (touch) => {
this.push(
TOUCH_START_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
touch.identifier,
touch.screenX,
touch.screenY,
);
});
}
// `touchmove` event handler
handleTouchMove(event) {
this.iterateTouches(event, (touch) => {
this.push(
TOUCH_MOVE_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
touch.identifier,
touch.screenX,
touch.screenY,
0,
);
});
}
// `touchend` event handler
handleTouchEnd(event) {
this.iterateTouches(event, (touch) => {
this.push(
TOUCH_END_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
touch.identifier,
touch.screenX,
touch.screenY,
);
});
}
// Touch helper
// eslint-disable-next-line class-methods-use-this
iterateTouches(event, fn) {
Array.from(event.changedTouches).forEach(fn);
}
// Wheel event handler
handleWheel(event) {
this.push(
WHEEL_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
event.deltaX,
event.deltaY,
event.deltaZ,
event.deltaMode,
);
}
// Scroll event handler
handleScroll(event) {
this.push(
SCROLL_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
window.scrollX,
window.scrollY,
);
}
// Handle "page unload" events the recommended way.
//
// For details, see:
// - https://caniuse.com/?search=visibilitychange
// - https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event
// - https://developer.mozilla.org/en-US/docs/Web/API/Window/pagehide_event
handleVisibilityChange(/* event */) {
if (document.visibilityState === 'hidden') {
this.requestSubmission();
}
}
// Handle "page unload" events the safe way -- that is compatible with
// browsers that does not support `visibilitychange` events.
//
// For more info, see: `handleVisibilityChange` too.
handlePageHide(/* event */) {
this.requestSubmission();
}
// Handle document freeze event
handleFreeze(/* event */) {
this.requestSubmission();
}
// Handle window focus leave event: register event and send stream
// automatically.
handleBlur(event) {
this.push(
BLUR_WINDOW_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
event,
);
this.requestSubmission();
this.emit('pageStateChanged', PAGE_STATE_INACTIVE);
}
// Handle window focus event: register a new event to the stream
handleFocus(event) {
TimeUtils.actualizeEpoch();
this.emit('pageStateChanged', PAGE_STATE_ACTIVE);
this.push(
FOCUS_WINDOW_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(event.timeStamp),
event,
);
}
// Start collecting requestAnimationFrame messages
startRequestAnimationFrame() {
this._requestAnimationFrameId = requestAnimationFrame(
this.onRequestAnimationFrame,
);
}
// Stop collecting requestAnimationFrame messages
stopRequestAnimationFrame() {
if (this._requestAnimationFrameId !== undefined) {
cancelAnimationFrame(this._requestAnimationFrameId);
this._requestAnimationFrameId = undefined;
}
}
// Event handler for requestAnimationFrame
onRequestAnimationFrame(timeStamp) {
this._requestAnimationFrameId = requestAnimationFrame(
this.onRequestAnimationFrame,
);
this.push(
REQUEST_ANIMATION_FRAME_MESSAGE_TYPE,
TimeUtils.convertEventTimeToTs(timeStamp),
);
}
}
Object.assign(Handlers.prototype, eventEmitterMixin);
export default Handlers;