├── .browserslistrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .nvmrc ├── .prettierrc ├── .stylelintrc ├── Gulpfile.js ├── LICENSE ├── README.md ├── dist ├── what-input.js ├── what-input.min.js └── what-input.min.js.map ├── package.json ├── src ├── images │ └── select-arrow.svg ├── markup │ └── index.html ├── scripts │ ├── what-input.d.ts │ └── what-input.js └── styles │ ├── _html.scss │ ├── _layout.scss │ ├── _mixins.scss │ ├── _variables.scss │ └── index.scss └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | last 2 versions 4 | > 1% 5 | ie >= 10 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | 3 | root = true 4 | 5 | # Unix-style newlines with a newline ending every file 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended"], 3 | "parserOptions": { 4 | "ecmaVersion": 2017 5 | }, 6 | 7 | "env": { 8 | "browser": true, 9 | "es6": true, 10 | "node": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | .publish 3 | .idea 4 | node_modules 5 | build 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.15.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-config-prettier"], 3 | "rules": { 4 | "at-rule-no-unknown": [true, { 5 | ignoreAtRules: ['include', 'extend', 'mixin', 'function', 'return', 'for', 'if', 'else', 'warn'] 6 | }], 7 | "no-descending-specificity": null 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * load plugins 3 | */ 4 | 5 | const pkg = require('./package.json') 6 | 7 | const banner = [ 8 | '/**', 9 | ' * <%= pkg.name %> - <%= pkg.description %>', 10 | ' * @version v<%= pkg.version %>', 11 | ' * @link <%= pkg.homepage %>', 12 | ' * @license <%= pkg.license %>', 13 | ' */', 14 | '' 15 | ].join('\n') 16 | 17 | // gulp 18 | const gulp = require('gulp') 19 | 20 | // load all plugins in "devDependencies" into the letiable $ 21 | const $ = require('gulp-load-plugins')({ 22 | pattern: ['*'], 23 | scope: ['devDependencies'] 24 | }) 25 | 26 | /* 27 | * clean task 28 | */ 29 | 30 | function clean() { 31 | return $.del(['**/.DS_Store', './build/*', './dist/*']) 32 | } 33 | 34 | /* 35 | * scripts tasks 36 | */ 37 | 38 | function scripts() { 39 | return gulp 40 | .src(['./src/scripts/what-input.js']) 41 | .pipe($.standard()) 42 | .pipe( 43 | $.standard.reporter('default', { 44 | breakOnError: false, 45 | quiet: true 46 | }) 47 | ) 48 | .pipe( 49 | $.webpackStream({ 50 | module: { 51 | loaders: [ 52 | { 53 | test: /.jsx?$/, 54 | loader: 'babel-loader', 55 | exclude: /node_modules/, 56 | query: { 57 | presets: ['env'] 58 | } 59 | } 60 | ] 61 | }, 62 | output: { 63 | chunkFilename: '[name].js', 64 | library: 'whatInput', 65 | libraryTarget: 'umd', 66 | umdNamedDefine: true 67 | } 68 | }) 69 | ) 70 | .pipe($.rename('what-input.js')) 71 | .pipe($.header(banner, { pkg: pkg })) 72 | .pipe(gulp.dest('./dist/')) 73 | .pipe(gulp.dest('./build/scripts/')) 74 | .pipe($.sourcemaps.init()) 75 | .pipe($.uglify()) 76 | .pipe( 77 | $.rename({ 78 | suffix: '.min' 79 | }) 80 | ) 81 | .pipe($.header(banner, { pkg: pkg })) 82 | .pipe($.sourcemaps.write('./')) 83 | .pipe(gulp.dest('./dist/')) 84 | .pipe($.notify('Build complete')) 85 | } 86 | 87 | /* 88 | * stylesheets 89 | */ 90 | 91 | function styles() { 92 | let processors = [ 93 | $.autoprefixer(), 94 | $.cssMqpacker({ 95 | sort: true 96 | }) 97 | ] 98 | 99 | return gulp 100 | .src(['./src/styles/index.scss']) 101 | .pipe( 102 | $.plumber({ 103 | errorHandler: $.notify.onError('Error: <%= error.message %>') 104 | }) 105 | ) 106 | .pipe($.sourcemaps.init()) 107 | .pipe($.sassGlob()) 108 | .pipe($.sass()) 109 | .pipe($.postcss(processors)) 110 | .pipe( 111 | $.cssnano({ 112 | minifySelectors: false, 113 | reduceIdents: false, 114 | zindex: false 115 | }) 116 | ) 117 | .pipe($.sourcemaps.write('./')) 118 | .pipe(gulp.dest('./build/styles')) 119 | .pipe($.browserSync.stream()) 120 | .pipe($.notify('Styles task complete')) 121 | } 122 | 123 | /* 124 | * images task 125 | */ 126 | 127 | function images() { 128 | return gulp.src(['./src/images/**/*']).pipe(gulp.dest('./build/images')) 129 | } 130 | 131 | /* 132 | * markup task 133 | */ 134 | 135 | function markup() { 136 | return gulp.src(['./src/markup/*']).pipe(gulp.dest('./build')) 137 | } 138 | 139 | /* 140 | * deploy task 141 | */ 142 | 143 | function deploy() { 144 | return gulp.src('./build/**/*').pipe($.ghPages()) 145 | } 146 | 147 | /* 148 | * default task 149 | */ 150 | 151 | function watch() { 152 | $.browserSync.init({ 153 | server: { 154 | baseDir: './build/' 155 | } 156 | }) 157 | 158 | gulp.watch( 159 | ['./src/scripts/what-input.js', './src/scripts/polyfills/*.js'], 160 | scripts, 161 | { events: 'all' }, 162 | function() { 163 | $.browserSync.reload 164 | } 165 | ) 166 | 167 | gulp.watch(['./src/styles/{,*/}{,*/}*.scss'], styles) 168 | 169 | gulp.watch(['./src/markup/*.html'], markup, { events: 'all' }, function() { 170 | $.browserSync.reload 171 | }) 172 | } 173 | 174 | exports.default = gulp.series( 175 | clean, 176 | gulp.parallel(markup, scripts, styles, images), 177 | watch 178 | ) 179 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Jeremy Fields 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What Input? 2 | 3 | **A global utility for tracking the current input method (mouse, keyboard or touch).** 4 | 5 | ## _What Input_ is now v5 6 | 7 | Now with more information and less opinion! 8 | 9 | _What Input_ adds data attributes to the `window` based on the type of input being used. It also exposes a simple API that can be used for scripting interactions. 10 | 11 | ## How it works 12 | 13 | _What Input_ uses event bubbling on the `window` to watch for mouse, keyboard and touch events (via `mousedown`, `keydown` and `touchstart`). It then sets or updates a `data-whatinput` attribute. 14 | 15 | Pointer Events are supported but note that `pen` inputs are remapped to `touch`. 16 | 17 | _What Input_ also exposes a tiny API that allows the developer to ask for the current input, set custom ignore keys, and set and remove custom callback functions. 18 | 19 | _What Input does not make assumptions about the input environment before the page is interacted with._ However, the `mousemove` and `pointermove` events are used to set a `data-whatintent="mouse"` attribute to indicate that a mouse is being used _indirectly_. 20 | 21 | ## Demo 22 | 23 | Check out the demo to see _What Input_ in action. 24 | 25 | https://ten1seven.github.io/what-input 26 | 27 | ### Interacting with Forms 28 | 29 | Since interacting with a form _always_ requires use of the keyboard, _What Input_ uses the `data-whatintent` attribute to display a "buffered" version of input events while form ``s, ` 64 |

65 | 66 |

67 | 68 |

69 | 70 |

71 | 72 |

73 | 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 element. 288 | clearElement() 289 | return 290 | } 291 | 292 | currentElement = event.target.nodeName.toLowerCase() 293 | docElem.setAttribute('data-whatelement', currentElement) 294 | 295 | if (event.target.classList && event.target.classList.length) { 296 | docElem.setAttribute( 297 | 'data-whatclasses', 298 | event.target.classList.toString().replace(' ', ',') 299 | ) 300 | } 301 | } 302 | 303 | const clearElement = () => { 304 | currentElement = null 305 | 306 | docElem.removeAttribute('data-whatelement') 307 | docElem.removeAttribute('data-whatclasses') 308 | } 309 | 310 | const persistInput = (which, value) => { 311 | if (shouldPersist) { 312 | try { 313 | window.sessionStorage.setItem('what-' + which, value) 314 | } catch (e) { 315 | // fail silently 316 | } 317 | } 318 | } 319 | 320 | /* 321 | * utilities 322 | */ 323 | 324 | const pointerType = (event) => { 325 | if (typeof event.pointerType === 'number') { 326 | return pointerMap[event.pointerType] 327 | } else { 328 | // treat pen like touch 329 | return event.pointerType === 'pen' ? 'touch' : event.pointerType 330 | } 331 | } 332 | 333 | // prevent touch detection from being overridden by event execution order 334 | const validateTouch = (value) => { 335 | const timestamp = Date.now() 336 | 337 | const touchIsValid = 338 | value === 'mouse' && 339 | currentInput === 'touch' && 340 | timestamp - currentTimestamp < 200 341 | 342 | currentTimestamp = timestamp 343 | 344 | return touchIsValid 345 | } 346 | 347 | // detect version of mouse wheel event to use 348 | // via https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event 349 | const detectWheel = () => { 350 | let wheelType = null 351 | 352 | // Modern browsers support "wheel" 353 | if ('onwheel' in document.createElement('div')) { 354 | wheelType = 'wheel' 355 | } else { 356 | // Webkit and IE support at least "mousewheel" 357 | // or assume that remaining browsers are older Firefox 358 | wheelType = 359 | document.onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll' 360 | } 361 | 362 | return wheelType 363 | } 364 | 365 | // runs callback functions 366 | const fireFunctions = (type) => { 367 | for (let i = 0, len = functionList.length; i < len; i++) { 368 | if (functionList[i].type === type) { 369 | functionList[i].fn.call( 370 | this, 371 | type === 'input' ? currentInput : currentIntent 372 | ) 373 | } 374 | } 375 | } 376 | 377 | // finds matching element in an object 378 | const objPos = (match) => { 379 | for (let i = 0, len = functionList.length; i < len; i++) { 380 | if (functionList[i].fn === match) { 381 | return i 382 | } 383 | } 384 | } 385 | 386 | const detectScrolling = (event) => { 387 | if (mousePos.x !== event.screenX || mousePos.y !== event.screenY) { 388 | isScrolling = false 389 | 390 | mousePos.x = event.screenX 391 | mousePos.y = event.screenY 392 | } else { 393 | isScrolling = true 394 | } 395 | } 396 | 397 | // manual version of `closest()` 398 | const checkClosest = (elem, tag) => { 399 | const ElementPrototype = window.Element.prototype 400 | 401 | if (!ElementPrototype.matches) { 402 | ElementPrototype.matches = 403 | ElementPrototype.msMatchesSelector || 404 | ElementPrototype.webkitMatchesSelector 405 | } 406 | 407 | if (!ElementPrototype.closest) { 408 | do { 409 | if (elem.matches(tag)) { 410 | return elem 411 | } 412 | 413 | elem = elem.parentElement || elem.parentNode 414 | } while (elem !== null && elem.nodeType === 1) 415 | 416 | return null 417 | } else { 418 | return elem.closest(tag) 419 | } 420 | } 421 | 422 | /* 423 | * init 424 | */ 425 | 426 | // don't start script unless browser cuts the mustard 427 | // (also passes if polyfills are used) 428 | if ('addEventListener' in window && Array.prototype.indexOf) { 429 | setUp() 430 | } 431 | 432 | /* 433 | * api 434 | */ 435 | 436 | return { 437 | // returns string: the current input type 438 | // opt: 'intent'|'input' 439 | // 'input' (default): returns the same value as the `data-whatinput` attribute 440 | // 'intent': includes `data-whatintent` value if it's different than `data-whatinput` 441 | ask: (opt) => { 442 | return opt === 'intent' ? currentIntent : currentInput 443 | }, 444 | 445 | // returns string: the currently focused element or null 446 | element: () => { 447 | return currentElement 448 | }, 449 | 450 | // overwrites ignored keys with provided array 451 | ignoreKeys: (arr) => { 452 | ignoreMap = arr 453 | }, 454 | 455 | // overwrites specific char keys to update on 456 | specificKeys: (arr) => { 457 | specificMap = arr 458 | }, 459 | 460 | // attach functions to input and intent "events" 461 | // funct: function to fire on change 462 | // eventType: 'input'|'intent' 463 | registerOnChange: (fn, eventType) => { 464 | functionList.push({ 465 | fn: fn, 466 | type: eventType || 'input' 467 | }) 468 | }, 469 | 470 | unRegisterOnChange: (fn) => { 471 | const position = objPos(fn) 472 | 473 | if (position || position === 0) { 474 | functionList.splice(position, 1) 475 | } 476 | }, 477 | 478 | clearStorage: () => { 479 | window.sessionStorage.clear() 480 | } 481 | } 482 | })() 483 | -------------------------------------------------------------------------------- /src/styles/_html.scss: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 9 | } 10 | 11 | body { 12 | color: #555; 13 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | a, 19 | button, 20 | input, 21 | select, 22 | textarea { 23 | @include a11y-focus; 24 | } 25 | 26 | a { 27 | color: #337ab7; 28 | text-decoration: none; 29 | 30 | @include hover { 31 | text-decoration: underline; 32 | } 33 | } 34 | 35 | h1 { 36 | border-bottom: 1px solid #eee; 37 | font-size: 36px; 38 | font-weight: 500; 39 | margin: 20px 0 10px; 40 | padding-bottom: 9px; 41 | } 42 | 43 | p { 44 | margin: ($unit * 2) 0; 45 | 46 | @media (min-width: 800px) { 47 | margin: ($unit * 3) 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/styles/_layout.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | box-sizing: content-box; 3 | font-size: 14px; 4 | line-height: 1.4; 5 | margin: 0 auto; 6 | max-width: $container-width; 7 | padding: 0 ($unit * 2); 8 | 9 | @media (min-width: 800px) { 10 | padding: 0 ($unit * 3); 11 | } 12 | } 13 | 14 | .lede { 15 | font-size: 20px; 16 | font-weight: 300; 17 | line-height: ($unit * 4); 18 | 19 | @media (min-width: 800px) { 20 | font-size: 22px; 21 | } 22 | } 23 | 24 | .well { 25 | @include clearfix; 26 | 27 | background-color: #f5f5f5; 28 | border: 1px solid #e3e3e3; 29 | border-radius: 4px; 30 | margin: ($unit * 3) 0; 31 | padding: ($unit * 3); 32 | } 33 | 34 | .well-row { 35 | @media (min-width: 800px) { 36 | display: flex; 37 | margin-left: ($unit * -3); 38 | } 39 | } 40 | 41 | .well-column { 42 | @include null-margins; 43 | 44 | @media (max-width: 799px) { 45 | + .well-column { 46 | border-top: 1px solid #ccc; 47 | margin-top: ($unit * 4); 48 | padding-top: ($unit * 3); 49 | } 50 | } 51 | 52 | @media (min-width: 800px) { 53 | padding-left: ($unit * 3); 54 | width: 50%; 55 | } 56 | 57 | button { 58 | appearance: none; 59 | background-color: #337ab7; 60 | border: 1px solid #2e6da4; 61 | border-radius: 4px; 62 | color: #fff; 63 | cursor: pointer; 64 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 65 | font-size: 14px; 66 | font-weight: 400; 67 | margin: 0; 68 | padding: $unit ($unit * 2); 69 | text-align: center; 70 | transition: all 0.2s ease; 71 | 72 | @include hover { 73 | background-color: #286090; 74 | border-color: #204d74; 75 | } 76 | } 77 | 78 | label { 79 | color: #333; 80 | display: block; 81 | font-size: 14px; 82 | font-weight: 700; 83 | line-height: ($unit * 3); 84 | } 85 | 86 | p { 87 | margin: ($unit * 2) 0; 88 | 89 | &.checkbox label { 90 | font-weight: 400; 91 | } 92 | } 93 | 94 | input { 95 | &:not([type='submit']):not([type='checkbox']):not([type='radio']) { 96 | @include input-style-base; 97 | 98 | box-shadow: inset 0 1px 1px rgba(#000, 0.075); 99 | transition: all 0.2s ease; 100 | 101 | @include placeholder { 102 | color: #999; 103 | } 104 | 105 | [data-whatintent='mouse'] &:focus { 106 | border-color: #31708f; 107 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 108 | 0 0 8px rgba(49, 112, 143, 0.6); 109 | } 110 | 111 | [data-whatinput='touch'] &:focus { 112 | border-color: #8a6d3b; 113 | } 114 | } 115 | } 116 | 117 | select { 118 | @include input-style-base; 119 | 120 | background-image: url(../images/select-arrow.svg); 121 | background-position: calc(100% - 10px) 50%; 122 | background-repeat: no-repeat; 123 | background-size: 10px 6px; 124 | padding-right: 30px; 125 | 126 | [data-whatintent='mouse'] &:focus { 127 | border-color: #31708f; 128 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 129 | 0 0 8px rgba(49, 112, 143, 0.6); 130 | } 131 | 132 | [data-whatinput='touch'] &:focus { 133 | border-color: #8a6d3b; 134 | } 135 | 136 | @media (min-width: 800px) { 137 | min-width: 50%; 138 | width: auto; 139 | } 140 | 141 | // hide arrow in IE 142 | &::-ms-expand { 143 | display: none; 144 | } 145 | } 146 | 147 | textarea { 148 | @include input-style-base; 149 | 150 | box-shadow: inset 0 1px 1px rgba(#000, 0.075); 151 | height: 5em; 152 | padding-bottom: $unit; 153 | padding-top: $unit; 154 | transition: all 0.2s ease; 155 | 156 | [data-whatinput='touch'] &:focus { 157 | border-color: #8a6d3b; 158 | } 159 | 160 | [data-whatintent='mouse'] &:focus { 161 | border-color: #31708f; 162 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 163 | 0 0 8px rgba(49, 112, 143, 0.6); 164 | } 165 | } 166 | } 167 | 168 | .list-group { 169 | list-style: none; 170 | padding: 0; 171 | } 172 | 173 | .list-group-item { 174 | &:first-child a { 175 | border-radius: 4px 4px 0 0; 176 | } 177 | 178 | &:last-child a { 179 | border-bottom: 1px solid #ddd; 180 | border-radius: 0 0 4px 4px; 181 | } 182 | 183 | a { 184 | @include a11y-focus; 185 | 186 | background-color: #fff; 187 | border: 1px solid #ddd; 188 | border-bottom: none; 189 | color: #555; 190 | display: block; 191 | padding: ($unit * 2) ($unit * 2); 192 | text-decoration: none; 193 | transition: all 0.2s ease; 194 | 195 | @include hover { 196 | background-color: #f5f5f5; 197 | } 198 | 199 | &:active, 200 | &:focus { 201 | position: relative; 202 | } 203 | } 204 | } 205 | 206 | footer { 207 | @include clearfix; 208 | 209 | font-size: 14px; 210 | margin: ($unit * 4) 0; 211 | 212 | p { 213 | margin: 0; 214 | } 215 | 216 | .text-love { 217 | color: #a94442; 218 | } 219 | 220 | .pull-left { 221 | float: left; 222 | margin: 0; 223 | } 224 | 225 | .pull-right { 226 | float: right; 227 | margin: 0; 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &::after { 3 | clear: both; 4 | content: ''; 5 | display: table; 6 | } 7 | } 8 | 9 | @mixin null-margins { 10 | > :first-child { 11 | margin-top: 0 !important; 12 | } 13 | 14 | > :last-child { 15 | margin-bottom: 0 !important; 16 | } 17 | } 18 | 19 | @mixin hover { 20 | &:focus, 21 | &:hover { 22 | @content; 23 | } 24 | } 25 | 26 | @mixin a11y-focus { 27 | &:active, 28 | &:focus { 29 | box-shadow: 0 0 0 3px orange !important; 30 | outline: none !important; 31 | } 32 | 33 | [data-whatintent='mouse'] &:active, 34 | [data-whatintent='mouse'] &:focus, 35 | [data-whatintent='touch'] &:active, 36 | [data-whatintent='touch'] &:focus { 37 | box-shadow: none !important; 38 | } 39 | } 40 | 41 | @mixin input-style-base { 42 | appearance: none; 43 | background-color: #fff; 44 | border: 1px solid #ccc; 45 | border-radius: 4px; 46 | color: #555; 47 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 48 | font-size: 16px; 49 | height: ($unit * 4); 50 | margin: 0; 51 | padding: 0 $unit; 52 | width: 100%; 53 | } 54 | 55 | @mixin placeholder { 56 | &::-webkit-input-placeholder { 57 | // Chrome/Opera/Safari 58 | @content; 59 | } 60 | 61 | &::-moz-placeholder { 62 | // Firefox 19+ 63 | @content; 64 | } 65 | 66 | &:-ms-input-placeholder { 67 | // IE 10+ 68 | @content; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $unit: 8px; 2 | $container-width: 1100px; 3 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'mixins'; 3 | @import 'html'; 4 | @import 'layout'; 5 | 6 | /* 7 | * what-input styles 8 | */ 9 | 10 | // indicator 11 | .input-indicator, 12 | .input-intent { 13 | border-radius: 3px; 14 | display: inline-block; 15 | padding: 0 3px; 16 | transition: all 0.2s ease; 17 | } 18 | 19 | [data-whatinput='mouse'] .input-indicator.-mouse, 20 | [data-whatintent='mouse'] .input-intent.-mouse { 21 | background-color: rgba(#337ab7, 0.2); 22 | box-shadow: 0 0 0 1px rgba(#337ab7, 0.3); 23 | color: #337ab7; 24 | } 25 | 26 | [data-whatinput='keyboard'] .input-indicator.-keyboard, 27 | [data-whatintent='keyboard'] .input-intent.-keyboard { 28 | background-color: rgba(orange, 0.1); 29 | box-shadow: 0 0 0 1px rgba(orange, 0.3); 30 | color: orange; 31 | } 32 | 33 | [data-whatinput='touch'] .input-indicator.-touch, 34 | [data-whatintent='touch'] .input-intent.-touch { 35 | background-color: rgba(#8a6d3b, 0.1); 36 | box-shadow: 0 0 0 1px rgba(#8a6d3b, 0.3); 37 | color: #8a6d3b; 38 | } 39 | 40 | // suppress focus outline for mouse and touch 41 | [data-whatintent='mouse'], 42 | [data-whatintent='touch'] { 43 | *:focus { 44 | outline: none; 45 | } 46 | } 47 | 48 | // divs or sections with `tabindex` 49 | html:not([data-whatinput='keyboard']) div:focus { 50 | outline: none; 51 | } 52 | --------------------------------------------------------------------------------