13 |
What Input?
14 |
15 |
16 | A global utility for tracking the current input method (mouse, keyboard, or touch), as well as the current intent (mouse, keyboard, or touch).
17 |
18 |
19 |
20 | Tab, click or tap the links and form controls to see how What Input allows them to be styled differently.
21 |
22 |
23 |
24 |
25 |
26 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
74 |
75 |
76 |
77 |
88 |
89 |
90 |
91 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/src/scripts/what-input.d.ts:
--------------------------------------------------------------------------------
1 | declare const whatInput: {
2 | ask: (strategy?: Strategy) => InputMethod;
3 | element: () => string | null;
4 | ignoreKeys: (keyCodes: number[]) => void;
5 | specificKeys: (keyCodes: number[]) => void;
6 | registerOnChange: (callback: (type: InputMethod) => void, strategy?: Strategy) => void;
7 | unRegisterOnChange: (callback: (type: InputMethod) => void) => void;
8 | clearStorage: () => void;
9 | };
10 |
11 | export type InputMethod = "initial" | "pointer" | "keyboard" | "mouse" | "touch";
12 |
13 | export type Strategy = "input" | "intent";
14 |
15 | export default whatInput;
16 |
--------------------------------------------------------------------------------
/src/scripts/what-input.js:
--------------------------------------------------------------------------------
1 | module.exports = (() => {
2 | /*
3 | * bail out if there is no document or window
4 | * (i.e. in a node/non-DOM environment)
5 | *
6 | * Return a stubbed API instead
7 | */
8 | if (typeof document === 'undefined' || typeof window === 'undefined') {
9 | return {
10 | // always return "initial" because no interaction will ever be detected
11 | ask: () => 'initial',
12 |
13 | // always return null
14 | element: () => null,
15 |
16 | // no-op
17 | ignoreKeys: () => {},
18 |
19 | // no-op
20 | specificKeys: () => {},
21 |
22 | // no-op
23 | registerOnChange: () => {},
24 |
25 | // no-op
26 | unRegisterOnChange: () => {}
27 | }
28 | }
29 |
30 | /*
31 | * variables
32 | */
33 |
34 | // cache document.documentElement
35 | const docElem = document.documentElement
36 |
37 | // currently focused dom element
38 | let currentElement = null
39 |
40 | // last used input type
41 | let currentInput = 'initial'
42 |
43 | // last used input intent
44 | let currentIntent = currentInput
45 |
46 | // UNIX timestamp of current event
47 | let currentTimestamp = Date.now()
48 |
49 | // check for a `data-whatpersist` attribute on either the `html` or `body` elements, defaults to `true`
50 | let shouldPersist = false
51 |
52 | // form input types
53 | const formInputs = ['button', 'input', 'select', 'textarea']
54 |
55 | // empty array for holding callback functions
56 | const functionList = []
57 |
58 | // list of modifier keys commonly used with the mouse and
59 | // can be safely ignored to prevent false keyboard detection
60 | let ignoreMap = [
61 | 16, // shift
62 | 17, // control
63 | 18, // alt
64 | 91, // Windows key / left Apple cmd
65 | 93 // Windows menu / right Apple cmd
66 | ]
67 |
68 | let specificMap = []
69 |
70 | // mapping of events to input types
71 | const inputMap = {
72 | keydown: 'keyboard',
73 | keyup: 'keyboard',
74 | mousedown: 'mouse',
75 | mousemove: 'mouse',
76 | MSPointerDown: 'pointer',
77 | MSPointerMove: 'pointer',
78 | pointerdown: 'pointer',
79 | pointermove: 'pointer',
80 | touchstart: 'touch',
81 | touchend: 'touch'
82 | }
83 |
84 | // boolean: true if the page is being scrolled
85 | let isScrolling = false
86 |
87 | // store current mouse position
88 | const mousePos = {
89 | x: null,
90 | y: null
91 | }
92 |
93 | // map of IE 10 pointer events
94 | const pointerMap = {
95 | 2: 'touch',
96 | 3: 'touch', // treat pen like touch
97 | 4: 'mouse'
98 | }
99 |
100 | // check support for passive event listeners
101 | let supportsPassive = false
102 |
103 | try {
104 | const opts = Object.defineProperty({}, 'passive', {
105 | get: () => {
106 | supportsPassive = true
107 | }
108 | })
109 |
110 | window.addEventListener('test', null, opts)
111 | } catch (e) {
112 | // fail silently
113 | }
114 |
115 | /*
116 | * set up
117 | */
118 |
119 | const setUp = () => {
120 | // add correct mouse wheel event mapping to `inputMap`
121 | inputMap[detectWheel()] = 'mouse'
122 |
123 | addListeners()
124 | }
125 |
126 | /*
127 | * events
128 | */
129 |
130 | const addListeners = () => {
131 | // `pointermove`, `MSPointerMove`, `mousemove` and mouse wheel event binding
132 | // can only demonstrate potential, but not actual, interaction
133 | // and are treated separately
134 | const options = supportsPassive ? { passive: true, capture: true } : true
135 |
136 | document.addEventListener('DOMContentLoaded', setPersist, true)
137 |
138 | // pointer events (mouse, pen, touch)
139 | if (window.PointerEvent) {
140 | window.addEventListener('pointerdown', setInput, true)
141 | window.addEventListener('pointermove', setIntent, true)
142 | } else if (window.MSPointerEvent) {
143 | window.addEventListener('MSPointerDown', setInput, true)
144 | window.addEventListener('MSPointerMove', setIntent, true)
145 | } else {
146 | // mouse events
147 | window.addEventListener('mousedown', setInput, true)
148 | window.addEventListener('mousemove', setIntent, true)
149 |
150 | // touch events
151 | if ('ontouchstart' in window) {
152 | window.addEventListener('touchstart', setInput, options)
153 | window.addEventListener('touchend', setInput, true)
154 | }
155 | }
156 |
157 | // mouse wheel
158 | window.addEventListener(detectWheel(), setIntent, options)
159 |
160 | // keyboard events
161 | window.addEventListener('keydown', setInput, true)
162 | window.addEventListener('keyup', setInput, true)
163 |
164 | // focus events
165 | window.addEventListener('focusin', setElement, true)
166 | window.addEventListener('focusout', clearElement, true)
167 | }
168 |
169 | // checks if input persistence should happen and
170 | // get saved state from session storage if true (defaults to `false`)
171 | const setPersist = () => {
172 | shouldPersist = !(
173 | docElem.getAttribute('data-whatpersist') === 'false' ||
174 | document.body.getAttribute('data-whatpersist') === 'false'
175 | )
176 |
177 | if (shouldPersist) {
178 | // check for session variables and use if available
179 | try {
180 | if (window.sessionStorage.getItem('what-input')) {
181 | currentInput = window.sessionStorage.getItem('what-input')
182 | }
183 |
184 | if (window.sessionStorage.getItem('what-intent')) {
185 | currentIntent = window.sessionStorage.getItem('what-intent')
186 | }
187 | } catch (e) {
188 | // fail silently
189 | }
190 | }
191 |
192 | // always run these so at least `initial` state is set
193 | doUpdate('input')
194 | doUpdate('intent')
195 | }
196 |
197 | // checks conditions before updating new input
198 | const setInput = (event) => {
199 | const eventKey = event.which
200 | let value = inputMap[event.type]
201 |
202 | if (value === 'pointer') {
203 | value = pointerType(event)
204 | }
205 |
206 | const ignoreMatch =
207 | !specificMap.length && ignoreMap.indexOf(eventKey) === -1
208 |
209 | const specificMatch =
210 | specificMap.length && specificMap.indexOf(eventKey) !== -1
211 |
212 | let shouldUpdate =
213 | (value === 'keyboard' && eventKey && (ignoreMatch || specificMatch)) ||
214 | value === 'mouse' ||
215 | value === 'touch'
216 |
217 | // prevent touch detection from being overridden by event execution order
218 | if (validateTouch(value)) {
219 | shouldUpdate = false
220 | }
221 |
222 | if (shouldUpdate && currentInput !== value) {
223 | currentInput = value
224 |
225 | persistInput('input', currentInput)
226 | doUpdate('input')
227 | }
228 |
229 | if (shouldUpdate && currentIntent !== value) {
230 | // preserve intent for keyboard interaction with form fields
231 | const activeElem = document.activeElement
232 | const notFormInput =
233 | activeElem &&
234 | activeElem.nodeName &&
235 | (formInputs.indexOf(activeElem.nodeName.toLowerCase()) === -1 ||
236 | (activeElem.nodeName.toLowerCase() === 'button' &&
237 | !checkClosest(activeElem, 'form')))
238 |
239 | if (notFormInput) {
240 | currentIntent = value
241 |
242 | persistInput('intent', currentIntent)
243 | doUpdate('intent')
244 | }
245 | }
246 | }
247 |
248 | // updates the doc and `inputTypes` array with new input
249 | const doUpdate = (which) => {
250 | docElem.setAttribute(
251 | 'data-what' + which,
252 | which === 'input' ? currentInput : currentIntent
253 | )
254 |
255 | fireFunctions(which)
256 | }
257 |
258 | // updates input intent for `mousemove` and `pointermove`
259 | const setIntent = (event) => {
260 | let value = inputMap[event.type]
261 |
262 | if (value === 'pointer') {
263 | value = pointerType(event)
264 | }
265 |
266 | // test to see if `mousemove` happened relative to the screen to detect scrolling versus mousemove
267 | detectScrolling(event)
268 |
269 | // only execute if scrolling isn't happening
270 | if (
271 | ((!isScrolling && !validateTouch(value)) ||
272 | (isScrolling && event.type === 'wheel') ||
273 | event.type === 'mousewheel' ||
274 | event.type === 'DOMMouseScroll') &&
275 | currentIntent !== value
276 | ) {
277 | currentIntent = value
278 |
279 | persistInput('intent', currentIntent)
280 | doUpdate('intent')
281 | }
282 | }
283 |
284 | const setElement = (event) => {
285 | if (!event.target.nodeName) {
286 | // If nodeName is undefined, clear the element
287 | // This can happen if click inside an