Skip to content

Instantly share code, notes, and snippets.

@phun-ky
Created June 10, 2021 07:06
Show Gist options
  • Save phun-ky/efd8785690ab3279a43d2da7bc634a40 to your computer and use it in GitHub Desktop.
Save phun-ky/efd8785690ab3279a43d2da7bc634a40 to your computer and use it in GitHub Desktop.
const whatinput = (global) => {
/*
* variables
*/
// cache document.documentElement
const docElem = global ? global : document.documentElement;
if (!global) {
global = window;
}
// currently focused dom element
let currentElement = null;
// last used input type
let currentInput = 'initial';
// last used input intent
let currentIntent = currentInput;
// UNIX timestamp of current event
let currentTimestamp = Date.now();
// form input types
const formInputs = ['button', 'input', 'select', 'textarea'];
// list of modifier keys commonly used with the mouse and
// can be safely ignored to prevent false keyboard detection
let ignoreMap = [
16, // shift
17, // control
18, // alt
91, // Windows key / left Apple cmd
93, // Windows menu / right Apple cmd
];
let specificMap = [];
// mapping of events to input types
const inputMap = {
keydown: 'keyboard',
keyup: 'keyboard',
mousedown: 'mouse',
mousemove: 'mouse',
MSPointerDown: 'pointer',
MSPointerMove: 'pointer',
pointerdown: 'pointer',
pointermove: 'pointer',
touchstart: 'touch',
touchend: 'touch',
};
// boolean: true if the page is being scrolled
let isScrolling = false;
// store current mouse position
const mousePos = {
x: null,
y: null,
};
// map of IE 10 pointer events
const pointerMap = {
2: 'touch',
3: 'touch', // treat pen like touch
4: 'mouse',
};
// check support for passive event listeners
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', {
get: () => {
supportsPassive = true;
return supportsPassive;
},
});
global.addEventListener('test', null, opts);
} catch (e) {
// fail silently
}
/*
* set up
*/
const setUp = () => {
// add correct mouse wheel event mapping to `inputMap`
inputMap[detectWheel()] = 'mouse';
addListeners();
};
/*
* events
*/
const addListeners = () => {
// `pointermove`, `MSPointerMove`, `mousemove` and mouse wheel event binding
// can only demonstrate potential, but not actual, interaction
// and are treated separately
const options = supportsPassive ? { passive: true } : false;
// pointer events (mouse, pen, touch)
if (window.PointerEvent) {
global.addEventListener('pointerdown', setInput);
global.addEventListener('pointermove', setIntent);
} else if (window.MSPointerEvent) {
global.addEventListener('MSPointerDown', setInput);
global.addEventListener('MSPointerMove', setIntent);
} else {
// mouse events
global.addEventListener('mousedown', setInput);
global.addEventListener('mousemove', setIntent);
// touch events
if ('ontouchstart' in window) {
global.addEventListener('touchstart', setInput, options);
global.addEventListener('touchend', setInput);
}
}
// mouse wheel
global.addEventListener(detectWheel(), setIntent, options);
// keyboard events
global.addEventListener('keydown', setInput);
global.addEventListener('keyup', setInput);
// focus events
global.addEventListener('focusin', setElement);
global.addEventListener('focusout', clearElement);
};
// checks conditions before updating new input
const setInput = (event) => {
const eventKey = event.which;
let value = inputMap[event.type];
if (value === 'pointer') {
value = pointerType(event);
}
const ignoreMatch = !specificMap.length && ignoreMap.indexOf(eventKey) === -1;
const specificMatch = specificMap.length && specificMap.indexOf(eventKey) !== -1;
let shouldUpdate =
(value === 'keyboard' && eventKey && (ignoreMatch || specificMatch)) || value === 'mouse' || value === 'touch';
// prevent touch detection from being overridden by event execution order
if (validateTouch(value)) {
shouldUpdate = false;
}
if (shouldUpdate && currentInput !== value) {
currentInput = value;
doUpdate('input');
}
if (shouldUpdate && currentIntent !== value) {
// preserve intent for keyboard interaction with form fields
const activeElem = document.activeElement;
const notFormInput =
activeElem &&
activeElem.nodeName &&
(formInputs.indexOf(activeElem.nodeName.toLowerCase()) === -1 ||
(activeElem.nodeName.toLowerCase() === 'button' && !activeElem.closest('form')));
if (notFormInput) {
currentIntent = value;
doUpdate('intent');
}
}
};
// updates the doc and `inputTypes` array with new input
const doUpdate = (which) => {
docElem.setAttribute('data-what' + which, which === 'input' ? currentInput : currentIntent);
};
// updates input intent for `mousemove` and `pointermove`
const setIntent = (event) => {
let value = inputMap[event.type];
if (value === 'pointer') {
value = pointerType(event);
}
// test to see if `mousemove` happened relative to the screen to detect scrolling versus mousemove
detectScrolling(event);
// only execute if scrolling isn't happening
if (
((!isScrolling && !validateTouch(value)) ||
(isScrolling && event.type === 'wheel') ||
event.type === 'mousewheel' ||
event.type === 'DOMMouseScroll') &&
currentIntent !== value
) {
currentIntent = value;
doUpdate('intent');
}
};
const setElement = (event) => {
if (!event.target.nodeName) {
// If nodeName is undefined, clear the element
// This can happen if click inside an <svg> element.
clearElement();
return;
}
currentElement = event.target.nodeName.toLowerCase();
docElem.setAttribute('data-whatelement', currentElement);
if (event.target.classList && event.target.classList.length) {
docElem.setAttribute('data-whatclasses', event.target.classList.toString().replace(' ', ','));
}
};
const clearElement = () => {
currentElement = null;
docElem.removeAttribute('data-whatelement');
docElem.removeAttribute('data-whatclasses');
};
/*
* utilities
*/
const pointerType = (event) => {
if (typeof event.pointerType === 'number') {
return pointerMap[event.pointerType];
} else {
// treat pen like touch
return event.pointerType === 'pen' ? 'touch' : event.pointerType;
}
};
// prevent touch detection from being overridden by event execution order
const validateTouch = (value) => {
const timestamp = Date.now();
const touchIsValid = value === 'mouse' && currentInput === 'touch' && timestamp - currentTimestamp < 200;
currentTimestamp = timestamp;
return touchIsValid;
};
// detect version of mouse wheel event to use
// via https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event
const detectWheel = () => {
let wheelType = null;
// Modern browsers support "wheel"
if ('onwheel' in document.createElement('div')) {
wheelType = 'wheel';
} else {
// Webkit and IE support at least "mousewheel"
// or assume that remaining browsers are older Firefox
wheelType = document.onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll';
}
return wheelType;
};
const detectScrolling = (event) => {
if (mousePos.x !== event.screenX || mousePos.y !== event.screenY) {
isScrolling = false;
mousePos.x = event.screenX;
mousePos.y = event.screenY;
} else {
isScrolling = true;
}
};
/*
* init
*/
setUp();
};
export default whatinput;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment