-
-
Save phun-ky/efd8785690ab3279a43d2da7bc634a40 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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