/** * Copyright (c) Nicolas Gallagher. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import { canUseDOM } from '../canUseDOM' import { createEventHandle } from '../createEventHandle/index' export type Modality = 'keyboard' | 'mouse' | 'touch' | 'pen' const supportsPointerEvent = () => !!(typeof window !== 'undefined' && window.PointerEvent != null) let activeModality = 'keyboard' let modality = 'keyboard' let previousModality let previousActiveModality let isEmulatingMouseEvents = false const listeners = new Set() const KEYBOARD = 'keyboard' const MOUSE = 'mouse' const TOUCH = 'touch' const BLUR = 'blur' const CONTEXTMENU = 'contextmenu' const FOCUS = 'focus' const KEYDOWN = 'keydown' const MOUSEDOWN = 'mousedown' const MOUSEMOVE = 'mousemove' const MOUSEUP = 'mouseup' const POINTERDOWN = 'pointerdown' const POINTERMOVE = 'pointermove' const SCROLL = 'scroll' const SELECTIONCHANGE = 'selectionchange' const TOUCHCANCEL = 'touchcancel' const TOUCHMOVE = 'touchmove' const TOUCHSTART = 'touchstart' const VISIBILITYCHANGE = 'visibilitychange' const bubbleOptions = { passive: true } const captureOptions = { capture: true, passive: true } // Window events const addBlurListener = createEventHandle(BLUR, bubbleOptions) const addFocusListener = createEventHandle(FOCUS, bubbleOptions) // Must be capture phase because 'stopPropagation' might prevent these // events bubbling to the document. const addVisibilityChangeListener = createEventHandle(VISIBILITYCHANGE, captureOptions) const addKeyDownListener = createEventHandle(KEYDOWN, captureOptions) const addPointerDownListener = createEventHandle(POINTERDOWN, captureOptions) const addPointerMoveListener = createEventHandle(POINTERMOVE, captureOptions) // Fallback events const addContextMenuListener = createEventHandle(CONTEXTMENU, captureOptions) const addMouseDownListener = createEventHandle(MOUSEDOWN, captureOptions) const addMouseMoveListener = createEventHandle(MOUSEMOVE, captureOptions) const addMouseUpListener = createEventHandle(MOUSEUP, captureOptions) const addScrollListener = createEventHandle(SCROLL, captureOptions) const addSelectiomChangeListener = createEventHandle(SELECTIONCHANGE, captureOptions) const addTouchCancelListener = createEventHandle(TOUCHCANCEL, captureOptions) const addTouchMoveListener = createEventHandle(TOUCHMOVE, captureOptions) const addTouchStartListener = createEventHandle(TOUCHSTART, captureOptions) function restoreModality() { if (previousModality != null || previousActiveModality != null) { if (previousModality != null) { modality = previousModality previousModality = null } if (previousActiveModality != null) { activeModality = previousActiveModality previousActiveModality = null } callListeners() } } function onBlurWindow() { previousModality = modality previousActiveModality = activeModality activeModality = KEYBOARD modality = KEYBOARD callListeners() // for fallback events isEmulatingMouseEvents = false } function onFocusWindow() { restoreModality() } function onKeyDown(event) { if (event.metaKey || event.altKey || event.ctrlKey) { return } if (modality !== KEYBOARD) { modality = KEYBOARD activeModality = KEYBOARD callListeners() } } function onVisibilityChange() { if (document.visibilityState !== 'hidden') { restoreModality() } } function onPointerish(event: any) { const eventType = event.type if (supportsPointerEvent()) { if (eventType === POINTERDOWN) { if (activeModality !== event.pointerType) { modality = event.pointerType activeModality = event.pointerType callListeners() } return } if (eventType === POINTERMOVE) { if (modality !== event.pointerType) { modality = event.pointerType callListeners() } return } } // Fallback for non-PointerEvent environment else { if (!isEmulatingMouseEvents) { if (eventType === MOUSEDOWN) { if (activeModality !== MOUSE) { modality = MOUSE activeModality = MOUSE callListeners() } } if (eventType === MOUSEMOVE) { if (modality !== MOUSE) { modality = MOUSE callListeners() } } } // Flag when browser may produce emulated events if (eventType === TOUCHSTART) { isEmulatingMouseEvents = true if (event.touches && event.touches.length > 1) { isEmulatingMouseEvents = false } if (activeModality !== TOUCH) { modality = TOUCH activeModality = TOUCH callListeners() } return } // Remove flag after emulated events are finished or cancelled, and if an // event occurs that cuts short a touch event sequence. if ( eventType === CONTEXTMENU || eventType === MOUSEUP || eventType === SELECTIONCHANGE || eventType === SCROLL || eventType === TOUCHCANCEL || eventType === TOUCHMOVE ) { isEmulatingMouseEvents = false } } } if (canUseDOM) { addBlurListener(window, onBlurWindow) addFocusListener(window, onFocusWindow) addKeyDownListener(document, onKeyDown) addPointerDownListener(document, onPointerish) addPointerMoveListener(document, onPointerish) addVisibilityChangeListener(document, onVisibilityChange) // fallbacks addContextMenuListener(document, onPointerish) addMouseDownListener(document, onPointerish) addMouseMoveListener(document, onPointerish) addMouseUpListener(document, onPointerish) addTouchCancelListener(document, onPointerish) addTouchMoveListener(document, onPointerish) addTouchStartListener(document, onPointerish) addSelectiomChangeListener(document, onPointerish) addScrollListener(document, onPointerish) } function callListeners() { const value = { activeModality, modality } listeners.forEach((listener) => { // @ts-ignore listener(value) }) } export function getActiveModality() { return activeModality as Modality } export function getModality() { return modality as Modality } export function addModalityListener( listener: (arg0: { activeModality: Modality; modality: Modality }) => void ): () => void { listeners.add(listener) return () => { listeners.delete(listener) } } export function testOnly_resetActiveModality() { isEmulatingMouseEvents = false activeModality = KEYBOARD modality = KEYBOARD }