├── .gitignore ├── src ├── index.ts ├── shared.ts ├── tap.ts └── swipe.ts ├── CHANGELOG.md ├── rollup.config.js ├── scripts └── move-type-declarations.js ├── tsconfig.json ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.d.ts 4 | dist -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tap'; 2 | export * from './swipe'; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # TODO changelog 2 | 3 | ## 1.0.0 4 | 5 | * First release -------------------------------------------------------------------------------- /src/shared.ts: -------------------------------------------------------------------------------- 1 | export function add(node: EventTarget, event: string, handler: EventListener) { 2 | node.addEventListener(event, handler); 3 | return () => node.removeEventListener(event, handler); 4 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import sucrase from 'rollup-plugin-sucrase'; 2 | import pkg from './package.json'; 3 | 4 | export default { 5 | input: 'src/index.ts', 6 | output: [ 7 | { file: pkg.main, format: 'cjs' }, 8 | { file: pkg.module, format: 'esm' } 9 | ], 10 | plugins: [ 11 | sucrase({ 12 | transforms: ['typescript'] 13 | }) 14 | ] 15 | }; -------------------------------------------------------------------------------- /scripts/move-type-declarations.js: -------------------------------------------------------------------------------- 1 | const sander = require('sander'); 2 | const glob = require('tiny-glob/sync'); 3 | 4 | for (const file of glob('src/**/*.js')) { 5 | sander.unlinkSync(file); 6 | } 7 | 8 | sander.rimrafSync('types'); 9 | for (const file of glob('src/**/*.d.ts')) { 10 | sander.renameSync(file).to(file.replace(/^src/, 'types')); 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "diagnostics": true, 5 | "noImplicitThis": true, 6 | "noEmitOnError": true, 7 | "lib": ["es5", "es6", "dom"] 8 | }, 9 | "target": "ES5", 10 | "module": "ES6", 11 | "include": [ 12 | "src" 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sveltejs/gestures", 3 | "description": "Svelte actions for cross-platform gesture detection", 4 | "version": "0.0.1", 5 | "repository": "sveltejs/gestures", 6 | "main": "dist/gestures.js", 7 | "module": "dist/gestures.mjs", 8 | "types": "types/index.d.ts", 9 | "files": [ 10 | "dist", 11 | "types" 12 | ], 13 | "devDependencies": { 14 | "@types/node": "^10.9.4", 15 | "rollup": "^0.65.2", 16 | "rollup-plugin-sucrase": "^2.1.0", 17 | "sander": "^0.6.0", 18 | "tiny-glob": "^0.2.6", 19 | "typescript": "^3.3.3333" 20 | }, 21 | "scripts": { 22 | "build-declarations": "tsc -d && node scripts/move-type-declarations.js", 23 | "build": "npm run build-declarations && rollup -c", 24 | "dev": "rollup -cw", 25 | "test": "echo \"no tests. open the demos in a browser instead\"", 26 | "prepublishOnly": "npm run build" 27 | }, 28 | "license": "LIL" 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Rich Harris 2 | 3 | Permission is hereby granted by the authors of this software, to any person, to use the software for any purpose, free of charge, including the rights to run, read, copy, change, distribute and sell it, and including usage rights to any patents the authors may hold on it, subject to the following conditions: 4 | 5 | This license, or a link to its text, must be included with all copies of the software and any derivative works. 6 | 7 | Any modification to the software submitted to the authors may be incorporated into the software under the terms of this license. 8 | 9 | The software is provided "as is", without warranty of any kind, including but not limited to the warranties of title, fitness, merchantability and non-infringement. The authors have no obligation to provide support or updates for the software, and may not be held liable for any damages, claims or other liability arising from its use. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @sveltejs/gestures 2 | 3 | A (work-in-progress) collection of gesture recognisers for Svelte components. 4 | 5 | Each recogniser is implemented as an action that emits custom events. Pointer events are used where possible, falling back to mouse and touch events. 6 | 7 | 8 | ## tap ([demo](https://v3.svelte.technology/repl?version=3.0.0-beta.10&gist=ffbdb659f2c52c8510bec42af3ffb0d1)) 9 | 10 | This action fires a `tap` event when the user taps on an element with either a mouse or a finger (or other pointing device). If the pointer is down for more than 300ms, it doesn't count, unlike with `click` events. 11 | 12 | Pressing the spacebar on a focused button will also fire a `tap` event. Taps on disabled form elements are disregarded. 13 | 14 | The `event.detail` object has `x` and `y` properties corresponding to `clientX` and `clientY`. If the original event was a spacebar keypress, both are `null`. 15 | 16 | ```html 17 | 24 | 25 | 28 | ``` 29 | 30 | ## swipe 31 | 32 | This action include three events `swipe`, `swipestart` and `swipeend`. The `swipestart` event 33 | will fire when the pointer is down and the `swipeend` event is fired when the pointer is up, if after 300ms the pointermove event is not fired, the `swipe` and `swipeend` events are canceled. 34 | 35 | The `swipe` event will be fired if a minimal `TRESHOLD` distance is accomplished. This event will include in `event.detail` a `direction` string, which can be one of `left`, `right`, `up` and `down`, and a `distance` number. 36 | 37 | ```html 38 | 47 | 48 | 53 | 54 |
active = true} 58 | on:swipeend={e => active = false} 59 | on:swipe={handler} 60 | >
61 | ``` 62 | 63 | TODO: `pan`, `rotate`, `pinch`, `press` 64 | 65 | ## License 66 | 67 | [LIL](LICENSE) -------------------------------------------------------------------------------- /src/tap.ts: -------------------------------------------------------------------------------- 1 | import { add } from "./shared"; 2 | 3 | function dispatch_tap(node: EventTarget, x: number, y: number) { 4 | node.dispatchEvent(new CustomEvent('tap', { 5 | detail: { x, y } 6 | })); 7 | } 8 | 9 | function handle_focus(event: FocusEvent) { 10 | const remove_keydown_handler = add(event.currentTarget, 'keydown', (event: KeyboardEvent) => { 11 | if (event.which === 32) dispatch_tap(event.currentTarget, null, null); 12 | }); 13 | 14 | const remove_blur_handler = add(event.currentTarget, 'blur', (event: KeyboardEvent) => { 15 | remove_keydown_handler(); 16 | remove_blur_handler(); 17 | }); 18 | } 19 | 20 | function is_button(node: HTMLButtonElement | HTMLInputElement) { 21 | return node.tagName === 'BUTTON' || node.type === 'button'; 22 | } 23 | 24 | function tap_pointer(node: EventTarget) { 25 | function handle_pointerdown(event: PointerEvent) { 26 | if ((node as HTMLButtonElement).disabled) return; 27 | const { clientX, clientY } = event; 28 | 29 | const remove_pointerup_handler = add(node, 'pointerup', (event: PointerEvent) => { 30 | if (Math.abs(event.clientX - clientX) > 5) return; 31 | if (Math.abs(event.clientY - clientY) > 5) return; 32 | 33 | dispatch_tap(node, event.clientX, event.clientY); 34 | remove_pointerup_handler(); 35 | }); 36 | 37 | setTimeout(remove_pointerup_handler, 300); 38 | } 39 | 40 | const remove_pointerdown_handler = add(node, 'pointerdown', handle_pointerdown); 41 | const remove_focus_handler = is_button(node as HTMLButtonElement) && add(node, 'focus', handle_focus); 42 | 43 | return { 44 | destroy() { 45 | remove_pointerdown_handler(); 46 | remove_focus_handler && remove_focus_handler(); 47 | } 48 | }; 49 | } 50 | 51 | function tap_legacy(node: EventTarget) { 52 | let mouse_enabled = true; 53 | let mouse_timeout: NodeJS.Timeout; 54 | 55 | function handle_mousedown(event: MouseEvent) { 56 | const { clientX, clientY } = event; 57 | 58 | const remove_mouseup_handler = add(node, 'mouseup', (event: MouseEvent) => { 59 | if (!mouse_enabled) return; 60 | if (Math.abs(event.clientX - clientX) > 5) return; 61 | if (Math.abs(event.clientY - clientY) > 5) return; 62 | 63 | dispatch_tap(node, event.clientX, event.clientY); 64 | remove_mouseup_handler(); 65 | }); 66 | 67 | clearTimeout(mouse_timeout); 68 | setTimeout(remove_mouseup_handler, 300); 69 | } 70 | 71 | function handle_touchstart(event: TouchEvent) { 72 | if (event.changedTouches.length !== 1) return; 73 | if ((node as HTMLButtonElement).disabled) return; 74 | 75 | const touch = event.changedTouches[0]; 76 | const { identifier, clientX, clientY } = touch; 77 | 78 | const remove_touchend_handler = add(node, 'touchend', (event: TouchEvent) => { 79 | const touch = Array.from(event.changedTouches).find(t => t.identifier === identifier); 80 | if (!touch) return; 81 | 82 | if (Math.abs(touch.clientX - clientX) > 5) return; 83 | if (Math.abs(touch.clientY - clientY) > 5) return; 84 | 85 | dispatch_tap(node, touch.clientX, touch.clientY); 86 | 87 | mouse_enabled = false; 88 | mouse_timeout = setTimeout(() => { 89 | mouse_enabled = true; 90 | }, 350); 91 | }); 92 | 93 | setTimeout(remove_touchend_handler, 300); 94 | } 95 | 96 | const remove_mousedown_handler = add(node, 'mousedown', handle_mousedown); 97 | const remove_touchstart_handler = add(node, 'touchstart', handle_touchstart); 98 | const remove_focus_handler = is_button(node as HTMLButtonElement) && add(node, 'focus', handle_focus); 99 | 100 | return { 101 | destroy() { 102 | remove_mousedown_handler(); 103 | remove_touchstart_handler(); 104 | remove_focus_handler && remove_focus_handler(); 105 | } 106 | }; 107 | } 108 | 109 | export const tap = typeof PointerEvent === 'function' 110 | ? tap_pointer 111 | : tap_legacy; -------------------------------------------------------------------------------- /src/swipe.ts: -------------------------------------------------------------------------------- 1 | import { add } from "./shared"; 2 | 3 | function dispatch_swipe(node: EventTarget, direction: string, distance: number) { 4 | node.dispatchEvent(new CustomEvent('swipe', { 5 | detail: { direction, distance } 6 | })); 7 | } 8 | 9 | function dispatch_swipe_start(node: EventTarget) { 10 | node.dispatchEvent(new CustomEvent('swipestart')); 11 | } 12 | 13 | function dispatch_swipe_end(node: EventTarget) { 14 | node.dispatchEvent(new CustomEvent('swipeend')); 15 | } 16 | 17 | const TRESHOLD = 0 18 | 19 | function swipe_pointer(node: EventTarget) { 20 | function handle_pointer_down(event: PointerEvent) { 21 | dispatch_swipe_start(node); 22 | const remove_pointerup_handler = add(node, 'pointerup', (event: PointerEvent) => { 23 | dispatch_swipe_end(node); 24 | remove_pointerup_handler(); 25 | }) 26 | const remove_pointermove_handler = add(node, 'pointermove', (event: PointerEvent) => { 27 | if (Math.abs(event.movementX) > TRESHOLD) { 28 | dispatch_swipe(node, event.movementX > 0 ? 'right' : 'left', event.movementX); 29 | } else if (Math.abs(event.movementY) > TRESHOLD) { 30 | dispatch_swipe(node, event.movementY > 0 ? 'down' : 'up', event.movementY); 31 | } 32 | remove_pointermove_handler(); 33 | }) 34 | setTimeout(remove_pointermove_handler, 300); 35 | } 36 | 37 | const remove_pointerdown_handler = add(node, 'pointerdown', handle_pointer_down); 38 | 39 | return { 40 | destroy() { 41 | remove_pointerdown_handler(); 42 | } 43 | } 44 | } 45 | 46 | function swipe_legacy(node: EventTarget) { 47 | function handle_mousedown(event: MouseEvent) { 48 | dispatch_swipe_start(node); 49 | const remove_mouseup_handler = add(node, 'mouseup', (event: MouseEvent) => { 50 | dispatch_swipe_end(node); 51 | remove_mouseup_handler(); 52 | }) 53 | 54 | const remove_mousemove_handler = add(node, 'mousemve', (event: MouseEvent) => { 55 | if (Math.abs(event.movementX) > TRESHOLD) { 56 | dispatch_swipe(node, event.movementX > 0 ? 'right' : 'left', event.movementX); 57 | } else if (Math.abs(event.movementY) > TRESHOLD) { 58 | dispatch_swipe(node, event.movementY > 0 ? 'down' : 'up', event.movementY); 59 | } 60 | remove_mousemove_handler(); 61 | }) 62 | setTimeout(remove_mousemove_handler, 300); 63 | } 64 | 65 | function handle_touchstart(event: TouchEvent) { 66 | if (event.changedTouches.length !== 1) return 67 | 68 | event.preventDefault(); 69 | dispatch_swipe_start(node); 70 | 71 | const touch = event.changedTouches[0]; 72 | const { identifier, clientX, clientY } = touch; 73 | 74 | const remove_touchend_handler = add(node, 'touchend', (event: TouchEvent) => { 75 | event.preventDefault(); 76 | dispatch_swipe_end(node); 77 | remove_touchend_handler(); 78 | }) 79 | 80 | const remove_touchmove_handler = add(node, 'touchmove', (event: TouchEvent) => { 81 | const touch = Array.from(event.changedTouches).find(t => t.identifier === identifier); 82 | if (!touch) return 83 | 84 | if (Math.abs(touch.clientX - clientX) > TRESHOLD) { 85 | dispatch_swipe(node, touch.clientX - clientX > 0 ? 'right' : 'left', touch.clientX - clientX); 86 | } else if (Math.abs(touch.clientY - clientY) > TRESHOLD) { 87 | dispatch_swipe(node, touch.clientY - clientY > 0 ? 'down' : 'up', touch.clientY - clientY); 88 | } 89 | event.preventDefault(); 90 | remove_touchmove_handler(); 91 | }) 92 | setTimeout(remove_touchmove_handler, 300); 93 | } 94 | 95 | const remove_mousedown_handler = add(node, 'mousedown', handle_mousedown); 96 | const remove_touchstart_handler = add(node, 'touchstart', handle_touchstart); 97 | 98 | return { 99 | destroy() { 100 | remove_mousedown_handler(); 101 | remove_touchstart_handler(); 102 | } 103 | } 104 | } 105 | 106 | export const swipe = typeof PointerEvent === 'function' 107 | ? swipe_pointer 108 | : swipe_legacy --------------------------------------------------------------------------------