Skip to content

Instantly share code, notes, and snippets.

@artursopelnik
Last active March 5, 2026 13:13
Show Gist options
  • Select an option

  • Save artursopelnik/b81948d01add8f800d076fb0cfde8897 to your computer and use it in GitHub Desktop.

Select an option

Save artursopelnik/b81948d01add8f800d076fb0cfde8897 to your computer and use it in GitHub Desktop.
what-input-lite (ESM + TypeScript, no legacy code, no touch)
/**
* what-iput-lite
* A global utility for tracking the current input method (mouse/pointer, keyboard).
* ESM + TypeScript, no touch detection, no legacy code.
*/
export type InputType = 'mouse' | 'keyboard' | 'initial';
export type InputIntent = InputType;
export type ChangeCallback = (input: InputType) => void;
export type WatchTarget = 'input' | 'intent';
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
let currentInput: InputType = 'initial';
let currentIntent: InputIntent = 'initial';
const inputCallbacks: ChangeCallback[] = [];
const intentCallbacks: ChangeCallback[] = [];
let ignoreKeys: Set<number> = new Set([
16, // Shift
17, // Control
18, // Alt
91, // Meta left (Cmd / Win)
92, // Win right
93, // Meta right / Context Menu
]);
let specificKeys: Set<number> | null = null;
// While a form element is focused via mouse, freeze whatintent so typing
// inside the field doesn't flip intent to "keyboard".
let intentFrozen = false;
const FORM_ELEMENTS = new Set(['input', 'select', 'textarea', 'button']);
// ---------------------------------------------------------------------------
// Shadow DOM helper
// ---------------------------------------------------------------------------
/**
* Returns the actual event target, piercing shadow DOM boundaries.
* e.target is retargeted to the shadow host for events inside shadow roots,
* so composedPath()[0] gives the real element (e.g. <input> inside <k-input>).
*/
function getDeepTarget(e: Event): Element | null {
const path = e.composedPath();
return (path.length > 0 ? path[0] : e.target) as Element | null;
}
// ---------------------------------------------------------------------------
// DOM attribute helpers
// ---------------------------------------------------------------------------
const ROOT = document.documentElement;
function setAttr(key: string, value: string): void {
ROOT.setAttribute(key, value);
}
// ---------------------------------------------------------------------------
// Update helpers
// ---------------------------------------------------------------------------
function updateInput(type: InputType): void {
if (currentInput !== type) {
currentInput = type;
setAttr('data-whatinput', type);
inputCallbacks.forEach(cb => cb(type));
}
}
function updateIntent(type: InputIntent): void {
if (intentFrozen) return;
if (currentIntent !== type) {
currentIntent = type;
setAttr('data-whatintent', type);
intentCallbacks.forEach(cb => cb(type));
}
}
// ---------------------------------------------------------------------------
// Form-element focus tracking (intent buffer)
// ---------------------------------------------------------------------------
function onFocusIn(e: FocusEvent): void {
const tag = getDeepTarget(e)?.tagName?.toLowerCase();
if (tag && FORM_ELEMENTS.has(tag)) {
// Only freeze if the current intent is mouse (user clicked into the field)
if (currentIntent === 'mouse') intentFrozen = true;
}
}
function onFocusOut(e: FocusEvent): void {
const tag = getDeepTarget(e)?.tagName?.toLowerCase();
if (tag && FORM_ELEMENTS.has(tag)) {
intentFrozen = false;
}
}
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
function onPointerDown(e: PointerEvent): void {
// Ignore non-mouse pointers (pen → treat as mouse if you want, or skip)
if (e.pointerType !== 'mouse') return;
updateInput('mouse');
updateIntent('mouse');
}
function onPointerMove(e: PointerEvent): void {
if (e.pointerType !== 'mouse') return;
updateIntent('mouse');
}
function onKeyDown(e: KeyboardEvent): void {
const code = e.keyCode ?? e.which;
if (specificKeys) {
if (!specificKeys.has(code)) return;
} else if (ignoreKeys.has(code)) {
return;
}
updateInput('keyboard');
updateIntent('keyboard');
}
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
setAttr('data-whatinput', currentInput);
setAttr('data-whatintent', currentIntent);
window.addEventListener('pointerdown', onPointerDown, { capture: true, passive: true });
window.addEventListener('pointermove', onPointerMove, { capture: true, passive: true });
window.addEventListener('keydown', onKeyDown, { capture: true, passive: true });
window.addEventListener('focusin', onFocusIn, { capture: true, passive: true });
window.addEventListener('focusout', onFocusOut, { capture: true, passive: true });
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/** Returns the current input type or intent. */
export function ask(which: 'input' | 'intent' = 'input'): InputType {
return which === 'intent' ? currentIntent : currentInput;
}
/** Register a callback fired when input or intent changes. */
export function registerOnChange(callback: ChangeCallback, target: WatchTarget = 'input'): void {
const list = target === 'intent' ? intentCallbacks : inputCallbacks;
if (!list.includes(callback)) list.push(callback);
}
/** Remove a previously registered callback. */
export function unRegisterOnChange(callback: ChangeCallback): void {
for (const list of [inputCallbacks, intentCallbacks]) {
const idx = list.indexOf(callback);
if (idx !== -1) list.splice(idx, 1);
}
}
/**
* Override which keyCodes are ignored (won't switch to "keyboard").
* Replaces the default set: Shift, Ctrl, Alt, Meta.
*/
export function setIgnoreKeys(keys: number[]): void {
ignoreKeys = new Set(keys);
specificKeys = null;
}
/**
* Only these keyCodes will trigger the keyboard intent.
* Overrides ignoreKeys. Pass null to reset.
*/
export function setSpecificKeys(keys: number[] | null): void {
specificKeys = keys ? new Set(keys) : null;
}
/** Destroy all listeners (useful for SPA cleanup). */
export function destroy(): void {
window.removeEventListener('pointerdown', onPointerDown, { capture: true });
window.removeEventListener('pointermove', onPointerMove, { capture: true });
window.removeEventListener('keydown', onKeyDown, { capture: true });
window.removeEventListener('focusin', onFocusIn, { capture: true });
window.removeEventListener('focusout', onFocusOut, { capture: true });
inputCallbacks.length = 0;
intentCallbacks.length = 0;
}
const whatInput = { ask, registerOnChange, unRegisterOnChange, setIgnoreKeys, setSpecificKeys, destroy };
export default whatInput;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment