├── .gitignore ├── assets ├── demo-touch.gif └── demo-touch-simulated.gif ├── .editorconfig ├── package.json ├── README.md └── src └── TouchableDock.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | !dist/index.html 4 | dist 5 | .env 6 | -------------------------------------------------------------------------------- /assets/demo-touch.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimDaub/preact-touchable-dock/HEAD/assets/demo-touch.gif -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,yml,json,cpp,cc,h,html,md,sh,mjml,css}] 2 | indent_style = space 3 | indent_size = 2 4 | -------------------------------------------------------------------------------- /assets/demo-touch-simulated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TimDaub/preact-touchable-dock/HEAD/assets/demo-touch-simulated.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-touchable-dock", 3 | "version": "0.3.5", 4 | "description": "A touch and drag and droppable dock for single page web applications.", 5 | "source": "src/TouchableDock.js", 6 | "main": "dist/TouchableDock.js", 7 | "module": "dist/TouchableDock.module.js", 8 | "unpkg": "dist/TouchableDock.umd.js", 9 | "scripts": { 10 | "build:iife": "microbundle build --name TouchableDock -f iife -o dist/TouchableDock.min.js", 11 | "build": "microbundle build --name TouchableDock && npm run build:iife", 12 | "dev": "concurrently 'npm run watch' 'npm run serve'", 13 | "watch": "watch 'npm run build' ./src/ -d", 14 | "serve": "python3 -m http.server --directory dist" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+ssh://git@github.com/TimDaub/preact-touchable-dock.git" 19 | }, 20 | "keywords": [ 21 | "preact", 22 | "dock", 23 | "sidebar", 24 | "touch", 25 | "drag", 26 | "and", 27 | "drop" 28 | ], 29 | "author": "Tim Daubenschütz (https://timdaub.github.io)", 30 | "bugs": { 31 | "url": "https://github.com/TimDaub/preact-touchable-dock/issues" 32 | }, 33 | "homepage": "https://github.com/TimDaub/preact-touchable-dock#readme", 34 | "devDependencies": { 35 | "concurrently": "5.2.0", 36 | "microbundle": "0.12.3", 37 | "watch": "1.0.2" 38 | }, 39 | "dependencies": { 40 | "htm": "3.0.4", 41 | "jss": "10.3.0", 42 | "jss-preset-default": "10.3.0", 43 | "preact": "10.4.6" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # preact-touchable-dock 2 | [![npm version](https://badge.fury.io/js/preact-touchable-dock.svg)](https://badge.fury.io/js/preact-touchable-dock) 3 | 4 | > A touch and drag and droppable dock for single page web applications. 5 | 6 | Mobile|Simulated Mobile 7 | :-------------------------:|:-------------------------: 8 | ![](./assets/demo-touch.gif) | ![](./assets/demo-touch-simulated.gif) 9 | 10 | ## Installation 11 | 12 | ```bash 13 | $ npm i --save preact-touchable-dock 14 | # or 15 | $ yarn add preact-touchable-dock 16 | ``` 17 | 18 | ## Usage 19 | 20 | - [Demo](https://jsfiddle.net/bkcu1qfj/1/) 21 | 22 | ```html 23 | 24 | 25 | 26 | 27 | Touchable Dock Demo 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 77 | 78 | 79 | 80 | 81 | ``` 82 | 83 | ### Notes 84 | 85 | - `TouchableDock` inserts inline classes via [JSS](https://cssinjs.org). This 86 | allows users to customize its style by adjusting classes like `.touchableDock` 87 | and `.touchableDockHandle`. 88 | - Changing the dock's stage works by calling the `setStage` method through a 89 | ref. Possible values are `["hide", "hint", "full"]`. 90 | - `props.onClose` allows to listen for close events emitted from the dock. 91 | 92 | ## Contributing 93 | 94 | ```bash 95 | $ git clone git@github.com:TimDaub/preact-touchable-dock.git 96 | $ cd preact-touchable-dock 97 | $ npm i 98 | $ npm run dev 99 | ``` 100 | 101 | ## Changelog 102 | 103 | ### 0.3.5 104 | 105 | - Bug fix: Don't allow scrolling of body in `stage === full` 106 | 107 | ### 0.3.4 108 | 109 | - Added `onClick` prop 110 | 111 | ### 0.3.3 112 | 113 | - Forgot to update the build lol 114 | 115 | ### 0.3.2 116 | 117 | - Bug fix: `Uncaught ReferenceError: pageY is not defined` 118 | 119 | ### 0.3.1 120 | 121 | - Bug fix: Allow other components to receive touch and mouse movement event by 122 | conditionally applying `evt.preventDefault` 123 | - Bug fix: Allow adjusting dock's height in scrolled position 124 | 125 | ### 0.3.0 126 | 127 | - Unmount children when component is in `stage === "hide"` to allow usage of 128 | `componentWillUnmount` in child 129 | 130 | ### 0.2.2 131 | 132 | - Add `onClose` prop to component for listening to close events. 133 | 134 | ### 0.2.1 135 | 136 | - Add closing action button 137 | 138 | ### 0.2.0 139 | 140 | - Deprecate changing `stage` through props and allow only through new method 141 | called `setStage` 142 | 143 | ### 0.1.0 144 | 145 | - Deliver CSS classes as JS-generated inline classes using JSS 146 | 147 | ### 0.0.1 148 | 149 | - Initial release 150 | -------------------------------------------------------------------------------- /src/TouchableDock.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from "preact"; 2 | import htm from "htm"; 3 | import jss from "jss"; 4 | import preset from "jss-preset-default"; 5 | 6 | const html = htm.bind(h); 7 | jss.setup({ ...preset(), ...{ createGenerateId: () => ({ key }) => key } }); 8 | 9 | const transitionSpeed = ".1s"; 10 | 11 | const styles = { 12 | touchableDock: ` 13 | width: 100%; 14 | position: fixed; 15 | bottom: 0; 16 | left: 0; 17 | z-index:9999; 18 | `, 19 | 20 | touchableDockHandle: { 21 | display: "flex", 22 | justifyContent: "center", 23 | cursor: "pointer", 24 | height: "50px", 25 | "&::before": ` 26 | content: ""; 27 | height: 10px; 28 | width: 50px; 29 | background-color: grey; 30 | margin-top: 20px; 31 | ` 32 | }, 33 | closeAction: ` 34 | font-family: Arial, sans-serif; 35 | font-size: 2.5em; 36 | font-weight: 300; 37 | 38 | position: absolute; 39 | top: 0; 40 | left: 0; 41 | margin: 5px 0 0 15px; 42 | color: #424242; 43 | line-height: 1; 44 | ` 45 | }; 46 | 47 | const { classes } = jss.createStyleSheet(styles).attach(); 48 | 49 | class TouchableDock extends Component { 50 | // TODO: 51 | // - Add a close button to the left 52 | // - When clicked on any content, expand to full 53 | constructor(props) { 54 | super(props); 55 | 56 | this.state = { 57 | height: 0, 58 | mouseDown: false, 59 | touch: false, 60 | stage: "hide" 61 | }; 62 | 63 | this.handleMovement = this.handleMovement.bind(this); 64 | this.setStage = this.setStage.bind(this); 65 | } 66 | componentDidMount() { 67 | // NOTE: If we had added the event listeners only to the handle component, 68 | // they wouldn't trigger on any other component outside the handle. However, 69 | // we want them to trigger on the whole document. 70 | document.addEventListener("mouseup", () => 71 | this.setState({ mouseDown: false }) 72 | ); 73 | document.addEventListener("touchend", () => 74 | this.setState({ touch: false }) 75 | ); 76 | document.addEventListener("mousemove", this.handleMovement); 77 | document.addEventListener("touchmove", this.handleMovement, { 78 | passive: false 79 | }); 80 | } 81 | 82 | setStage(stage) { 83 | const { onClose } = this.props; 84 | if (stage === "hide" && onClose && typeof onClose === "function") { 85 | onClose(); 86 | } 87 | // NOTE: Idea from https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/ 88 | // with slight modifications/corrections 89 | if (stage === "full") { 90 | const scrollY = window.scrollY; 91 | document.body.style.position = 'fixed'; 92 | document.body.style.top = `-${scrollY}px`; 93 | } else { 94 | const scrollY = document.body.style.top; 95 | document.body.style.position = ''; 96 | document.body.style.top = ''; 97 | window.scrollTo(0, parseInt(scrollY || '0') * -1); 98 | } 99 | this.setState({ height: 0, stage }); 100 | } 101 | 102 | componentDidUpdate(prevProps, prevState) { 103 | const { height, mouseDown, stage, touch } = this.state; 104 | const newHeight = parseFloat(height); 105 | if (!mouseDown && !touch) { 106 | if (newHeight > 35 && newHeight !== 100) { 107 | this.setStage("full"); 108 | } 109 | 110 | if (newHeight > 0 && stage === "full" && newHeight !== 0) { 111 | this.setStage("hide"); 112 | } 113 | 114 | if (newHeight < 35 && newHeight !== 0) { 115 | this.setStage("hide"); 116 | } 117 | } 118 | } 119 | screenHeight() { 120 | return Math.max( 121 | document.documentElement.clientHeight || 0, 122 | window.innerHeight || 0 123 | ); 124 | } 125 | calcHeight(y, screenHeight) { 126 | const height = ((screenHeight - y) / screenHeight) * 100; 127 | this.setState({ 128 | height: height + "%" 129 | }); 130 | } 131 | handleMovement(evt) { 132 | const { touch, mouseDown } = this.state; 133 | 134 | // NOTE: We depend on `clientY` here, as it gives us the user's cursor 135 | // position relative to the viewport. It allows the user to adjust the 136 | // dock's position even when having scrolled on the page. 137 | let y; 138 | if (touch && evt.touches && evt.touches.length > 0) { 139 | evt.preventDefault(); 140 | y = evt.touches[0].clientY; 141 | } else if (mouseDown) { 142 | y = evt.clientY; 143 | } else { 144 | // NOTE: There can be cases where the mouse is moved, but without prior 145 | // click on the component. In this case we don't want to move at all. 146 | return; 147 | } 148 | this.calcHeight(y, this.screenHeight()); 149 | } 150 | render() { 151 | const { children } = this.props; 152 | let { style, onClose, onClick } = this.props; 153 | const { height, mouseDown, touch, stage } = this.state; 154 | let defaultHeight; 155 | 156 | if (stage === "hide") { 157 | defaultHeight = "0px"; 158 | } else if (stage === "hint") { 159 | defaultHeight = "35%"; 160 | } else { 161 | defaultHeight = "100%"; 162 | } 163 | 164 | style = parseFloat(height) 165 | ? { ...style, height } 166 | : { ...style, ...{ height: defaultHeight } }; 167 | 168 | style = !(mouseDown || touch) 169 | ? { ...style, ...{ transition: `height ${transitionSpeed} ease-in` } } 170 | : style; 171 | 172 | // NOTE: If we set the dock to bottom: 0 permanently and the user added a 173 | // border, this border would show up in hidden mode as the border extends 174 | // outside of an element's box. Hence, we set it it -10% to make sure it's 175 | // of display while still preserving the height transition. 176 | style = stage === "hide" ? { ...style, ...{ bottom: "-10%" } } : style; 177 | 178 | return html` 179 |
183 |
this.setState({ mouseDown: true })} 186 | onTouchStart=${() => this.setState({ touch: true })}> 187 | { 190 | // NOTE: We call `stopPropagation` here to avoid firing an 191 | // additional `onClick` event. 192 | evt.stopPropagation(); 193 | this.setStage("hide"); 194 | }}> 195 | × 196 | 197 |
198 | 200 | ${stage !== "hide" ? children : null} 201 |
202 | `; 203 | } 204 | } 205 | 206 | export default TouchableDock; 207 | --------------------------------------------------------------------------------