├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── github-light.css ├── nipplejs.jpg ├── nipplejs.png └── styles.css ├── bin ├── changelog.js └── copyToGhPages.js ├── example ├── codepen-demo.html ├── dual-joysticks.html ├── iframe.html ├── lock-axes.html ├── reset-joystick.html └── square-dual-joysticks.html ├── package-lock.json ├── package.json ├── src ├── collection.js ├── index.js ├── manager.js ├── nipple.js ├── super.js └── utils.js ├── test └── nipplejs.casper.js ├── types └── index.d.ts └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "browsers": ["last 2 versions", "safari >= 7"] 8 | }, 9 | "modules": false 10 | } 11 | ] 12 | ], 13 | "plugins": ["add-module-exports"] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "no-unused-vars": [ 14 | "error", 15 | { "args": "none" } 16 | ], 17 | "indent": [ 18 | "error", 19 | 4 20 | ], 21 | "linebreak-style": [ 22 | "error", 23 | "unix" 24 | ], 25 | "quotes": [ 26 | "error", 27 | "single" 28 | ], 29 | "semi": [ 30 | "error", 31 | "always" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots, Codepen or JSFiddle** 24 | If applicable, add screenshots, codepen or jsfiddle to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEAT]" 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Proposed changes 2 | 3 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 4 | 5 | ## Types of changes 6 | 7 | What types of changes does your code introduce to NippleJS? 8 | _Put an `x` in the boxes that apply_ 9 | 10 | - [ ] Bugfix (non-breaking change which fixes an issue) 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 13 | 14 | ## Checklist 15 | 16 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 17 | 18 | - [ ] I have read the [CONTRIBUTING](../../CONTRIBUTING.md) doc 19 | - [ ] Lint and tests pass locally with my changes 20 | - [ ] I have added necessary documentation (if appropriate) 21 | 22 | ## Further comments 23 | 24 | If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc... 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .vscode 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.10.1](https://github.com/yoannmoinet/nipplejs/compare/v0.10.0...v0.10.1) (2023-03-06) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **manager:** add `ids` & `id` to types ([#207](https://github.com/yoannmoinet/nipplejs/issues/207)) ([6b7b6dc](https://github.com/yoannmoinet/nipplejs/commit/6b7b6dceb7e876fbc0f3c12b8e7e9069353079cf)) 7 | 8 | 9 | 10 | # [0.10.0](https://github.com/yoannmoinet/nipplejs/compare/v0.9.1...v0.10.0) (2022-10-10) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * missing type for `setPosition` ([#194](https://github.com/yoannmoinet/nipplejs/issues/194)) ([3a2e495](https://github.com/yoannmoinet/nipplejs/commit/3a2e495873ae786d1502e759b2dcaa17a3bc0835)) 16 | * use transform translate for nipple front ([#187](https://github.com/yoannmoinet/nipplejs/issues/187)) ([52e5bd2](https://github.com/yoannmoinet/nipplejs/commit/52e5bd2c5498328478ab2e5db2c65714f5a93f53)) 17 | 18 | 19 | 20 | ## [0.9.1](https://github.com/yoannmoinet/nipplejs/compare/v0.9.0...v0.9.1) (2022-03-26) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * **zone:** handle offset when parent is flex ([#182](https://github.com/yoannmoinet/nipplejs/issues/182)) ([ef9ec5a](https://github.com/yoannmoinet/nipplejs/commit/ef9ec5a7ca2680ad233b4d7fccc71f6c025aca50)) 26 | 27 | 28 | ### Features 29 | 30 | * **improvement:** restJoystick now accepts an object ([#162](https://github.com/yoannmoinet/nipplejs/issues/162)) ([ce3a67d](https://github.com/yoannmoinet/nipplejs/commit/ce3a67dbfa7b64c5de9ff4a74c008d46e42f5194)) 31 | 32 | 33 | 34 | # [0.9.0](https://github.com/yoannmoinet/nipplejs/compare/v0.8.7...v0.9.0) (2021-02-24) 35 | 36 | 37 | ### Features 38 | 39 | * **follow:** Add option to follow thumbstick ([#160](https://github.com/yoannmoinet/nipplejs/issues/160)) ([5c4c002](https://github.com/yoannmoinet/nipplejs/commit/5c4c0027b73fa25b5410d478199844a5277c1e76)) 40 | 41 | 42 | 43 | ## [0.8.7](https://github.com/yoannmoinet/nipplejs/compare/v0.8.6...v0.8.7) (2020-09-06) 44 | 45 | 46 | 47 | ## [0.8.6](https://github.com/yoannmoinet/nipplejs/compare/v0.8.3...v0.8.6) (2020-09-06) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * add missing type for `dynamicPage` ([cc97650](https://github.com/yoannmoinet/nipplejs/commit/cc97650db299b0a3b5db4da0a98a561825904dc7)) 53 | 54 | 55 | ### Features 56 | 57 | * add an options to use with dynamic pages ([8258501](https://github.com/yoannmoinet/nipplejs/commit/8258501721b9972a9b2206a9a67224a200cdf83b)) 58 | * Add shape option to joystick ([#107](https://github.com/yoannmoinet/nipplejs/issues/107)) ([5a8f8ba](https://github.com/yoannmoinet/nipplejs/commit/5a8f8bacea3776630a270a25dbbd760c9451bb55)) 59 | * add unit vector representation to move event output ([a4496fe](https://github.com/yoannmoinet/nipplejs/commit/a4496fee62ca59f5097e972faf696e4e17a33829)) 60 | 61 | 62 | 63 | ## [0.8.3](https://github.com/yoannmoinet/nipplejs/compare/v0.8.2...v0.8.3) (2019-08-24) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * better lockX and lockY mecanism ([8355fb8](https://github.com/yoannmoinet/nipplejs/commit/8355fb8dd884541e2c1c4f5ad0f69ee2674be1ca)), closes [#111](https://github.com/yoannmoinet/nipplejs/issues/111) 69 | * released outside will be considered inside ([9a8a7b4](https://github.com/yoannmoinet/nipplejs/commit/9a8a7b4da42e9342621560a0588f580a31dbb419)), closes [#109](https://github.com/yoannmoinet/nipplejs/issues/109) 70 | * reset direction when below the threshold ([6f41b38](https://github.com/yoannmoinet/nipplejs/commit/6f41b38e1c6b06b6bf0fe86ba4d5a4668689dc0d)), closes [#110](https://github.com/yoannmoinet/nipplejs/issues/110) 71 | 72 | 73 | 74 | ## [0.8.2](https://github.com/yoannmoinet/nipplejs/compare/v0.8.1...v0.8.2) (2019-05-08) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * allow older typescript ([c164597](https://github.com/yoannmoinet/nipplejs/commit/c164597d5cc8d18f4ed2553a76554b1995d78e97)) 80 | * joystick in 'dynamic' mode gets stuck on iOS ([94bb784](https://github.com/yoannmoinet/nipplejs/commit/94bb784fecc5eeb5a7fcf03675fa074046fd9663)), closes [#94](https://github.com/yoannmoinet/nipplejs/issues/94) 81 | 82 | 83 | ### Features 84 | 85 | * add raw data on move for data-only usage ([c818fb2](https://github.com/yoannmoinet/nipplejs/commit/c818fb2daffb7333e919f427ae53cb5ee9fce047)), closes [#54](https://github.com/yoannmoinet/nipplejs/issues/54) 86 | 87 | 88 | 89 | ## [0.8.1](https://github.com/yoannmoinet/nipplejs/compare/v0.7.3...v0.8.1) (2019-02-17) 90 | 91 | 92 | ### Features 93 | 94 | * upgrade dev experience with webpack and es6 ([cc1e824](https://github.com/yoannmoinet/nipplejs/commit/cc1e824c32527a2c85f8b053d7134317b6e93563)) 95 | 96 | 97 | 98 | ## [0.7.3](https://github.com/yoannmoinet/nipplejs/compare/v0.7.2...v0.7.3) (2018-12-11) 99 | 100 | 101 | 102 | ## [0.7.2](https://github.com/yoannmoinet/nipplejs/compare/v0.7.1...v0.7.2) (2018-12-11) 103 | 104 | 105 | ### Features 106 | 107 | * Add TypeScript Definitions ([#85](https://github.com/yoannmoinet/nipplejs/issues/85)) ([0b1f4ae](https://github.com/yoannmoinet/nipplejs/commit/0b1f4ae0d54b41ef1a9c1feef4f820b8a85b8519)), closes [#21](https://github.com/yoannmoinet/nipplejs/issues/21) 108 | 109 | 110 | 111 | ## [0.7.1](https://github.com/yoannmoinet/nipplejs/compare/v0.7.0...v0.7.1) (2018-06-06) 112 | 113 | 114 | 115 | # [0.7.0](https://github.com/yoannmoinet/nipplejs/compare/v0.6.8...v0.7.0) (2018-06-06) 116 | 117 | 118 | ### Bug Fixes 119 | 120 | * register touchcancel and pointercancel events ([2c541a6](https://github.com/yoannmoinet/nipplejs/commit/2c541a60427a92b3b26582ed781fac99399b41f5)) 121 | 122 | 123 | ### Features 124 | 125 | * **joystick:** add lockX and lockY options ([6baba97](https://github.com/yoannmoinet/nipplejs/commit/6baba9707999f77336efd3d87f2d3a04a87d5d80)) 126 | 127 | 128 | 129 | ## [0.6.8](https://github.com/yoannmoinet/nipplejs/compare/v0.6.7...v0.6.8) (2017-12-19) 130 | 131 | 132 | ### Bug Fixes 133 | 134 | * add touchcancel handling as touchend for events coming from outside ([ebc501e](https://github.com/yoannmoinet/nipplejs/commit/ebc501e2760af6dd048cac4dbd51d57e0e160a61)), closes [#61](https://github.com/yoannmoinet/nipplejs/issues/61) [#57](https://github.com/yoannmoinet/nipplejs/issues/57) [#33](https://github.com/yoannmoinet/nipplejs/issues/33) [#31](https://github.com/yoannmoinet/nipplejs/issues/31) [#30](https://github.com/yoannmoinet/nipplejs/issues/30) 135 | * update manager with changing identifier ([45ec1d7](https://github.com/yoannmoinet/nipplejs/commit/45ec1d7f23064b8e2096e3ab1120a69a5147ce9f)), closes [#64](https://github.com/yoannmoinet/nipplejs/issues/64) 136 | 137 | 138 | ### Features 139 | 140 | * add restJoystick option ([55b6482](https://github.com/yoannmoinet/nipplejs/commit/55b6482af3ee937ccd87154d1475396bbe80b15b)) 141 | 142 | 143 | 144 | ## [0.6.7](https://github.com/yoannmoinet/nipplejs/compare/v0.6.6...v0.6.7) (2016-10-27) 145 | 146 | 147 | ### Bug Fixes 148 | 149 | * **events:** trigger events with the correct id and not identifier ([17f0fe6](https://github.com/yoannmoinet/nipplejs/commit/17f0fe6740633021e96c5e1a3db43a3572035e56)) 150 | * **identifier:** better identifier management ([ff44916](https://github.com/yoannmoinet/nipplejs/commit/ff4491661a88b9d9af6a1ec229723e2f27affd65)) 151 | * update dependencies ([73bcda9](https://github.com/yoannmoinet/nipplejs/commit/73bcda952d01882566a72dd52ec3678548eefe51)) 152 | 153 | 154 | 155 | ## [0.6.6](https://github.com/yoannmoinet/nipplejs/compare/v0.6.5...v0.6.6) (2016-08-23) 156 | 157 | 158 | ### Bug Fixes 159 | 160 | * **bin:** adapt slashes to windows if needed ([7dffd7c](https://github.com/yoannmoinet/nipplejs/commit/7dffd7cfe2945b92be6244efcffafaa1d8a6066c)) 161 | * **bin:** add missing space ([48f6a1f](https://github.com/yoannmoinet/nipplejs/commit/48f6a1f8d3211bdf70714bd8a28f837d35efd301)) 162 | * **bin:** make the copyToGhPages script windows compatible ([11da89a](https://github.com/yoannmoinet/nipplejs/commit/11da89adac1760555184a50933c733a75a021f7f)) 163 | * **dataOnly:** conserve chainability even with dataOnly ([d1eddd0](https://github.com/yoannmoinet/nipplejs/commit/d1eddd05c5ab6787f5cb78608190ee80b762f757)) 164 | 165 | 166 | 167 | ## [0.6.5](https://github.com/yoannmoinet/nipplejs/compare/v0.6.4...v0.6.5) (2016-08-15) 168 | 169 | 170 | ### Bug Fixes 171 | 172 | * **collection:** avoid any native touch action on the `options.zone` ([bea2c42](https://github.com/yoannmoinet/nipplejs/commit/bea2c42a083c1e8e177eeaba9de4dac513db0006)) 173 | 174 | 175 | 176 | ## [0.6.4](https://github.com/yoannmoinet/nipplejs/compare/v0.6.3...v0.6.4) (2016-07-20) 177 | 178 | 179 | ### Bug Fixes 180 | 181 | * **nipple:** remove size of container and apply on children only ([0033342](https://github.com/yoannmoinet/nipplejs/commit/0033342a5759c080c907247efca50e63623eab6d)), closes [#32](https://github.com/yoannmoinet/nipplejs/issues/32) 182 | 183 | 184 | 185 | ## [0.6.3](https://github.com/yoannmoinet/nipplejs/compare/v0.6.2...v0.6.3) (2016-05-29) 186 | 187 | 188 | ### Bug Fixes 189 | 190 | * **manager:** add missing closing bracket ([9b99637](https://github.com/yoannmoinet/nipplejs/commit/9b99637b6e40b403a82feb4e92e8c6af2dc83193)) 191 | * **resize:** listen to resize via event instead of global handler ([14c28f1](https://github.com/yoannmoinet/nipplejs/commit/14c28f1ef868abc99e63761b208d9eb2a796bb54)) 192 | 193 | 194 | 195 | ## [0.6.2](https://github.com/yoannmoinet/nipplejs/compare/v0.6.1...v0.6.2) (2015-12-31) 196 | 197 | 198 | 199 | ## [0.6.1](https://github.com/yoannmoinet/nipplejs/compare/v0.6.0...v0.6.1) (2015-12-26) 200 | 201 | 202 | ### Bug Fixes 203 | 204 | * **collection:** trigger `start` event before any potential direction ([c5441fd](https://github.com/yoannmoinet/nipplejs/commit/c5441fd6f28290efd7c05355b121ef897bfe8203)) 205 | * **nipple:** fragment control of previous directions ([790fa26](https://github.com/yoannmoinet/nipplejs/commit/790fa2654e7b107c5957845a9b9652b77e665905)), closes [#25](https://github.com/yoannmoinet/nipplejs/issues/25) 206 | 207 | 208 | ### Features 209 | 210 | * **nipple:** add `resetDirection()` to cancel previous directions ([66f6e3b](https://github.com/yoannmoinet/nipplejs/commit/66f6e3baeb9445873eae65aa5590de07a045b0f6)) 211 | 212 | 213 | 214 | # [0.6.0](https://github.com/yoannmoinet/nipplejs/compare/v0.5.6...v0.6.0) (2015-11-29) 215 | 216 | 217 | ### Bug Fixes 218 | 219 | * clean identifier if not found in any collection ([3496395](https://github.com/yoannmoinet/nipplejs/commit/349639507a15f3b54e5d3a39dce653af5fe7b263)) 220 | * handle scrolling offset by resetting zone's box ([6ccd0b5](https://github.com/yoannmoinet/nipplejs/commit/6ccd0b5d2ee52fddccf1104f2d1a49b3260ae8ff)) 221 | * remove useless injections ([a0b1d7e](https://github.com/yoannmoinet/nipplejs/commit/a0b1d7e68371d33917b3205cd2ef257466c11787)) 222 | * return the found nipple in `get()` ([439aa69](https://github.com/yoannmoinet/nipplejs/commit/439aa695660aa2b72079163f7ff6c326e92fad67)) 223 | * unbind document when collection is manually destroyed ([6e66b62](https://github.com/yoannmoinet/nipplejs/commit/6e66b62c6b96b2a9b02b7cf86dc1ded963bd8022)) 224 | * **nipple:** call off() after triggering ([bf8e5c3](https://github.com/yoannmoinet/nipplejs/commit/bf8e5c3782565d81d11556a734b51fc831e81cfe)) 225 | * **nipple:** only trigger itself, not its manager ([6b39786](https://github.com/yoannmoinet/nipplejs/commit/6b39786e39d0d95a3db5981274795878abb31fcf)) 226 | * **super:** move internal declarations where needed ([a4c79c2](https://github.com/yoannmoinet/nipplejs/commit/a4c79c240dac3d1a5be2ff200430ad95dc951ac5)) 227 | * **utils:** return object on u.extend ([9197c00](https://github.com/yoannmoinet/nipplejs/commit/9197c005a4c0752b35738920867d93ea860e69e1)) 228 | 229 | 230 | ### Features 231 | 232 | * **nipple:** add an incremental id to absolutely differentiate each ([598a5eb](https://github.com/yoannmoinet/nipplejs/commit/598a5eb69a354f506a7d30b97c95c40a47987433)) 233 | * **utils:** add u.map to execute fn in array or single element ([515aa5d](https://github.com/yoannmoinet/nipplejs/commit/515aa5d44bd0f14fc301a8185d28398f3b0a6a76)) 234 | * **utils:** add u.safeExtend to only replace existent attributes ([5212ffe](https://github.com/yoannmoinet/nipplejs/commit/5212ffe54cd55ca667ab603ab2bbb2dcaa313408)) 235 | 236 | 237 | ### Performance Improvements 238 | 239 | * safety clean if nipple isn't found in collection ([ca20985](https://github.com/yoannmoinet/nipplejs/commit/ca209853cf575f67e96c5b2454caa9bf17f02c6c)) 240 | 241 | 242 | 243 | ## [0.5.6](https://github.com/yoannmoinet/nipplejs/compare/v0.5.5...v0.5.6) (2015-11-09) 244 | 245 | 246 | 247 | ## [0.5.5](https://github.com/yoannmoinet/nipplejs/compare/v0.5.4...v0.5.5) (2015-11-09) 248 | 249 | 250 | ### Bug Fixes 251 | 252 | * **manager:** allow multitouch to be more than 1 ([7569536](https://github.com/yoannmoinet/nipplejs/commit/756953636e9330104c94205e01ac821a827a4008)) 253 | * **manager:** clean the start process, no duplicate ([7e30551](https://github.com/yoannmoinet/nipplejs/commit/7e305511fba8c4bc71bf9fb9040d1fc169cd6519)) 254 | * **manager:** remove handler only if last touch ([1f03064](https://github.com/yoannmoinet/nipplejs/commit/1f03064ba0039e079d64dc853faf11d33bcb9c53)) 255 | 256 | 257 | 258 | ## [0.5.4](https://github.com/yoannmoinet/nipplejs/compare/v0.5.3...v0.5.4) (2015-11-08) 259 | 260 | 261 | ### Bug Fixes 262 | 263 | * **manager:** better handle identifiers ([54b3cff](https://github.com/yoannmoinet/nipplejs/commit/54b3cff9cfac518ea2306d6c62b1e97699e9dfb1)), closes [#16](https://github.com/yoannmoinet/nipplejs/issues/16) 264 | * **nipple:** use `document.body.contains` for IE11 doesn't shortcut ([6e5b80a](https://github.com/yoannmoinet/nipplejs/commit/6e5b80a056127c17396a77e91b26e2dc662e0ca4)), closes [#17](https://github.com/yoannmoinet/nipplejs/issues/17) 265 | 266 | 267 | 268 | ## [0.5.3](https://github.com/yoannmoinet/nipplejs/compare/v0.5.2...v0.5.3) (2015-10-04) 269 | 270 | 271 | ### Bug Fixes 272 | 273 | * **manager:** add back pressure variable... oups ([a46982f](https://github.com/yoannmoinet/nipplejs/commit/a46982fe31e01594d593eae46e2ad6c868116085)) 274 | 275 | 276 | 277 | ## [0.5.2](https://github.com/yoannmoinet/nipplejs/compare/v0.5.1...v0.5.2) (2015-10-04) 278 | 279 | 280 | ### Bug Fixes 281 | 282 | * **manager:** correctly unbind all events when destroying manager ([e3f96ef](https://github.com/yoannmoinet/nipplejs/commit/e3f96efb3b78b11e7e985a860edb87861af4188d)) 283 | * **utils:** handle touch based on their event's type ([406a7b2](https://github.com/yoannmoinet/nipplejs/commit/406a7b2c5b3dc820c08bda4c7854e62b09d86e03)) 284 | 285 | 286 | ### Features 287 | 288 | * **manager:** better pressure management cross-plateform ([d867812](https://github.com/yoannmoinet/nipplejs/commit/d8678122dcedf9a4ca385d44f143b34bc7ec89ea)) 289 | 290 | 291 | 292 | ## [0.5.1](https://github.com/yoannmoinet/nipplejs/compare/v0.5.0...v0.5.1) (2015-10-03) 293 | 294 | 295 | ### Bug Fixes 296 | 297 | * **nipple:** remove useless backPosition ([4c76adc](https://github.com/yoannmoinet/nipplejs/commit/4c76adc4c66920d223ed1f2e67d35af0f5881f87)) 298 | * **super:** better inheritance ([4a3a8dc](https://github.com/yoannmoinet/nipplejs/commit/4a3a8dc33cc9eb5eca53a0ed3a2d308cc06da34d)) 299 | 300 | 301 | 302 | # [0.5.0](https://github.com/yoannmoinet/nipplejs/compare/v0.4.2...v0.5.0) (2015-09-29) 303 | 304 | 305 | ### Bug Fixes 306 | 307 | * **manager:** add default pressure at 0 ([1cb31d0](https://github.com/yoannmoinet/nipplejs/commit/1cb31d08a81ee27db2fe1d1b0ad46291ada337ae)) 308 | * **nipple:** control dom presence before add or remove ([7cc1d8c](https://github.com/yoannmoinet/nipplejs/commit/7cc1d8ca0961af6ab64dcedeadef4153fbfabdd0)) 309 | * **nipple:** return the manager's known object in event triggers ([2deb42c](https://github.com/yoannmoinet/nipplejs/commit/2deb42c757e7edc4ff8408a13d7bdf7883e403c6)) 310 | * remove useless rimraf ([26db70f](https://github.com/yoannmoinet/nipplejs/commit/26db70fe2e310e67c492e0503f4bfab5b8705a8c)) 311 | * **npm:** add rimraf as a dev dependency ([deb7acc](https://github.com/yoannmoinet/nipplejs/commit/deb7accd3dab565604704b35e2bc4d9d81e3c187)), closes [#12](https://github.com/yoannmoinet/nipplejs/issues/12) 312 | 313 | 314 | ### Features 315 | 316 | * **manager:** handle resize and reinit positions ([261fb59](https://github.com/yoannmoinet/nipplejs/commit/261fb593a0dc9c8e36c23ea95a769fbf7c13c0ce)) 317 | * **nipple:** remove position offset ([be59bc4](https://github.com/yoannmoinet/nipplejs/commit/be59bc46c3a4b9c23084cea0ddb714447bcbc7af)) 318 | * add dataOnly option ([2f527b1](https://github.com/yoannmoinet/nipplejs/commit/2f527b1db1262083575d1fd5e770c2c176112f1c)) 319 | * add two modes, static and semi ([347feaa](https://github.com/yoannmoinet/nipplejs/commit/347feaaa38cd4e16f603dfe61580b15a1f53e70b)) 320 | * **manager:** add destroy method ([295654f](https://github.com/yoannmoinet/nipplejs/commit/295654f1edcd5618f4f4c1fe9cf5ce0b1e96f758)) 321 | * **manager:** expose nippleOptions ([968103c](https://github.com/yoannmoinet/nipplejs/commit/968103cdc02dbbe9df63f27e6fddf0d1c73aee06)) 322 | * **nipple:** add destroy method ([713e505](https://github.com/yoannmoinet/nipplejs/commit/713e5056d448056b194507b60f53abf777fc81a7)) 323 | * **off:** allow unsubscribing on all events at once ([c30bfad](https://github.com/yoannmoinet/nipplejs/commit/c30bfada378eb7a66c54f2548f5cd8b504ecb3f0)) 324 | 325 | 326 | 327 | ## [0.4.2](https://github.com/yoannmoinet/nipplejs/compare/v0.4.1...v0.4.2) (2015-09-15) 328 | 329 | 330 | ### Bug Fixes 331 | 332 | * correctly return directions ([871d782](https://github.com/yoannmoinet/nipplejs/commit/871d782342b7f3faf50e79ba32972a6bef0e9dc2)) 333 | 334 | 335 | 336 | ## [0.4.1](https://github.com/yoannmoinet/nipplejs/compare/v0.4.0...v0.4.1) (2015-09-15) 337 | 338 | 339 | ### Bug Fixes 340 | 341 | * return the computed direction ([b3d80b3](https://github.com/yoannmoinet/nipplejs/commit/b3d80b3ac3e37559ff3ac359da580f7ae2ba1cc0)) 342 | 343 | 344 | 345 | # [0.4.0](https://github.com/yoannmoinet/nipplejs/compare/v0.3.1...v0.4.0) (2015-09-15) 346 | 347 | 348 | ### Bug Fixes 349 | 350 | * **manager:** streamline events triggered ([76941ec](https://github.com/yoannmoinet/nipplejs/commit/76941ecaf6a50f70c00cd28289a809d073247f90)) 351 | 352 | 353 | ### Features 354 | 355 | * **nipple:** add a callback after show and hide ([5b24124](https://github.com/yoannmoinet/nipplejs/commit/5b24124a883f0116b7158bef711329b38d458d2c)) 356 | * **nipple:** add the positions to the instance ([fac981b](https://github.com/yoannmoinet/nipplejs/commit/fac981b0dcb974bc15b41db1bff11f1e1ed3fb4d)) 357 | * **off:** allow unsubscribing of a type at once ([cbbefca](https://github.com/yoannmoinet/nipplejs/commit/cbbefcac0e17410d87a88c4031fadde2aa52bdf9)) 358 | * **on:** pass the target in the event ([bc0630d](https://github.com/yoannmoinet/nipplejs/commit/bc0630de035efcf08e2037d499da04b754082884)) 359 | * add more events for both nipple and manager ([f7966b9](https://github.com/yoannmoinet/nipplejs/commit/f7966b9cfe8d09087b078404b799e1fec177b43c)) 360 | * add support for both mouse and touch at the same time ([e8fcba2](https://github.com/yoannmoinet/nipplejs/commit/e8fcba2ffc33eec8b51f4ec5d95879240e29ee21)) 361 | * add support for pressure ([80e7770](https://github.com/yoannmoinet/nipplejs/commit/80e777034934b1dc2f71626351b8088628b3319a)), closes [#10](https://github.com/yoannmoinet/nipplejs/issues/10) 362 | * attach values to each instance of nipple ([c5bf9ab](https://github.com/yoannmoinet/nipplejs/commit/c5bf9ab70ed8adc655e978160bf1178d5dbe9905)) 363 | * limit the number of simultaneous nipples ([801e91d](https://github.com/yoannmoinet/nipplejs/commit/801e91d9e14f179470338213af5deaf1c3588146)) 364 | * support multitouch correctly ([33bbe23](https://github.com/yoannmoinet/nipplejs/commit/33bbe23442da539a815d294fdda3fb416bf2e812)), closes [#2](https://github.com/yoannmoinet/nipplejs/issues/2) [#6](https://github.com/yoannmoinet/nipplejs/issues/6) 365 | 366 | 367 | 368 | ## [0.3.1](https://github.com/yoannmoinet/nipplejs/compare/v0.3.0...v0.3.1) (2015-09-07) 369 | 370 | 371 | ### Bug Fixes 372 | 373 | * **on:** allows multiple spaces in event string ([b89bd0e](https://github.com/yoannmoinet/nipplejs/commit/b89bd0e2fe450d3431f5b1636e24356803e25f45)) 374 | * **on:** removes variable name clash. ([3d70a3c](https://github.com/yoannmoinet/nipplejs/commit/3d70a3c2c6e22e9dcdedf59febb84547651b9e06)) 375 | 376 | 377 | 378 | # [0.3.0](https://github.com/yoannmoinet/nipplejs/compare/v0.2.1...v0.3.0) (2015-09-06) 379 | 380 | 381 | ### Features 382 | 383 | * offset angle by 180 for units circle ([3002014](https://github.com/yoannmoinet/nipplejs/commit/30020144d5cdfb66f6a41799cc6144bf4872d08a)) 384 | * support IE PointerEvents ([ed65d1e](https://github.com/yoannmoinet/nipplejs/commit/ed65d1e5d5089fb068e2db9b05bd94dc6560ede0)), closes [#1](https://github.com/yoannmoinet/nipplejs/issues/1) 385 | 386 | 387 | 388 | ## [0.2.1](https://github.com/yoannmoinet/nipplejs/compare/v0.2.0...v0.2.1) (2015-09-06) 389 | 390 | 391 | ### Bug Fixes 392 | 393 | * correct chrome's clientBoundingBox ([b94bc43](https://github.com/yoannmoinet/nipplejs/commit/b94bc4365653368a32a5985ae7d30af24961894b)) 394 | 395 | 396 | 397 | # [0.2.0](https://github.com/yoannmoinet/nipplejs/compare/v0.1.1...v0.2.0) (2015-09-06) 398 | 399 | 400 | ### Bug Fixes 401 | 402 | * **on:** return self to allow chain ([f068cc9](https://github.com/yoannmoinet/nipplejs/commit/f068cc9140a8003872cc7135ace614a8582136bb)) 403 | 404 | 405 | ### Features 406 | 407 | * **on:** let listen to multiple events ([d19ad97](https://github.com/yoannmoinet/nipplejs/commit/d19ad9789c136f68f6c38110355d22cec1585841)) 408 | * **on, off:** allow to chain calls ([c82c580](https://github.com/yoannmoinet/nipplejs/commit/c82c580531589e0338bc967e25efe6fd4d8f5c50)) 409 | * **trigger:** trigger only if different direction ([9e334ba](https://github.com/yoannmoinet/nipplejs/commit/9e334ba41a5c5b935af18acbbfbe4ea67bd6b5b8)) 410 | 411 | 412 | 413 | ## [0.1.1](https://github.com/yoannmoinet/nipplejs/compare/v0.1.0...v0.1.1) (2015-09-06) 414 | 415 | 416 | ### Bug Fixes 417 | 418 | * add scroll offset ([7b36fb6](https://github.com/yoannmoinet/nipplejs/commit/7b36fb6fde4e4b48a0f18b6267049683de13a699)) 419 | * remove useless log ([b958cbf](https://github.com/yoannmoinet/nipplejs/commit/b958cbf33b8ab2fb1bcbfb8092a83c7acfe5fc63)) 420 | * send data to start event ([c1f1688](https://github.com/yoannmoinet/nipplejs/commit/c1f16887a98d35a00505b34c011a941e9b66d32f)) 421 | 422 | 423 | 424 | # 0.1.0 (2015-09-05) 425 | 426 | 427 | 428 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | Your help is more than welcome, I would be very honored to have you on my side. 3 | 4 | Here are some very basic guidelines. 5 | 6 | ### Commits 7 | Please follow these [guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits) so your commits will be taken by the self-generated changelog. 8 | 9 | ### Style 10 | [ESLint](http://eslint.org/) is set up on the project. It will be checked at build time. 11 | 12 | We follow a **4 spaces** rule around here. 13 | 14 | ### Workflow 15 | You can use the available scripts if needed. 16 | 17 | - `npm start` will start the webpack server on [`localhost:9000`](http://localhost:9000) and build the library. 18 | - You can navigate examples if needed: 19 | - [./example/codepen-demo.html](http://localhost:9000/example/codepen-demo.html) 20 | - [./example/dual-joysticks.html](http://localhost:9000/example/dual-joysticks.html) 21 | - [./example/lock-axes.html](http://localhost:9000/example/lock-axes.html) 22 | - `npm test` will test using CasperJS, you have to run `npm start` in another window to have a local server available to CasperJS. You need to install CasperJS and PhantomJS globally for it to run `npm install -g casperjs phantomjs` 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Yoann Moinet 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt tag](./assets/nipplejs.png) 2 | > A vanilla virtual joystick for touch capable interfaces 3 | 4 | 5 | [![npm](https://img.shields.io/npm/v/nipplejs.svg)](https://npmjs.org/package/nipplejs) 6 | [![npm](https://img.shields.io/npm/dm/nipplejs.svg)](https://npmjs.org/package/nipplejs) 7 | 8 | # Table Of Contents 9 |
10 | 11 | 12 | 13 | - [Install](#install) 14 | - [Demo](#demo) 15 | - [Usage](#usage) 16 | - [Options](#options) 17 | * [`options.zone` defaults to 'body'](#optionszone-defaults-to-body) 18 | * [`options.color` defaults to 'white'](#optionscolor-defaults-to-white) 19 | * [`options.size` defaults to 100](#optionssize-defaults-to-100) 20 | * [`options.threshold` defaults to 0.1](#optionsthreshold-defaults-to-01) 21 | * [`options.fadeTime` defaults to 250](#optionsfadetime-defaults-to-250) 22 | * [`options.multitouch` defaults to false](#optionsmultitouch-defaults-to-false) 23 | * [`options.maxNumberOfNipples` defaults to 1](#optionsmaxnumberofnipples-defaults-to-1) 24 | * [`options.dataOnly` defaults to false](#optionsdataonly-defaults-to-false) 25 | * [`options.position` defaults to `{top: 0, left: 0}`](#optionsposition-defaults-to-top-0-left-0) 26 | * [`options.mode` defaults to 'dynamic'.](#optionsmode-defaults-to-dynamic) 27 | + [`'dynamic'`](#dynamic) 28 | + [`'semi'`](#semi) 29 | + [`'static'`](#static) 30 | * [`options.restJoystick` defaults to true](#optionsrestjoystick-defaults-to-true) 31 | * [`options.restOpacity` defaults to 0.5](#optionsrestopacity-defaults-to-05) 32 | * [`options.catchDistance` defaults to 200](#optionscatchdistance-defaults-to-200) 33 | * [`options.lockX` defaults to false](#optionslockx-defaults-to-false) 34 | * [`options.lockY` defaults to false](#optionslocky-defaults-to-false) 35 | * [`options.shape` defaults to 'circle'](#optionsshape-defaults-to-circle) 36 | + [`'circle'`](#circle) 37 | + [`'square'`](#square) 38 | * [`options.dynamicPage` defaults to false](#optionsdynamicpage-defaults-to-false) 39 | * [`options.follow` defaults to false](#optionsfollow-defaults-to-false) 40 | - [API](#api) 41 | * [NippleJS instance (manager)](#nipplejs-instance-manager) 42 | + [`manager.on(type, handler)`](#managerontype-handler) 43 | + [`manager.off([type, handler])`](#managerofftype-handler) 44 | + [`manager.get(identifier)`](#managergetidentifier) 45 | + [`manager.destroy()`](#managerdestroy) 46 | + [`manager.ids`](#managerids) 47 | + [`manager.id`](#managerid) 48 | * [nipple instance (joystick)](#nipple-instance-joystick) 49 | * [`joystick.on`, `joystick.off`](#joystickon-joystickoff) 50 | * [`joystick.el`](#joystickel) 51 | * [`joystick.show([cb])`](#joystickshowcb) 52 | * [`joystick.hide([cb])`](#joystickhidecb) 53 | * [`joystick.add()`](#joystickadd) 54 | * [`joystick.remove()`](#joystickremove) 55 | * [`joystick.destroy()`](#joystickdestroy) 56 | * [`joystick.setPosition(cb, { x, y })`](#joysticksetpositioncb--x-y-) 57 | * [`joystick.identifier`](#joystickidentifier) 58 | * [`joystick.trigger(type [, data])`](#joysticktriggertype--data) 59 | * [`joystick.position`](#joystickposition) 60 | * [`joystick.frontPosition`](#joystickfrontposition) 61 | * [`joystick.ui`](#joystickui) 62 | - [Events](#events) 63 | * [manager only](#manager-only) 64 | + [`added`](#added) 65 | + [`removed`](#removed) 66 | * [manager and joysticks](#manager-and-joysticks) 67 | + [`start`](#start) 68 | + [`end`](#end) 69 | + [`move`](#move) 70 | + [`dir`](#dir) 71 | + [`plain`](#plain) 72 | + [`shown`](#shown) 73 | + [`hidden`](#hidden) 74 | + [`destroyed`](#destroyed) 75 | + [`pressure`](#pressure) 76 | - [Contributing](#contributing) 77 | 78 | 79 | 80 |
81 | 82 | ## Install 83 | 84 | ```bash 85 | npm install nipplejs --save 86 | ``` 87 | 88 | ---- 89 | 90 | ## Demo 91 | Check out the [demo here](http://yoannmoinet.github.io/nipplejs/#demo). 92 | 93 | ---- 94 | 95 | ## Usage 96 | 97 | Import it the way you want into your project : 98 | 99 | ```javascript 100 | // CommonJS 101 | var manager = require('nipplejs').create(options); 102 | ``` 103 | 104 | ```javascript 105 | // AMD 106 | define(['nipplejs'], function (nipplejs) { 107 | var manager = nipplejs.create(options); 108 | }); 109 | ``` 110 | 111 | ```javascript 112 | // Module 113 | import nipplejs from 'nipplejs'; 114 | ``` 115 | 116 | ```html 117 | 118 | 119 | 122 | ``` 123 | 124 | **:warning: NB :warning:** Your joystick's container **has** to have its CSS `position` property set, either `absolute`, `relative`, `static`, .... 125 | 126 | ---- 127 | 128 | ## Options 129 | You can configure your joystick in different ways : 130 | 131 | ```javascript 132 | var options = { 133 | zone: Element, // active zone 134 | color: String, 135 | size: Integer, 136 | threshold: Float, // before triggering a directional event 137 | fadeTime: Integer, // transition time 138 | multitouch: Boolean, 139 | maxNumberOfNipples: Number, // when multitouch, what is too many? 140 | dataOnly: Boolean, // no dom element whatsoever 141 | position: Object, // preset position for 'static' mode 142 | mode: String, // 'dynamic', 'static' or 'semi' 143 | restJoystick: Boolean|Object, // Re-center joystick on rest state 144 | restOpacity: Number, // opacity when not 'dynamic' and rested 145 | lockX: Boolean, // only move on the X axis 146 | lockY: Boolean, // only move on the Y axis 147 | catchDistance: Number, // distance to recycle previous joystick in 148 | // 'semi' mode 149 | shape: String, // 'circle' or 'square' 150 | dynamicPage: Boolean, // Enable if the page has dynamically visible elements 151 | follow: Boolean, // Makes the joystick follow the thumbstick 152 | }; 153 | ``` 154 | 155 | All options are optional :sunglasses:. 156 | 157 | ### `options.zone` defaults to 'body' 158 | The dom element in which all your joysticks will be injected. 159 | 160 | ```html 161 |
162 | 163 | 164 | 170 | ``` 171 | 172 | This zone also serve as the mouse/touch events handler. 173 | 174 | It represents the zone where all your joysticks will be active. 175 | 176 | ### `options.color` defaults to 'white' 177 | The background color of your joystick's elements. 178 | 179 | Can be any valid CSS color. 180 | 181 | ### `options.size` defaults to 100 182 | The size in pixel of the outer circle. 183 | 184 | The inner circle is 50% of this size. 185 | 186 | ### `options.threshold` defaults to 0.1 187 | This is the strength needed to trigger a directional event. 188 | 189 | Basically, the center is 0 and the outer is 1. 190 | 191 | You need to at least go to 0.1 to trigger a directional event. 192 | 193 | ### `options.fadeTime` defaults to 250 194 | The time it takes for joystick to fade-out and fade-in when activated or de-activated. 195 | 196 | ### `options.multitouch` defaults to false 197 | Enable the multitouch capabilities. 198 | 199 | If, for reasons, you need to have multiple nipples in the same zone. 200 | 201 | Otherwise, it will only get one, and all new touches won't do a thing. 202 | 203 | Please note that multitouch is off when in `static` or `semi` modes. 204 | 205 | ### `options.maxNumberOfNipples` defaults to 1 206 | If you need to, you can also control the maximum number of instances that could be created. 207 | 208 | Obviously in a multitouch configuration. 209 | 210 | ### `options.dataOnly` defaults to false 211 | The library won't draw anything in the DOM and will only trigger events with data. 212 | 213 | ### `options.position` defaults to `{top: 0, left: 0}` 214 | An object that will determine the position of a `static` mode. 215 | 216 | You can pass any of the four `top`, `right`, `bottom` and `left`. 217 | 218 | They will be applied as any css property. 219 | 220 | Ex : 221 | - `{top: '50px', left: '50px'}` 222 | - `{left: '10%', bottom: '10%'}` 223 | 224 | ### `options.mode` defaults to 'dynamic'. 225 | Three modes are possible : 226 | 227 | #### `'dynamic'` 228 | - a new joystick is created at each new touch. 229 | - the joystick gets destroyed when released. 230 | - **can** be multitouch. 231 | 232 | #### `'semi'` 233 | - new joystick is created at each new touch farther than `options.catchDistance` of any previously created joystick. 234 | - the joystick is faded-out when released but not destroyed. 235 | - when touch is made **inside** the `options.catchDistance` a new direction is triggered immediately. 236 | - when touch is made **outside** the `options.catchDistance` the previous joystick is destroyed and a new one is created. 237 | - **cannot** be multitouch. 238 | 239 | #### `'static'` 240 | - a joystick is positioned immediately at `options.position`. 241 | - one joystick per zone. 242 | - each new touch triggers a new direction. 243 | - **cannot** be multitouch. 244 | 245 | ### `options.restJoystick` defaults to true 246 | Reset the joystick's position when it enters the rest state. 247 | 248 | You can pass a boolean value to reset the joystick's position for both the axis. 249 | ```js 250 | var joystick = nipplejs.create({ 251 | restJoystick: true, 252 | // This is converted to {x: true, y: true} 253 | 254 | // OR 255 | restJoystick: false, 256 | // This is converted to {x: false, y: false} 257 | }); 258 | ``` 259 | 260 | Or you can pass an object to specify which axis should be reset. 261 | ```js 262 | var joystick = nipplejs.create({ 263 | restJoystick: {x: false}, 264 | // This is converted to {x: false, y: true} 265 | 266 | // OR 267 | restJoystick: {x: false, y: true}, 268 | }); 269 | ``` 270 | 271 | ### `options.restOpacity` defaults to 0.5 272 | The opacity to apply when the joystick is in a rest position. 273 | 274 | ### `options.catchDistance` defaults to 200 275 | This is only useful in the `semi` mode, and determine at which distance we recycle the previous joystick. 276 | 277 | At 200 (px), if you press the zone into a rayon of 200px around the previously displayed joystick, 278 | it will act as a `static` one. 279 | 280 | ### `options.lockX` defaults to false 281 | Locks joystick's movement to the x (horizontal) axis 282 | 283 | ### `options.lockY` defaults to false 284 | Locks joystick's movement to the y (vertical) axis 285 | 286 | ### `options.shape` defaults to 'circle' 287 | The shape of region within which joystick can move. 288 | 289 | #### `'circle'` 290 | Creates circle region for joystick movement 291 | 292 | #### `'square'` 293 | Creates square region for joystick movement 294 | 295 | ### `options.dynamicPage` defaults to false 296 | Enable if the page has dynamically visible elements such as for Vue, React, Angular or simply some CSS hiding or showing some DOM. 297 | 298 | ### `options.follow` defaults to false 299 | Makes the joystick follow the thumbstick when it reaches the border. 300 | 301 | ---- 302 | 303 | ## API 304 | 305 | ### NippleJS instance (manager) 306 | 307 | Your manager has the following signature : 308 | 309 | ```javascript 310 | { 311 | on: Function, // handle internal event 312 | off: Function, // un-handle internal event 313 | get: Function, // get a specific joystick 314 | destroy: Function, // destroy everything 315 | ids: Array // array of assigned ids 316 | id: Number // id of the manager 317 | options: { 318 | zone: Element, // reactive zone 319 | multitouch: Boolean, 320 | maxNumberOfNipples: Number, 321 | mode: String, 322 | position: Object, 323 | catchDistance: Number, 324 | size: Number, 325 | threshold: Number, 326 | color: String, 327 | fadeTime: Number, 328 | dataOnly: Boolean, 329 | restJoystick: Boolean, 330 | restOpacity: Number 331 | } 332 | } 333 | ``` 334 | 335 | #### `manager.on(type, handler)` 336 | 337 | If you wish to listen to internal events like : 338 | 339 | ```javascript 340 | manager.on('event#1 event#2', function (evt, data) { 341 | // Do something. 342 | }); 343 | ``` 344 | 345 | Note that you can listen to multiple events at once by separating 346 | them either with a space or a comma (or both, I don't care). 347 | 348 | #### `manager.off([type, handler])` 349 | 350 | To remove an event handler : 351 | 352 | ```javascript 353 | manager.off('event', handler); 354 | ``` 355 | 356 | If you call off without arguments, all handlers will be removed. 357 | 358 | If you don't specify the handler but just a type, all handlers for that type will be removed. 359 | 360 | #### `manager.get(identifier)` 361 | 362 | A helper to get an instance via its identifier. 363 | 364 | ```javascript 365 | // Will return the nipple instantiated by the touch identified by 0 366 | manager.get(0); 367 | ``` 368 | 369 | #### `manager.destroy()` 370 | 371 | Gently remove all nipples from the DOM and unbind all events. 372 | 373 | ```javascript 374 | manager.destroy(); 375 | ``` 376 | 377 | #### `manager.ids` 378 | 379 | The array of nipples' ids under this manager. 380 | 381 | #### `manager.id` 382 | 383 | The incremented id of this manager. 384 | 385 | ### nipple instance (joystick) 386 | 387 | Each joystick has the following signature : 388 | 389 | ```javascript 390 | { 391 | on: Function, 392 | off: Function, 393 | el: Element, 394 | show: Function, // fade-in 395 | hide: Function, // fade-out 396 | add: Function, // inject into dom 397 | remove: Function, // remove from dom 398 | destroy: Function, 399 | setPosition: Function, 400 | identifier: Number, 401 | trigger: Function, 402 | position: { // position of the center 403 | x: Number, 404 | y: Number 405 | }, 406 | frontPosition: { // position of the front part 407 | x: Number, 408 | y: Number 409 | }, 410 | ui: { 411 | el: Element, 412 | front: Element, 413 | back: Element 414 | }, 415 | options: { 416 | color: String, 417 | size: Number, 418 | threshold: Number, 419 | fadeTime: Number 420 | } 421 | } 422 | ``` 423 | 424 | ### `joystick.on`, `joystick.off` 425 | 426 | The same as the manager. 427 | 428 | ### `joystick.el` 429 | 430 | Dom element in which the joystick gets created. 431 | 432 | ```html 433 |
434 |
435 |
436 |
437 | ``` 438 | 439 | ### `joystick.show([cb])` 440 | 441 | Will show the joystick at the last known place. 442 | 443 | You can pass a callback that will be executed at the end of the fade-in animation. 444 | 445 | ### `joystick.hide([cb])` 446 | 447 | Will fade-out the joystick. 448 | 449 | You can pass a callback that will be executed at the end of the fade-out animation. 450 | 451 | ### `joystick.add()` 452 | 453 | Add the joystick's element to the dom. 454 | 455 | ### `joystick.remove()` 456 | 457 | Remove the joystick's element from the dom. 458 | 459 | ### `joystick.destroy()` 460 | 461 | Gently remove this nipple from the DOM and unbind all related events. 462 | 463 | ### `joystick.setPosition(cb, { x, y })` 464 | 465 | Set the joystick to the specified position, where x and y are distances away from the center in pixels. This does not trigger joystick events. 466 | 467 | ### `joystick.identifier` 468 | 469 | Returns the unique identifier of the joystick. 470 | 471 | Tied to its touch's identifier. 472 | 473 | ### `joystick.trigger(type [, data])` 474 | 475 | Trigger an internal event from the joystick. 476 | 477 | The same as `on` you can trigger multiple events at the same time. 478 | 479 | ### `joystick.position` 480 | 481 | The absolute position of the center of the joystick. 482 | 483 | ### `joystick.frontPosition` 484 | 485 | The absolute position of the back part of the joystick's ui. 486 | 487 | ### `joystick.ui` 488 | 489 | The object that store its ui elements 490 | 491 | ```html 492 | { 493 | el:
494 | back:
495 | front:
496 | } 497 | ``` 498 | 499 | ---- 500 | 501 | ## Events 502 | 503 | You can listen events both on the manager and all the joysticks. 504 | 505 | But some of them are specific to its instance. 506 | 507 | If you need to listen to each joystick, for example, you can : 508 | 509 | ```javascript 510 | manager.on('added', function (evt, nipple) { 511 | nipple.on('start move end dir plain', function (evt) { 512 | // DO EVERYTHING 513 | }); 514 | }).on('removed', function (evt, nipple) { 515 | nipple.off('start move end dir plain'); 516 | }); 517 | ``` 518 | 519 | ### manager only 520 | 521 | #### `added` 522 | 523 | A joystick just got added. 524 | 525 | Will pass the instance alongside the event. 526 | 527 | #### `removed` 528 | 529 | A joystick just got removed. 530 | 531 | Fired at the end of the fade-out animation. 532 | 533 | Will pass the instance alongside the event. 534 | 535 | Won't be trigger in a `dataOnly` configuration. 536 | 537 | ### manager and joysticks 538 | 539 | Other events are available on both the manager and joysticks. 540 | 541 | When listening on the manager, 542 | you can also target **a joystick in particular** by prefixing 543 | the event with its identifier, **`0:start`** for example. 544 | 545 | Else you'll get all events from all the joysticks. 546 | 547 | #### `start` 548 | 549 | A joystick is activated. (the user pressed on the active zone) 550 | 551 | Will pass the instance alongside the event. 552 | 553 | #### `end` 554 | 555 | A joystick is de-activated. (the user released the active zone) 556 | 557 | Will pass the instance alongside the event. 558 | 559 | #### `move` 560 | 561 | A joystick is moved. 562 | 563 | Comes with data : 564 | 565 | ```javascript 566 | { 567 | identifier: 0, // the identifier of the touch/mouse that triggered it 568 | position: { // absolute position of the center in pixels 569 | x: 125, 570 | y: 95 571 | }, 572 | force: 0.2, // strength in % 573 | distance: 25.4, // distance from center in pixels 574 | pressure: 0.1, // the pressure applied by the touch 575 | angle: { 576 | radian: 1.5707963268, // angle in radian 577 | degree: 90 578 | }, 579 | vector: { // force unit vector 580 | x: 0.508, 581 | y: 3.110602869834277e-17 582 | }, 583 | raw: { // note: angle is the same, beyond the 50 pixel limit 584 | distance: 25.4, // distance which continues beyond the 50 pixel limit 585 | position: { // position of the finger/mouse in pixels, beyond joystick limits 586 | x: 125, 587 | y: 95 588 | } 589 | }, 590 | instance: Nipple // the nipple instance that triggered the event 591 | } 592 | ``` 593 | 594 | #### `dir` 595 | 596 | When a direction is reached after the threshold. 597 | 598 | Direction are split with a 45° angle. 599 | 600 | ```javascript 601 | // \ UP / 602 | // \ / 603 | // LEFT RIGHT 604 | // / \ 605 | // /DOWN \ 606 | ``` 607 | 608 | You can also listen to specific direction like : 609 | 610 | - `dir:up` 611 | - `dir:down` 612 | - `dir:right` 613 | - `dir:left` 614 | 615 | In this configuration only one direction is triggered at a time. 616 | 617 | #### `plain` 618 | 619 | When a plain direction is reached after the threshold. 620 | 621 | Plain directions are split with a 90° angle. 622 | 623 | ```javascript 624 | // UP | 625 | // ------ LEFT | RIGHT 626 | // DOWN | 627 | ``` 628 | 629 | You can also listen to specific plain direction like : 630 | 631 | - `plain:up` 632 | - `plain:down` 633 | - `plain:right` 634 | - `plain:left` 635 | 636 | In this configuration two directions can be triggered at a time, 637 | because the user could be both `up` and `left` for example. 638 | 639 | #### `shown` 640 | 641 | Is triggered at the end of the fade-in animation. 642 | 643 | Will pass the instance alongside the event. 644 | 645 | Won't be trigger in a `dataOnly` configuration. 646 | 647 | #### `hidden` 648 | 649 | Is triggered at the end of the fade-out animation. 650 | 651 | Will pass the instance alongside the event. 652 | 653 | Won't be trigger in a `dataOnly` configuration. 654 | 655 | #### `destroyed` 656 | 657 | Is triggered at the end of destroy. 658 | 659 | Will pass the instance alongside the event. 660 | 661 | #### `pressure` 662 | 663 | > MBP's [**Force Touch**](http://www.apple.com/macbook-pro/features-retina/#interact), iOS's [**3D Touch**](http://www.apple.com/iphone-6s/3d-touch/), Microsoft's [**pressure**](https://msdn.microsoft.com/en-us/library/hh772360%28v=vs.85%29.aspx) or MDN's [**force**](https://developer.mozilla.org/en-US/docs/Web/API/Touch/force) 664 | 665 | Is triggered when the pressure on the joystick is changed. 666 | 667 | The value, between 0 and 1, is sent back alongside the event. 668 | 669 | ---- 670 | 671 | ## Contributing 672 | You can follow [this document](./CONTRIBUTING.md) to help you get started. 673 | -------------------------------------------------------------------------------- /assets/github-light.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 GitHub Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | 16 | */ 17 | 18 | .pl-c /* comment */ { 19 | color: #969896; 20 | } 21 | 22 | .pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */, 23 | .pl-s .pl-v /* string variable */ { 24 | color: #0086b3; 25 | } 26 | 27 | .pl-e /* entity */, 28 | .pl-en /* entity.name */ { 29 | color: #795da3; 30 | } 31 | 32 | .pl-s .pl-s1 /* string source */, 33 | .pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ { 34 | color: #333; 35 | } 36 | 37 | .pl-ent /* entity.name.tag */ { 38 | color: #63a35c; 39 | } 40 | 41 | .pl-k /* keyword, storage, storage.type */ { 42 | color: #a71d5d; 43 | } 44 | 45 | .pl-pds /* punctuation.definition.string, string.regexp.character-class */, 46 | .pl-s /* string */, 47 | .pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, 48 | .pl-sr /* string.regexp */, 49 | .pl-sr .pl-cce /* string.regexp constant.character.escape */, 50 | .pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */, 51 | .pl-sr .pl-sre /* string.regexp source.ruby.embedded */ { 52 | color: #183691; 53 | } 54 | 55 | .pl-v /* variable */ { 56 | color: #ed6a43; 57 | } 58 | 59 | .pl-id /* invalid.deprecated */ { 60 | color: #b52a1d; 61 | } 62 | 63 | .pl-ii /* invalid.illegal */ { 64 | background-color: #b52a1d; 65 | color: #f8f8f8; 66 | } 67 | 68 | .pl-sr .pl-cce /* string.regexp constant.character.escape */ { 69 | color: #63a35c; 70 | font-weight: bold; 71 | } 72 | 73 | .pl-ml /* markup.list */ { 74 | color: #693a17; 75 | } 76 | 77 | .pl-mh /* markup.heading */, 78 | .pl-mh .pl-en /* markup.heading entity.name */, 79 | .pl-ms /* meta.separator */ { 80 | color: #1d3e81; 81 | font-weight: bold; 82 | } 83 | 84 | .pl-mq /* markup.quote */ { 85 | color: #008080; 86 | } 87 | 88 | .pl-mi /* markup.italic */ { 89 | color: #333; 90 | font-style: italic; 91 | } 92 | 93 | .pl-mb /* markup.bold */ { 94 | color: #333; 95 | font-weight: bold; 96 | } 97 | 98 | .pl-md /* markup.deleted, meta.diff.header.from-file */ { 99 | background-color: #ffecec; 100 | color: #bd2c00; 101 | } 102 | 103 | .pl-mi1 /* markup.inserted, meta.diff.header.to-file */ { 104 | background-color: #eaffea; 105 | color: #55a532; 106 | } 107 | 108 | .pl-mdr /* meta.diff.range */ { 109 | color: #795da3; 110 | font-weight: bold; 111 | } 112 | 113 | .pl-mo /* meta.output */ { 114 | color: #1d3e81; 115 | } 116 | 117 | -------------------------------------------------------------------------------- /assets/nipplejs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoannmoinet/nipplejs/9fca6e72bb99318d69092075015140179f4e32bf/assets/nipplejs.jpg -------------------------------------------------------------------------------- /assets/nipplejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yoannmoinet/nipplejs/9fca6e72bb99318d69092075015140179f4e32bf/assets/nipplejs.png -------------------------------------------------------------------------------- /assets/styles.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700); 2 | 3 | body { 4 | padding:50px; 5 | font:14px/1.5 Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; 6 | color:#777; 7 | font-weight:300; 8 | } 9 | 10 | h1, h2, h3, h4, h5, h6 { 11 | color:#222; 12 | margin:0 0 20px; 13 | } 14 | 15 | p, ul, ol, table, pre, dl { 16 | margin:0 0 20px; 17 | } 18 | 19 | h1, h2, h3 { 20 | line-height:1.1; 21 | } 22 | 23 | h1 { 24 | font-size:28px; 25 | } 26 | 27 | h2 { 28 | color:#393939; 29 | } 30 | 31 | h3, h4, h5, h6 { 32 | color:#494949; 33 | } 34 | 35 | a { 36 | color:#39c; 37 | font-weight:400; 38 | text-decoration:none; 39 | } 40 | 41 | a small { 42 | font-size:11px; 43 | color:#777; 44 | margin-top:-0.6em; 45 | display:block; 46 | } 47 | 48 | .wrapper { 49 | width:860px; 50 | margin:0 auto; 51 | } 52 | 53 | blockquote { 54 | border-left:1px solid #e5e5e5; 55 | margin:0; 56 | padding:0 0 0 20px; 57 | font-style:italic; 58 | } 59 | 60 | code, pre { 61 | font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; 62 | color:#333; 63 | font-size:12px; 64 | } 65 | 66 | pre { 67 | padding:8px 15px; 68 | background: #f8f8f8; 69 | border-radius:5px; 70 | border:1px solid #e5e5e5; 71 | overflow-x: auto; 72 | } 73 | 74 | table { 75 | width:100%; 76 | border-collapse:collapse; 77 | } 78 | 79 | th, td { 80 | text-align:left; 81 | padding:5px 10px; 82 | border-bottom:1px solid #e5e5e5; 83 | } 84 | 85 | dt { 86 | color:#444; 87 | font-weight:700; 88 | } 89 | 90 | th { 91 | color:#444; 92 | } 93 | 94 | img { 95 | max-width:100%; 96 | } 97 | 98 | header { 99 | width:270px; 100 | float:left; 101 | position:fixed; 102 | } 103 | 104 | header ul { 105 | list-style:none; 106 | height:40px; 107 | 108 | padding:0; 109 | 110 | background: #eee; 111 | background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); 112 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd)); 113 | background: -webkit-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 114 | background: -o-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 115 | background: -ms-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 116 | background: linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 117 | 118 | border-radius:5px; 119 | border:1px solid #d2d2d2; 120 | box-shadow:inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0; 121 | width:270px; 122 | } 123 | 124 | header li { 125 | width:89px; 126 | float:left; 127 | border-right:1px solid #d2d2d2; 128 | height:40px; 129 | } 130 | 131 | header ul a { 132 | line-height:1; 133 | font-size:11px; 134 | color:#999; 135 | display:block; 136 | text-align:center; 137 | padding-top:6px; 138 | height:40px; 139 | } 140 | 141 | strong { 142 | color:#222; 143 | font-weight:700; 144 | } 145 | 146 | header ul li + li { 147 | width:88px; 148 | border-left:1px solid #fff; 149 | } 150 | 151 | header ul li + li + li { 152 | border-right:none; 153 | width:89px; 154 | } 155 | 156 | header ul a strong { 157 | font-size:14px; 158 | display:block; 159 | color:#222; 160 | } 161 | 162 | section { 163 | width:500px; 164 | float:right; 165 | padding-bottom:50px; 166 | } 167 | 168 | small { 169 | font-size:11px; 170 | } 171 | 172 | hr { 173 | border:0; 174 | background:#e5e5e5; 175 | height:1px; 176 | margin:0 0 20px; 177 | } 178 | 179 | footer { 180 | width:270px; 181 | float:left; 182 | position:fixed; 183 | bottom:50px; 184 | } 185 | 186 | @media print, screen and (max-width: 960px) { 187 | 188 | div.wrapper { 189 | width:auto; 190 | margin:0; 191 | } 192 | 193 | header, section, footer { 194 | float:none; 195 | position:static; 196 | width:auto; 197 | } 198 | 199 | header { 200 | padding-right:320px; 201 | } 202 | 203 | section { 204 | border:1px solid #e5e5e5; 205 | border-width:1px 0; 206 | padding:20px 0; 207 | margin:0 0 20px; 208 | } 209 | 210 | header a small { 211 | display:inline; 212 | } 213 | 214 | header ul { 215 | position:absolute; 216 | right:50px; 217 | top:52px; 218 | } 219 | } 220 | 221 | #zone_joystick { 222 | position: relative; 223 | background: silver; 224 | box-sizing: content-box; 225 | height: 450px; 226 | } 227 | #debug { 228 | position: absolute; 229 | top: 0; 230 | right: 0; 231 | padding: 5px 0 5px 0; 232 | width: 300px; 233 | height: 450px; 234 | box-sizing: padding-box; 235 | color: white; 236 | } 237 | #debug ul { 238 | list-style: none; 239 | } 240 | #debug>ul { 241 | padding-left: 0; 242 | } 243 | #debug .data { 244 | color: #333333; 245 | font-weight: 800; 246 | } 247 | 248 | @media print, screen and (max-width: 720px) { 249 | body { 250 | word-wrap:break-word; 251 | } 252 | 253 | header { 254 | padding:0; 255 | } 256 | 257 | header ul, header p.view { 258 | position:static; 259 | } 260 | 261 | pre, code { 262 | word-wrap:normal; 263 | } 264 | } 265 | 266 | @media print, screen and (max-width: 480px) { 267 | body { 268 | padding:15px; 269 | } 270 | 271 | header ul { 272 | display:none; 273 | } 274 | } 275 | 276 | @media print { 277 | body { 278 | padding:0.4in; 279 | font-size:12pt; 280 | color:#444; 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /bin/changelog.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | var fs = require('fs'); 3 | var conventionalChangelog = require('conventional-changelog'); 4 | var changelogFile = fs.createWriteStream('CHANGELOG.md'); 5 | var exec = require('child_process').exec; 6 | 7 | conventionalChangelog({ 8 | preset: 'angular' 9 | }, {}, { 10 | // You'll want to add your first commit's hash in here, 11 | // otherwise it will take from the latest tag only. 12 | from: '', 13 | to: 'HEAD' 14 | }).pipe(changelogFile); 15 | 16 | // Commit what's changed in the changelog. 17 | changelogFile.on('unpipe', function (src) { 18 | console.log('commiting changes to CHANGELOG.md'); 19 | exec('git add CHANGELOG.md && git commit -m "docs: changelog"', function (err) { 20 | if (!err) { 21 | process.exit(0); 22 | } 23 | console.error(err); 24 | process.exit(1); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /bin/copyToGhPages.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | const exec = require('child_process').exec; 3 | const fs = require('fs'); 4 | 5 | const isWin = /^win/.test(process.platform); 6 | const mv = isWin ? 'move' : 'mv'; 7 | 8 | queue([ 9 | stash, 10 | checkoutPage, 11 | importReadme, 12 | modifyFile, 13 | commit, 14 | push, 15 | checkoutMaster 16 | ]); 17 | 18 | function queue (fns) { 19 | // Execute and remove the first function. 20 | const fn = fns.shift(); 21 | fn((err) => { 22 | if (!err) { 23 | if (fns.length) { 24 | // If we still have functions 25 | // we continue 26 | queue(fns); 27 | } else { 28 | // We exist if we've finished 29 | process.exit(0); 30 | } 31 | } else { 32 | // We log if we have an error. 33 | console.error(fn.name + ': ', err); 34 | process.exit(1); 35 | } 36 | }); 37 | } 38 | 39 | function stash (next) { 40 | console.log('- stash changes'); 41 | exec('git stash', next); 42 | } 43 | 44 | function checkoutPage (next) { 45 | console.log(' - checkout gh-pages.'); 46 | exec('git checkout gh-pages', next); 47 | } 48 | 49 | function importReadme (next) { 50 | console.log(' - checkout README from master and rename it to index.md'); 51 | exec(` 52 | git checkout master -- README.md && 53 | git reset README.md && 54 | ${mv} README.md index.md 55 | `, next); 56 | } 57 | 58 | function modifyFile (next) { 59 | console.log(' - reading the new index.md'); 60 | fs.readFile('index.md', (err, data) => { 61 | if (err) { 62 | next(err); 63 | return; 64 | } 65 | console.log(' - writing the new content for Jekyll'); 66 | const body = data.toString().split('\n'); 67 | body.splice(0, 3, '---', 'layout: index', '---'); 68 | fs.writeFile('index.md', body.join('\n'), next); 69 | }); 70 | } 71 | 72 | function commit (next) { 73 | console.log(' - commit latest doc to gh-pages'); 74 | exec(` 75 | git add index.md && 76 | git commit -m "chore: sync from master" 77 | `, next); 78 | } 79 | 80 | function push (next) { 81 | console.log(' - push latest doc to gh-pages'); 82 | exec('git push origin gh-pages', next); 83 | } 84 | 85 | function checkoutMaster (next) { 86 | console.log(' - checkout master.'); 87 | exec('git checkout master', next); 88 | } 89 | -------------------------------------------------------------------------------- /example/codepen-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NippleJS 6 | 7 | 8 | 9 | 97 | 98 | 99 |

First thing first. A demo. Executed with this configuration :

100 |
101 |
102 | <script src="./dist/nipplejs.min.js"></script>
103 | <script>
104 |     var dynamic = nipplejs.create({
105 |         zone: document.getElementById('dynamic'),
106 |         color: 'blue'
107 |     });
108 | </script>
109 |
110 |
111 |
112 | <script src="./dist/nipplejs.min.js"></script>
113 | <script>
114 |     var semi = nipplejs.create({
115 |         zone: document.getElementById('semi'),
116 |         mode: 'semi',
117 |         catchDistance: 150,
118 |         color: 'white'
119 |     });
120 | </script>
121 |
122 |
123 |
124 | <script src="./dist/nipplejs.min.js"></script>
125 | <script>
126 |     var static = nipplejs.create({
127 |         zone: document.getElementById('static'),
128 |         mode: 'static',
129 |         position: {left: '50%', top: '50%'},
130 |         color: 'red'
131 |     });
132 | </script>
133 |
134 |

Choose the mode you want to test.

135 |
136 |
dynamic
137 |
semi
138 |
static
139 |
140 |
141 |
142 |
    143 |
  • 144 | position : 145 |
      146 |
    • x :
    • 147 |
    • y :
    • 148 |
    149 |
  • 150 |
  • force :
  • 151 |
  • pressure :
  • 152 |
  • distance :
  • 153 |
  • 154 | angle : 155 |
      156 |
    • radian :
    • 157 |
    • degree :
    • 158 |
    159 |
  • 160 |
  • 161 | direction : 162 |
      163 |
    • x :
    • 164 |
    • y :
    • 165 |
    • angle :
    • 166 |
    167 |
  • 168 |
169 |
170 |
171 |

dynamic

172 |

semi

173 |

static

174 |
175 |
176 |
177 | > Open the demo in a new window. 178 |
179 | 180 | 306 | 307 | 308 | -------------------------------------------------------------------------------- /example/dual-joysticks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NippleJS 6 | 7 | 36 | 37 | 38 |
39 | 40 | 41 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /example/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/lock-axes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NippleJS 6 | 7 | 36 | 37 | 38 |
39 | 40 | 41 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /example/reset-joystick.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NippleJS 6 | 10 | 40 | 41 | 42 |
43 | 44 | 45 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /example/square-dual-joysticks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NippleJS 6 | 10 | 40 | 41 | 42 |
43 | 44 | 45 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.10.2", 3 | "name": "nipplejs", 4 | "description": "A virtual joystick for touch capable interfaces", 5 | "author": "Yoann Moinet (https://yoannmoi.net)", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/yoannmoinet/nipplejs.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/yoannmoinet/nipplejs/issues" 13 | }, 14 | "files": [ 15 | "dist/*", 16 | "src/*", 17 | "types/*" 18 | ], 19 | "homepage": "https://github.com/yoannmoinet/nipplejs", 20 | "main": "./dist/nipplejs.js", 21 | "browser": "./dist/nipplejs.js", 22 | "module": "./src/index.js", 23 | "types": "./types/index.d.ts", 24 | "directories": { 25 | "lib": "src", 26 | "test": "test", 27 | "example": "example" 28 | }, 29 | "scripts": { 30 | "postversion": "npm run changelog", 31 | "prepack": "NODE_ENV=production npm run build", 32 | "build": "webpack --config webpack.config.js", 33 | "build:dev": "webpack-dev-server --config webpack.config.js", 34 | "test": "casperjs test ./test/nipplejs.casper.js", 35 | "changelog": "node ./bin/changelog.js", 36 | "start": "npm run build:dev -- --progress --watch", 37 | "toc": "markdown-toc -i README.md", 38 | "copyGh": "node ./bin/copyToGhPages.js" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "7.5.5", 42 | "@babel/preset-env": "7.5.5", 43 | "babel-loader": "8.0.6", 44 | "babel-plugin-add-module-exports": "1.0.2", 45 | "conventional-changelog": "3.1.24", 46 | "eslint": "5.10.0", 47 | "eslint-loader": "2.2.1", 48 | "markdown-toc": "1.2.0", 49 | "webpack": "4.39.2", 50 | "webpack-cli": "3.3.7", 51 | "webpack-dev-server": "4.11.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/collection.js: -------------------------------------------------------------------------------- 1 | import Nipple from './nipple'; 2 | import Super from './super'; 3 | import * as u from './utils'; 4 | 5 | /////////////////////////// 6 | /// THE COLLECTION /// 7 | /////////////////////////// 8 | 9 | function Collection (manager, options) { 10 | var self = this; 11 | self.nipples = []; 12 | self.idles = []; 13 | self.actives = []; 14 | self.ids = []; 15 | self.pressureIntervals = {}; 16 | self.manager = manager; 17 | self.id = Collection.id; 18 | Collection.id += 1; 19 | 20 | // Defaults 21 | self.defaults = { 22 | zone: document.body, 23 | multitouch: false, 24 | maxNumberOfNipples: 10, 25 | mode: 'dynamic', 26 | position: {top: 0, left: 0}, 27 | catchDistance: 200, 28 | size: 100, 29 | threshold: 0.1, 30 | color: 'white', 31 | fadeTime: 250, 32 | dataOnly: false, 33 | restJoystick: true, 34 | restOpacity: 0.5, 35 | lockX: false, 36 | lockY: false, 37 | shape: 'circle', 38 | dynamicPage: false, 39 | follow: false 40 | }; 41 | 42 | self.config(options); 43 | 44 | // Overwrites 45 | if (self.options.mode === 'static' || self.options.mode === 'semi') { 46 | self.options.multitouch = false; 47 | } 48 | 49 | if (!self.options.multitouch) { 50 | self.options.maxNumberOfNipples = 1; 51 | } 52 | const computedStyle = getComputedStyle(self.options.zone.parentElement); 53 | if (computedStyle && computedStyle.display === 'flex') { 54 | self.parentIsFlex = true; 55 | } 56 | 57 | self.updateBox(); 58 | self.prepareNipples(); 59 | self.bindings(); 60 | self.begin(); 61 | 62 | return self.nipples; 63 | } 64 | 65 | Collection.prototype = new Super(); 66 | Collection.constructor = Collection; 67 | Collection.id = 0; 68 | 69 | Collection.prototype.prepareNipples = function () { 70 | var self = this; 71 | var nips = self.nipples; 72 | 73 | // Public API Preparation. 74 | nips.on = self.on.bind(self); 75 | nips.off = self.off.bind(self); 76 | nips.options = self.options; 77 | nips.destroy = self.destroy.bind(self); 78 | nips.ids = self.ids; 79 | nips.id = self.id; 80 | nips.processOnMove = self.processOnMove.bind(self); 81 | nips.processOnEnd = self.processOnEnd.bind(self); 82 | nips.get = function (id) { 83 | if (id === undefined) { 84 | return nips[0]; 85 | } 86 | for (var i = 0, max = nips.length; i < max; i += 1) { 87 | if (nips[i].identifier === id) { 88 | return nips[i]; 89 | } 90 | } 91 | return false; 92 | }; 93 | }; 94 | 95 | Collection.prototype.bindings = function () { 96 | var self = this; 97 | // Touch start event. 98 | self.bindEvt(self.options.zone, 'start'); 99 | // Avoid native touch actions (scroll, zoom etc...) on the zone. 100 | self.options.zone.style.touchAction = 'none'; 101 | self.options.zone.style.msTouchAction = 'none'; 102 | }; 103 | 104 | Collection.prototype.begin = function () { 105 | var self = this; 106 | var opts = self.options; 107 | 108 | // We place our static nipple 109 | // if needed. 110 | if (opts.mode === 'static') { 111 | var nipple = self.createNipple( 112 | opts.position, 113 | self.manager.getIdentifier() 114 | ); 115 | // Add it to the dom. 116 | nipple.add(); 117 | // Store it in idles. 118 | self.idles.push(nipple); 119 | } 120 | }; 121 | 122 | // Nipple Factory 123 | Collection.prototype.createNipple = function (position, identifier) { 124 | var self = this; 125 | var scroll = self.manager.scroll; 126 | var toPutOn = {}; 127 | var opts = self.options; 128 | var offset = { 129 | x: self.parentIsFlex ? scroll.x : (scroll.x + self.box.left), 130 | y: self.parentIsFlex ? scroll.y : (scroll.y + self.box.top) 131 | }; 132 | 133 | if (position.x && position.y) { 134 | toPutOn = { 135 | x: position.x - offset.x, 136 | y: position.y - offset.y 137 | }; 138 | } else if ( 139 | position.top || 140 | position.right || 141 | position.bottom || 142 | position.left 143 | ) { 144 | 145 | // We need to compute the position X / Y of the joystick. 146 | var dumb = document.createElement('DIV'); 147 | dumb.style.display = 'hidden'; 148 | dumb.style.top = position.top; 149 | dumb.style.right = position.right; 150 | dumb.style.bottom = position.bottom; 151 | dumb.style.left = position.left; 152 | dumb.style.position = 'absolute'; 153 | 154 | opts.zone.appendChild(dumb); 155 | var dumbBox = dumb.getBoundingClientRect(); 156 | opts.zone.removeChild(dumb); 157 | 158 | toPutOn = position; 159 | position = { 160 | x: dumbBox.left + scroll.x, 161 | y: dumbBox.top + scroll.y 162 | }; 163 | } 164 | 165 | var nipple = new Nipple(self, { 166 | color: opts.color, 167 | size: opts.size, 168 | threshold: opts.threshold, 169 | fadeTime: opts.fadeTime, 170 | dataOnly: opts.dataOnly, 171 | restJoystick: opts.restJoystick, 172 | restOpacity: opts.restOpacity, 173 | mode: opts.mode, 174 | identifier: identifier, 175 | position: position, 176 | zone: opts.zone, 177 | frontPosition: { 178 | x: 0, 179 | y: 0 180 | }, 181 | shape: opts.shape 182 | }); 183 | 184 | if (!opts.dataOnly) { 185 | u.applyPosition(nipple.ui.el, toPutOn); 186 | u.applyPosition(nipple.ui.front, nipple.frontPosition); 187 | } 188 | self.nipples.push(nipple); 189 | self.trigger('added ' + nipple.identifier + ':added', nipple); 190 | self.manager.trigger('added ' + nipple.identifier + ':added', nipple); 191 | 192 | self.bindNipple(nipple); 193 | 194 | return nipple; 195 | }; 196 | 197 | Collection.prototype.updateBox = function () { 198 | var self = this; 199 | self.box = self.options.zone.getBoundingClientRect(); 200 | }; 201 | 202 | Collection.prototype.bindNipple = function (nipple) { 203 | var self = this; 204 | var type; 205 | // Bubble up identified events. 206 | var handler = function (evt, data) { 207 | // Identify the event type with the nipple's id. 208 | type = evt.type + ' ' + data.id + ':' + evt.type; 209 | self.trigger(type, data); 210 | }; 211 | 212 | // When it gets destroyed. 213 | nipple.on('destroyed', self.onDestroyed.bind(self)); 214 | 215 | // Other events that will get bubbled up. 216 | nipple.on('shown hidden rested dir plain', handler); 217 | nipple.on('dir:up dir:right dir:down dir:left', handler); 218 | nipple.on('plain:up plain:right plain:down plain:left', handler); 219 | }; 220 | 221 | Collection.prototype.pressureFn = function (touch, nipple, identifier) { 222 | var self = this; 223 | var previousPressure = 0; 224 | clearInterval(self.pressureIntervals[identifier]); 225 | // Create an interval that will read the pressure every 100ms 226 | self.pressureIntervals[identifier] = setInterval(function () { 227 | var pressure = touch.force || touch.pressure || 228 | touch.webkitForce || 0; 229 | if (pressure !== previousPressure) { 230 | nipple.trigger('pressure', pressure); 231 | self.trigger('pressure ' + 232 | nipple.identifier + ':pressure', pressure); 233 | previousPressure = pressure; 234 | } 235 | }.bind(self), 100); 236 | }; 237 | 238 | Collection.prototype.onstart = function (evt) { 239 | var self = this; 240 | var opts = self.options; 241 | var origEvt = evt; 242 | evt = u.prepareEvent(evt); 243 | 244 | // Update the box position 245 | self.updateBox(); 246 | 247 | var process = function (touch) { 248 | // If we can create new nipples 249 | // meaning we don't have more active nipples than we should. 250 | if (self.actives.length < opts.maxNumberOfNipples) { 251 | self.processOnStart(touch); 252 | } 253 | else if(origEvt.type.match(/^touch/)){ 254 | // zombies occur when end event is not received on Safari 255 | // first touch removed before second touch, we need to catch up... 256 | // so remove where touches in manager that no longer exist 257 | Object.keys(self.manager.ids).forEach(function(k){ 258 | if(Object.values(origEvt.touches).findIndex(function(t){return t.identifier===k;}) < 0){ 259 | // manager has id that doesn't exist in touches 260 | var e = [evt[0]]; 261 | e.identifier = k; 262 | self.processOnEnd(e); 263 | } 264 | }); 265 | if(self.actives.length < opts.maxNumberOfNipples){ 266 | self.processOnStart(touch); 267 | } 268 | } 269 | }; 270 | 271 | u.map(evt, process); 272 | 273 | // We ask upstream to bind the document 274 | // on 'move' and 'end' 275 | self.manager.bindDocument(); 276 | return false; 277 | }; 278 | 279 | Collection.prototype.processOnStart = function (evt) { 280 | var self = this; 281 | var opts = self.options; 282 | var indexInIdles; 283 | var identifier = self.manager.getIdentifier(evt); 284 | var pressure = evt.force || evt.pressure || evt.webkitForce || 0; 285 | var position = { 286 | x: evt.pageX, 287 | y: evt.pageY 288 | }; 289 | 290 | var nipple = self.getOrCreate(identifier, position); 291 | 292 | // Update its touch identifier 293 | if (nipple.identifier !== identifier) { 294 | self.manager.removeIdentifier(nipple.identifier); 295 | } 296 | nipple.identifier = identifier; 297 | 298 | var process = function (nip) { 299 | // Trigger the start. 300 | nip.trigger('start', nip); 301 | self.trigger('start ' + nip.id + ':start', nip); 302 | 303 | nip.show(); 304 | if (pressure > 0) { 305 | self.pressureFn(evt, nip, nip.identifier); 306 | } 307 | // Trigger the first move event. 308 | self.processOnMove(evt); 309 | }; 310 | 311 | // Transfer it from idles to actives. 312 | if ((indexInIdles = self.idles.indexOf(nipple)) >= 0) { 313 | self.idles.splice(indexInIdles, 1); 314 | } 315 | 316 | // Store the nipple in the actives array 317 | self.actives.push(nipple); 318 | self.ids.push(nipple.identifier); 319 | 320 | if (opts.mode !== 'semi') { 321 | process(nipple); 322 | } else { 323 | // In semi we check the distance of the touch 324 | // to decide if we have to reset the nipple 325 | var distance = u.distance(position, nipple.position); 326 | if (distance <= opts.catchDistance) { 327 | process(nipple); 328 | } else { 329 | nipple.destroy(); 330 | self.processOnStart(evt); 331 | return; 332 | } 333 | } 334 | 335 | return nipple; 336 | }; 337 | 338 | Collection.prototype.getOrCreate = function (identifier, position) { 339 | var self = this; 340 | var opts = self.options; 341 | var nipple; 342 | 343 | // If we're in static or semi, we might already have an active. 344 | if (/(semi|static)/.test(opts.mode)) { 345 | // Get the active one. 346 | // TODO: Multi-touche for semi and static will start here. 347 | // Return the nearest one. 348 | nipple = self.idles[0]; 349 | if (nipple) { 350 | self.idles.splice(0, 1); 351 | return nipple; 352 | } 353 | 354 | if (opts.mode === 'semi') { 355 | // If we're in semi mode, we need to create one. 356 | return self.createNipple(position, identifier); 357 | } 358 | 359 | // eslint-disable-next-line no-console 360 | console.warn('Coudln\'t find the needed nipple.'); 361 | return false; 362 | } 363 | // In dynamic, we create a new one. 364 | nipple = self.createNipple(position, identifier); 365 | return nipple; 366 | }; 367 | 368 | Collection.prototype.processOnMove = function (evt) { 369 | var self = this; 370 | var opts = self.options; 371 | var identifier = self.manager.getIdentifier(evt); 372 | var nipple = self.nipples.get(identifier); 373 | var scroll = self.manager.scroll; 374 | 375 | // If we're moving without pressing 376 | // it's that we went out the active zone 377 | if (!u.isPressed(evt)) { 378 | this.processOnEnd(evt); 379 | return; 380 | } 381 | 382 | if (!nipple) { 383 | // This is here just for safety. 384 | // It shouldn't happen. 385 | // eslint-disable-next-line no-console 386 | console.error('Found zombie joystick with ID ' + identifier); 387 | self.manager.removeIdentifier(identifier); 388 | return; 389 | } 390 | 391 | if (opts.dynamicPage) { 392 | var elBox = nipple.el.getBoundingClientRect(); 393 | nipple.position = { 394 | x: scroll.x + elBox.left, 395 | y: scroll.y + elBox.top 396 | }; 397 | } 398 | 399 | nipple.identifier = identifier; 400 | 401 | var size = nipple.options.size / 2; 402 | var pos = { 403 | x: evt.pageX, 404 | y: evt.pageY 405 | }; 406 | 407 | if (opts.lockX){ 408 | pos.y = nipple.position.y; 409 | } 410 | if (opts.lockY) { 411 | pos.x = nipple.position.x; 412 | } 413 | 414 | var dist = u.distance(pos, nipple.position); 415 | var angle = u.angle(pos, nipple.position); 416 | var rAngle = u.radians(angle); 417 | var force = dist / size; 418 | 419 | var raw = { 420 | distance: dist, 421 | position: pos 422 | }; 423 | 424 | // Clamp the position 425 | var clamped_dist; 426 | var clamped_pos; 427 | if (nipple.options.shape === 'circle') { 428 | // Clamp to a circle 429 | clamped_dist = Math.min(dist, size); 430 | clamped_pos = u.findCoord(nipple.position, clamped_dist, angle); 431 | } else { 432 | // Clamp to a square 433 | clamped_pos = u.clamp(pos, nipple.position, size); 434 | clamped_dist = u.distance(clamped_pos, nipple.position); 435 | } 436 | 437 | if (opts.follow) { 438 | // follow behaviour 439 | if (dist > size) { 440 | let delta_x = pos.x - clamped_pos.x; 441 | let delta_y = pos.y - clamped_pos.y; 442 | nipple.position.x += delta_x; 443 | nipple.position.y += delta_y; 444 | nipple.el.style.top = (nipple.position.y - (self.box.top + scroll.y)) + 'px'; 445 | nipple.el.style.left = (nipple.position.x - (self.box.left + scroll.x)) + 'px'; 446 | 447 | dist = u.distance(pos, nipple.position); 448 | } 449 | } else { 450 | // clamp behaviour 451 | pos = clamped_pos; 452 | dist = clamped_dist; 453 | } 454 | 455 | var xPosition = pos.x - nipple.position.x; 456 | var yPosition = pos.y - nipple.position.y; 457 | 458 | nipple.frontPosition = { 459 | x: xPosition, 460 | y: yPosition 461 | }; 462 | 463 | if (!opts.dataOnly) { 464 | nipple.ui.front.style.transform = 'translate(' + xPosition + 'px,' + yPosition + 'px)'; 465 | } 466 | 467 | // Prepare event's datas. 468 | var toSend = { 469 | identifier: nipple.identifier, 470 | position: pos, 471 | force: force, 472 | pressure: evt.force || evt.pressure || evt.webkitForce || 0, 473 | distance: dist, 474 | angle: { 475 | radian: rAngle, 476 | degree: angle 477 | }, 478 | vector: { 479 | x: xPosition / size, 480 | y: - yPosition / size 481 | }, 482 | raw: raw, 483 | instance: nipple, 484 | lockX: opts.lockX, 485 | lockY: opts.lockY 486 | }; 487 | 488 | // Compute the direction's datas. 489 | toSend = nipple.computeDirection(toSend); 490 | 491 | // Offset angles to follow units circle. 492 | toSend.angle = { 493 | radian: u.radians(180 - angle), 494 | degree: 180 - angle 495 | }; 496 | 497 | // Send everything to everyone. 498 | nipple.trigger('move', toSend); 499 | self.trigger('move ' + nipple.id + ':move', toSend); 500 | }; 501 | 502 | Collection.prototype.processOnEnd = function (evt) { 503 | var self = this; 504 | var opts = self.options; 505 | var identifier = self.manager.getIdentifier(evt); 506 | var nipple = self.nipples.get(identifier); 507 | var removedIdentifier = self.manager.removeIdentifier(nipple.identifier); 508 | 509 | if (!nipple) { 510 | return; 511 | } 512 | 513 | if (!opts.dataOnly) { 514 | nipple.hide(function () { 515 | if (opts.mode === 'dynamic') { 516 | nipple.trigger('removed', nipple); 517 | self.trigger('removed ' + nipple.id + ':removed', nipple); 518 | self.manager 519 | .trigger('removed ' + nipple.id + ':removed', nipple); 520 | nipple.destroy(); 521 | } 522 | }); 523 | } 524 | 525 | // Clear the pressure interval reader 526 | clearInterval(self.pressureIntervals[nipple.identifier]); 527 | 528 | // Reset the direciton of the nipple, to be able to trigger a new direction 529 | // on start. 530 | nipple.resetDirection(); 531 | 532 | nipple.trigger('end', nipple); 533 | self.trigger('end ' + nipple.id + ':end', nipple); 534 | 535 | // Remove identifier from our bank. 536 | if (self.ids.indexOf(nipple.identifier) >= 0) { 537 | self.ids.splice(self.ids.indexOf(nipple.identifier), 1); 538 | } 539 | 540 | // Clean our actives array. 541 | if (self.actives.indexOf(nipple) >= 0) { 542 | self.actives.splice(self.actives.indexOf(nipple), 1); 543 | } 544 | 545 | if (/(semi|static)/.test(opts.mode)) { 546 | // Transfer nipple from actives to idles 547 | // if we're in semi or static mode. 548 | self.idles.push(nipple); 549 | } else if (self.nipples.indexOf(nipple) >= 0) { 550 | // Only if we're not in semi or static mode 551 | // we can remove the instance. 552 | self.nipples.splice(self.nipples.indexOf(nipple), 1); 553 | } 554 | 555 | // We unbind move and end. 556 | self.manager.unbindDocument(); 557 | 558 | // We add back the identifier of the idle nipple; 559 | if (/(semi|static)/.test(opts.mode)) { 560 | self.manager.ids[removedIdentifier.id] = removedIdentifier.identifier; 561 | } 562 | }; 563 | 564 | // Remove destroyed nipple from the lists 565 | Collection.prototype.onDestroyed = function(evt, nipple) { 566 | var self = this; 567 | if (self.nipples.indexOf(nipple) >= 0) { 568 | self.nipples.splice(self.nipples.indexOf(nipple), 1); 569 | } 570 | if (self.actives.indexOf(nipple) >= 0) { 571 | self.actives.splice(self.actives.indexOf(nipple), 1); 572 | } 573 | if (self.idles.indexOf(nipple) >= 0) { 574 | self.idles.splice(self.idles.indexOf(nipple), 1); 575 | } 576 | if (self.ids.indexOf(nipple.identifier) >= 0) { 577 | self.ids.splice(self.ids.indexOf(nipple.identifier), 1); 578 | } 579 | 580 | // Remove the identifier from our bank 581 | self.manager.removeIdentifier(nipple.identifier); 582 | 583 | // We unbind move and end. 584 | self.manager.unbindDocument(); 585 | }; 586 | 587 | // Cleanly destroy the manager 588 | Collection.prototype.destroy = function () { 589 | var self = this; 590 | self.unbindEvt(self.options.zone, 'start'); 591 | 592 | // Destroy nipples. 593 | self.nipples.forEach(function(nipple) { 594 | nipple.destroy(); 595 | }); 596 | 597 | // Clean 3DTouch intervals. 598 | for (var i in self.pressureIntervals) { 599 | if (self.pressureIntervals.hasOwnProperty(i)) { 600 | clearInterval(self.pressureIntervals[i]); 601 | } 602 | } 603 | 604 | // Notify the manager passing the instance 605 | self.trigger('destroyed', self.nipples); 606 | // We unbind move and end. 607 | self.manager.unbindDocument(); 608 | // Unbind everything. 609 | self.off(); 610 | }; 611 | 612 | export default Collection; 613 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Manager from './manager'; 2 | 3 | const factory = new Manager(); 4 | export default { 5 | create: function (options) { 6 | return factory.create(options); 7 | }, 8 | factory: factory 9 | }; 10 | -------------------------------------------------------------------------------- /src/manager.js: -------------------------------------------------------------------------------- 1 | import Collection from './collection'; 2 | import Super from './super'; 3 | import * as u from './utils'; 4 | 5 | /////////////////////// 6 | /// MANAGER /// 7 | /////////////////////// 8 | 9 | function Manager (options) { 10 | var self = this; 11 | self.ids = {}; 12 | self.index = 0; 13 | self.collections = []; 14 | self.scroll = u.getScroll(); 15 | 16 | self.config(options); 17 | self.prepareCollections(); 18 | 19 | // Listen for resize, to reposition every joysticks 20 | var resizeHandler = function () { 21 | var pos; 22 | self.collections.forEach(function (collection) { 23 | collection.forEach(function (nipple) { 24 | pos = nipple.el.getBoundingClientRect(); 25 | nipple.position = { 26 | x: self.scroll.x + pos.left, 27 | y: self.scroll.y + pos.top 28 | }; 29 | }); 30 | }); 31 | }; 32 | u.bindEvt(window, 'resize', function () { 33 | u.throttle(resizeHandler); 34 | }); 35 | 36 | // Listen for scrolls, so we have a global scroll value 37 | // without having to request it all the time. 38 | var scrollHandler = function () { 39 | self.scroll = u.getScroll(); 40 | }; 41 | u.bindEvt(window, 'scroll', function () { 42 | u.throttle(scrollHandler); 43 | }); 44 | 45 | return self.collections; 46 | } 47 | 48 | Manager.prototype = new Super(); 49 | Manager.constructor = Manager; 50 | 51 | Manager.prototype.prepareCollections = function () { 52 | var self = this; 53 | // Public API Preparation. 54 | self.collections.create = self.create.bind(self); 55 | // Listen to anything 56 | self.collections.on = self.on.bind(self); 57 | // Unbind general events 58 | self.collections.off = self.off.bind(self); 59 | // Destroy everything 60 | self.collections.destroy = self.destroy.bind(self); 61 | // Get any nipple 62 | self.collections.get = function (id) { 63 | var nipple; 64 | // Use .every() to break the loop as soon as found. 65 | self.collections.every(function (collection) { 66 | nipple = collection.get(id); 67 | return nipple ? false : true; 68 | }); 69 | return nipple; 70 | }; 71 | }; 72 | 73 | Manager.prototype.create = function (options) { 74 | return this.createCollection(options); 75 | }; 76 | 77 | // Collection Factory 78 | Manager.prototype.createCollection = function (options) { 79 | var self = this; 80 | var collection = new Collection(self, options); 81 | 82 | self.bindCollection(collection); 83 | self.collections.push(collection); 84 | 85 | return collection; 86 | }; 87 | 88 | Manager.prototype.bindCollection = function (collection) { 89 | var self = this; 90 | var type; 91 | // Bubble up identified events. 92 | var handler = function (evt, data) { 93 | // Identify the event type with the nipple's identifier. 94 | type = evt.type + ' ' + data.id + ':' + evt.type; 95 | self.trigger(type, data); 96 | }; 97 | 98 | // When it gets destroyed we clean. 99 | collection.on('destroyed', self.onDestroyed.bind(self)); 100 | 101 | // Other events that will get bubbled up. 102 | collection.on('shown hidden rested dir plain', handler); 103 | collection.on('dir:up dir:right dir:down dir:left', handler); 104 | collection.on('plain:up plain:right plain:down plain:left', handler); 105 | }; 106 | 107 | Manager.prototype.bindDocument = function () { 108 | var self = this; 109 | // Bind only if not already binded 110 | if (!self.binded) { 111 | self.bindEvt(document, 'move') 112 | .bindEvt(document, 'end'); 113 | self.binded = true; 114 | } 115 | }; 116 | 117 | Manager.prototype.unbindDocument = function (force) { 118 | var self = this; 119 | // If there are no touch left 120 | // unbind the document. 121 | if (!Object.keys(self.ids).length || force === true) { 122 | self.unbindEvt(document, 'move') 123 | .unbindEvt(document, 'end'); 124 | self.binded = false; 125 | } 126 | }; 127 | 128 | Manager.prototype.getIdentifier = function (evt) { 129 | var id; 130 | // If no event, simple increment 131 | if (!evt) { 132 | id = this.index; 133 | } else { 134 | // Extract identifier from event object. 135 | // Unavailable in mouse events so replaced by latest increment. 136 | id = evt.identifier === undefined ? evt.pointerId : evt.identifier; 137 | if (id === undefined) { 138 | id = this.latest || 0; 139 | } 140 | } 141 | 142 | if (this.ids[id] === undefined) { 143 | this.ids[id] = this.index; 144 | this.index += 1; 145 | } 146 | 147 | // Keep the latest id used in case we're using an unidentified mouseEvent 148 | this.latest = id; 149 | return this.ids[id]; 150 | }; 151 | 152 | Manager.prototype.removeIdentifier = function (identifier) { 153 | var removed = {}; 154 | for (var id in this.ids) { 155 | if (this.ids[id] === identifier) { 156 | removed.id = id; 157 | removed.identifier = this.ids[id]; 158 | delete this.ids[id]; 159 | break; 160 | } 161 | } 162 | return removed; 163 | }; 164 | 165 | Manager.prototype.onmove = function (evt) { 166 | var self = this; 167 | self.onAny('move', evt); 168 | return false; 169 | }; 170 | 171 | Manager.prototype.onend = function (evt) { 172 | var self = this; 173 | self.onAny('end', evt); 174 | return false; 175 | }; 176 | 177 | Manager.prototype.oncancel = function (evt) { 178 | var self = this; 179 | self.onAny('end', evt); 180 | return false; 181 | }; 182 | 183 | Manager.prototype.onAny = function (which, evt) { 184 | var self = this; 185 | var id; 186 | var processFn = 'processOn' + which.charAt(0).toUpperCase() + 187 | which.slice(1); 188 | evt = u.prepareEvent(evt); 189 | var processColl = function (e, id, coll) { 190 | if (coll.ids.indexOf(id) >= 0) { 191 | coll[processFn](e); 192 | // Mark the event to avoid cleaning it later. 193 | e._found_ = true; 194 | } 195 | }; 196 | var processEvt = function (e) { 197 | id = self.getIdentifier(e); 198 | u.map(self.collections, processColl.bind(null, e, id)); 199 | // If the event isn't handled by any collection, 200 | // we need to clean its identifier. 201 | if (!e._found_) { 202 | self.removeIdentifier(id); 203 | } 204 | }; 205 | 206 | u.map(evt, processEvt); 207 | 208 | return false; 209 | }; 210 | 211 | // Cleanly destroy the manager 212 | Manager.prototype.destroy = function () { 213 | var self = this; 214 | self.unbindDocument(true); 215 | self.ids = {}; 216 | self.index = 0; 217 | self.collections.forEach(function(collection) { 218 | collection.destroy(); 219 | }); 220 | self.off(); 221 | }; 222 | 223 | // When a collection gets destroyed 224 | // we clean behind. 225 | Manager.prototype.onDestroyed = function (evt, coll) { 226 | var self = this; 227 | if (self.collections.indexOf(coll) < 0) { 228 | return false; 229 | } 230 | self.collections.splice(self.collections.indexOf(coll), 1); 231 | }; 232 | 233 | export default Manager; 234 | -------------------------------------------------------------------------------- /src/nipple.js: -------------------------------------------------------------------------------- 1 | import Super from './super'; 2 | import * as u from './utils'; 3 | 4 | /////////////////////// 5 | /// THE NIPPLE /// 6 | /////////////////////// 7 | 8 | function Nipple (collection, options) { 9 | this.identifier = options.identifier; 10 | this.position = options.position; 11 | this.frontPosition = options.frontPosition; 12 | this.collection = collection; 13 | 14 | // Defaults 15 | this.defaults = { 16 | size: 100, 17 | threshold: 0.1, 18 | color: 'white', 19 | fadeTime: 250, 20 | dataOnly: false, 21 | restJoystick: true, 22 | restOpacity: 0.5, 23 | mode: 'dynamic', 24 | zone: document.body, 25 | lockX: false, 26 | lockY: false, 27 | shape: 'circle' 28 | }; 29 | 30 | this.config(options); 31 | 32 | // Overwrites 33 | if (this.options.mode === 'dynamic') { 34 | this.options.restOpacity = 0; 35 | } 36 | 37 | this.id = Nipple.id; 38 | Nipple.id += 1; 39 | this.buildEl() 40 | .stylize(); 41 | 42 | // Nipple's API. 43 | this.instance = { 44 | el: this.ui.el, 45 | on: this.on.bind(this), 46 | off: this.off.bind(this), 47 | show: this.show.bind(this), 48 | hide: this.hide.bind(this), 49 | add: this.addToDom.bind(this), 50 | remove: this.removeFromDom.bind(this), 51 | destroy: this.destroy.bind(this), 52 | setPosition:this.setPosition.bind(this), 53 | resetDirection: this.resetDirection.bind(this), 54 | computeDirection: this.computeDirection.bind(this), 55 | trigger: this.trigger.bind(this), 56 | position: this.position, 57 | frontPosition: this.frontPosition, 58 | ui: this.ui, 59 | identifier: this.identifier, 60 | id: this.id, 61 | options: this.options 62 | }; 63 | 64 | return this.instance; 65 | } 66 | 67 | Nipple.prototype = new Super(); 68 | Nipple.constructor = Nipple; 69 | Nipple.id = 0; 70 | 71 | // Build the dom element of the Nipple instance. 72 | Nipple.prototype.buildEl = function (options) { 73 | this.ui = {}; 74 | 75 | if (this.options.dataOnly) { 76 | return this; 77 | } 78 | 79 | this.ui.el = document.createElement('div'); 80 | this.ui.back = document.createElement('div'); 81 | this.ui.front = document.createElement('div'); 82 | 83 | this.ui.el.className = 'nipple collection_' + this.collection.id; 84 | this.ui.back.className = 'back'; 85 | this.ui.front.className = 'front'; 86 | 87 | this.ui.el.setAttribute('id', 'nipple_' + this.collection.id + 88 | '_' + this.id); 89 | 90 | this.ui.el.appendChild(this.ui.back); 91 | this.ui.el.appendChild(this.ui.front); 92 | 93 | return this; 94 | }; 95 | 96 | // Apply CSS to the Nipple instance. 97 | Nipple.prototype.stylize = function () { 98 | if (this.options.dataOnly) { 99 | return this; 100 | } 101 | var animTime = this.options.fadeTime + 'ms'; 102 | var borderStyle = u.getVendorStyle('borderRadius', '50%'); 103 | var transitStyle = u.getTransitionStyle('transition', 'opacity', animTime); 104 | var styles = {}; 105 | styles.el = { 106 | position: 'absolute', 107 | opacity: this.options.restOpacity, 108 | display: 'block', 109 | 'zIndex': 999 110 | }; 111 | 112 | styles.back = { 113 | position: 'absolute', 114 | display: 'block', 115 | width: this.options.size + 'px', 116 | height: this.options.size + 'px', 117 | left: 0, 118 | marginLeft: -this.options.size / 2 + 'px', 119 | marginTop: -this.options.size / 2 + 'px', 120 | background: this.options.color, 121 | 'opacity': '.5' 122 | }; 123 | 124 | styles.front = { 125 | width: this.options.size / 2 + 'px', 126 | height: this.options.size / 2 + 'px', 127 | position: 'absolute', 128 | display: 'block', 129 | left: 0, 130 | marginLeft: -this.options.size / 4 + 'px', 131 | marginTop: -this.options.size / 4 + 'px', 132 | background: this.options.color, 133 | 'opacity': '.5', 134 | transform: 'translate(0px, 0px)' 135 | }; 136 | 137 | u.extend(styles.el, transitStyle); 138 | if(this.options.shape === 'circle'){ 139 | u.extend(styles.back, borderStyle); 140 | } 141 | u.extend(styles.front, borderStyle); 142 | 143 | this.applyStyles(styles); 144 | 145 | return this; 146 | }; 147 | 148 | Nipple.prototype.applyStyles = function (styles) { 149 | // Apply styles 150 | for (var i in this.ui) { 151 | if (this.ui.hasOwnProperty(i)) { 152 | for (var j in styles[i]) { 153 | this.ui[i].style[j] = styles[i][j]; 154 | } 155 | } 156 | } 157 | 158 | return this; 159 | }; 160 | 161 | // Inject the Nipple instance into DOM. 162 | Nipple.prototype.addToDom = function () { 163 | // We're not adding it if we're dataOnly or already in dom. 164 | if (this.options.dataOnly || document.body.contains(this.ui.el)) { 165 | return this; 166 | } 167 | this.options.zone.appendChild(this.ui.el); 168 | return this; 169 | }; 170 | 171 | // Remove the Nipple instance from DOM. 172 | Nipple.prototype.removeFromDom = function () { 173 | if (this.options.dataOnly || !document.body.contains(this.ui.el)) { 174 | return this; 175 | } 176 | this.options.zone.removeChild(this.ui.el); 177 | return this; 178 | }; 179 | 180 | // Entirely destroy this nipple 181 | Nipple.prototype.destroy = function () { 182 | clearTimeout(this.removeTimeout); 183 | clearTimeout(this.showTimeout); 184 | clearTimeout(this.restTimeout); 185 | this.trigger('destroyed', this.instance); 186 | this.removeFromDom(); 187 | this.off(); 188 | }; 189 | 190 | // Fade in the Nipple instance. 191 | Nipple.prototype.show = function (cb) { 192 | var self = this; 193 | 194 | if (self.options.dataOnly) { 195 | return self; 196 | } 197 | 198 | clearTimeout(self.removeTimeout); 199 | clearTimeout(self.showTimeout); 200 | clearTimeout(self.restTimeout); 201 | 202 | self.addToDom(); 203 | 204 | self.restCallback(); 205 | 206 | setTimeout(function () { 207 | self.ui.el.style.opacity = 1; 208 | }, 0); 209 | 210 | self.showTimeout = setTimeout(function () { 211 | self.trigger('shown', self.instance); 212 | if (typeof cb === 'function') { 213 | cb.call(this); 214 | } 215 | }, self.options.fadeTime); 216 | 217 | return self; 218 | }; 219 | 220 | // Fade out the Nipple instance. 221 | Nipple.prototype.hide = function (cb) { 222 | var self = this; 223 | 224 | if (self.options.dataOnly) { 225 | return self; 226 | } 227 | 228 | self.ui.el.style.opacity = self.options.restOpacity; 229 | 230 | clearTimeout(self.removeTimeout); 231 | clearTimeout(self.showTimeout); 232 | clearTimeout(self.restTimeout); 233 | 234 | self.removeTimeout = setTimeout( 235 | function () { 236 | var display = self.options.mode === 'dynamic' ? 'none' : 'block'; 237 | self.ui.el.style.display = display; 238 | if (typeof cb === 'function') { 239 | cb.call(self); 240 | } 241 | 242 | self.trigger('hidden', self.instance); 243 | }, 244 | self.options.fadeTime 245 | ); 246 | 247 | if (self.options.restJoystick) { 248 | const rest = self.options.restJoystick; 249 | const newPosition = {}; 250 | 251 | newPosition.x = rest === true || rest.x !== false ? 0 : self.instance.frontPosition.x; 252 | newPosition.y = rest === true || rest.y !== false ? 0 : self.instance.frontPosition.y; 253 | 254 | self.setPosition(cb, newPosition); 255 | } 256 | 257 | return self; 258 | }; 259 | 260 | // Set the nipple to the specified position 261 | Nipple.prototype.setPosition = function (cb, position) { 262 | var self = this; 263 | self.frontPosition = { 264 | x: position.x, 265 | y: position.y 266 | }; 267 | var animTime = self.options.fadeTime + 'ms'; 268 | 269 | var transitStyle = {}; 270 | transitStyle.front = u.getTransitionStyle('transition', 271 | ['transform'], animTime); 272 | 273 | var styles = {front: {}}; 274 | styles.front = { 275 | transform: 'translate(' + self.frontPosition.x + 'px,' + self.frontPosition.y + 'px)' 276 | }; 277 | 278 | self.applyStyles(transitStyle); 279 | self.applyStyles(styles); 280 | 281 | self.restTimeout = setTimeout( 282 | function () { 283 | if (typeof cb === 'function') { 284 | cb.call(self); 285 | } 286 | self.restCallback(); 287 | }, 288 | self.options.fadeTime 289 | ); 290 | }; 291 | 292 | Nipple.prototype.restCallback = function () { 293 | var self = this; 294 | var transitStyle = {}; 295 | transitStyle.front = u.getTransitionStyle('transition', 'none', ''); 296 | self.applyStyles(transitStyle); 297 | self.trigger('rested', self.instance); 298 | }; 299 | 300 | Nipple.prototype.resetDirection = function () { 301 | // Fully rebuild the object to let the iteration possible. 302 | this.direction = { 303 | x: false, 304 | y: false, 305 | angle: false 306 | }; 307 | }; 308 | 309 | Nipple.prototype.computeDirection = function (obj) { 310 | var rAngle = obj.angle.radian; 311 | var angle45 = Math.PI / 4; 312 | var angle90 = Math.PI / 2; 313 | var direction, directionX, directionY; 314 | 315 | // Angular direction 316 | // \ UP / 317 | // \ / 318 | // LEFT RIGHT 319 | // / \ 320 | // /DOWN \ 321 | // 322 | if ( 323 | rAngle > angle45 && 324 | rAngle < (angle45 * 3) && 325 | !obj.lockX 326 | ) { 327 | direction = 'up'; 328 | } else if ( 329 | rAngle > -angle45 && 330 | rAngle <= angle45 && 331 | !obj.lockY 332 | ) { 333 | direction = 'left'; 334 | } else if ( 335 | rAngle > (-angle45 * 3) && 336 | rAngle <= -angle45 && 337 | !obj.lockX 338 | ) { 339 | direction = 'down'; 340 | } else if (!obj.lockY) { 341 | direction = 'right'; 342 | } 343 | 344 | // Plain direction 345 | // UP | 346 | // _______ | RIGHT 347 | // LEFT | 348 | // DOWN | 349 | if (!obj.lockY) { 350 | if (rAngle > -angle90 && rAngle < angle90) { 351 | directionX = 'left'; 352 | } else { 353 | directionX = 'right'; 354 | } 355 | } 356 | 357 | if (!obj.lockX) { 358 | if (rAngle > 0) { 359 | directionY = 'up'; 360 | } else { 361 | directionY = 'down'; 362 | } 363 | } 364 | 365 | if (obj.force > this.options.threshold) { 366 | var oldDirection = {}; 367 | var i; 368 | for (i in this.direction) { 369 | if (this.direction.hasOwnProperty(i)) { 370 | oldDirection[i] = this.direction[i]; 371 | } 372 | } 373 | 374 | var same = {}; 375 | 376 | this.direction = { 377 | x: directionX, 378 | y: directionY, 379 | angle: direction 380 | }; 381 | 382 | obj.direction = this.direction; 383 | 384 | for (i in oldDirection) { 385 | if (oldDirection[i] === this.direction[i]) { 386 | same[i] = true; 387 | } 388 | } 389 | 390 | // If all 3 directions are the same, we don't trigger anything. 391 | if (same.x && same.y && same.angle) { 392 | return obj; 393 | } 394 | 395 | if (!same.x || !same.y) { 396 | this.trigger('plain', obj); 397 | } 398 | 399 | if (!same.x) { 400 | this.trigger('plain:' + directionX, obj); 401 | } 402 | 403 | if (!same.y) { 404 | this.trigger('plain:' + directionY, obj); 405 | } 406 | 407 | if (!same.angle) { 408 | this.trigger('dir dir:' + direction, obj); 409 | } 410 | } else { 411 | this.resetDirection(); 412 | } 413 | 414 | return obj; 415 | }; 416 | 417 | export default Nipple; 418 | -------------------------------------------------------------------------------- /src/super.js: -------------------------------------------------------------------------------- 1 | /////////////////////// 2 | /// SUPER CLASS /// 3 | /////////////////////// 4 | import * as u from './utils'; 5 | 6 | // Constants 7 | var isTouch = !!('ontouchstart' in window); 8 | var isPointer = window.PointerEvent ? true : false; 9 | var isMSPointer = window.MSPointerEvent ? true : false; 10 | var events = { 11 | touch: { 12 | start: 'touchstart', 13 | move: 'touchmove', 14 | end: 'touchend, touchcancel' 15 | }, 16 | mouse: { 17 | start: 'mousedown', 18 | move: 'mousemove', 19 | end: 'mouseup' 20 | }, 21 | pointer: { 22 | start: 'pointerdown', 23 | move: 'pointermove', 24 | end: 'pointerup, pointercancel' 25 | }, 26 | MSPointer: { 27 | start: 'MSPointerDown', 28 | move: 'MSPointerMove', 29 | end: 'MSPointerUp' 30 | } 31 | }; 32 | var toBind; 33 | var secondBind = {}; 34 | if (isPointer) { 35 | toBind = events.pointer; 36 | } else if (isMSPointer) { 37 | toBind = events.MSPointer; 38 | } else if (isTouch) { 39 | toBind = events.touch; 40 | secondBind = events.mouse; 41 | } else { 42 | toBind = events.mouse; 43 | } 44 | 45 | function Super () {} 46 | 47 | // Basic event system. 48 | Super.prototype.on = function (arg, cb) { 49 | var self = this; 50 | var types = arg.split(/[ ,]+/g); 51 | var type; 52 | self._handlers_ = self._handlers_ || {}; 53 | 54 | for (var i = 0; i < types.length; i += 1) { 55 | type = types[i]; 56 | self._handlers_[type] = self._handlers_[type] || []; 57 | self._handlers_[type].push(cb); 58 | } 59 | return self; 60 | }; 61 | 62 | Super.prototype.off = function (type, cb) { 63 | var self = this; 64 | self._handlers_ = self._handlers_ || {}; 65 | 66 | if (type === undefined) { 67 | self._handlers_ = {}; 68 | } else if (cb === undefined) { 69 | self._handlers_[type] = null; 70 | } else if (self._handlers_[type] && 71 | self._handlers_[type].indexOf(cb) >= 0) { 72 | self._handlers_[type].splice(self._handlers_[type].indexOf(cb), 1); 73 | } 74 | 75 | return self; 76 | }; 77 | 78 | Super.prototype.trigger = function (arg, data) { 79 | var self = this; 80 | var types = arg.split(/[ ,]+/g); 81 | var type; 82 | self._handlers_ = self._handlers_ || {}; 83 | 84 | for (var i = 0; i < types.length; i += 1) { 85 | type = types[i]; 86 | if (self._handlers_[type] && self._handlers_[type].length) { 87 | self._handlers_[type].forEach(function (handler) { 88 | handler.call(self, { 89 | type: type, 90 | target: self 91 | }, data); 92 | }); 93 | } 94 | } 95 | }; 96 | 97 | // Configuration 98 | Super.prototype.config = function (options) { 99 | var self = this; 100 | self.options = self.defaults || {}; 101 | if (options) { 102 | self.options = u.safeExtend(self.options, options); 103 | } 104 | }; 105 | 106 | // Bind internal events. 107 | Super.prototype.bindEvt = function (el, type) { 108 | var self = this; 109 | self._domHandlers_ = self._domHandlers_ || {}; 110 | 111 | self._domHandlers_[type] = function () { 112 | if (typeof self['on' + type] === 'function') { 113 | self['on' + type].apply(self, arguments); 114 | } else { 115 | // eslint-disable-next-line no-console 116 | console.warn('[WARNING] : Missing "on' + type + '" handler.'); 117 | } 118 | }; 119 | 120 | u.bindEvt(el, toBind[type], self._domHandlers_[type]); 121 | 122 | if (secondBind[type]) { 123 | // Support for both touch and mouse at the same time. 124 | u.bindEvt(el, secondBind[type], self._domHandlers_[type]); 125 | } 126 | 127 | return self; 128 | }; 129 | 130 | // Unbind dom events. 131 | Super.prototype.unbindEvt = function (el, type) { 132 | var self = this; 133 | self._domHandlers_ = self._domHandlers_ || {}; 134 | 135 | u.unbindEvt(el, toBind[type], self._domHandlers_[type]); 136 | 137 | if (secondBind[type]) { 138 | // Support for both touch and mouse at the same time. 139 | u.unbindEvt(el, secondBind[type], self._domHandlers_[type]); 140 | } 141 | 142 | delete self._domHandlers_[type]; 143 | 144 | return this; 145 | }; 146 | 147 | export default Super; 148 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /////////////////////// 2 | /// UTILS /// 3 | /////////////////////// 4 | 5 | export const distance = (p1, p2) => { 6 | const dx = p2.x - p1.x; 7 | const dy = p2.y - p1.y; 8 | 9 | return Math.sqrt((dx * dx) + (dy * dy)); 10 | }; 11 | 12 | export const angle = (p1, p2) => { 13 | const dx = p2.x - p1.x; 14 | const dy = p2.y - p1.y; 15 | 16 | return degrees(Math.atan2(dy, dx)); 17 | }; 18 | 19 | export const findCoord = (p, d, a) => { 20 | const b = {x: 0, y: 0}; 21 | a = radians(a); 22 | b.x = p.x - d * Math.cos(a); 23 | b.y = p.y - d * Math.sin(a); 24 | return b; 25 | }; 26 | 27 | export const radians = (a) => { 28 | return a * (Math.PI / 180); 29 | }; 30 | 31 | export const degrees = (a) => { 32 | return a * (180 / Math.PI); 33 | }; 34 | 35 | export const isPressed = (evt) => { 36 | if (isNaN(evt.buttons)) { 37 | return evt.pressure !== 0; 38 | } 39 | return evt.buttons !== 0; 40 | }; 41 | 42 | const timers = new Map(); 43 | export const throttle = (cb) => { 44 | if (timers.has(cb)) { 45 | clearTimeout(timers.get(cb)); 46 | } 47 | timers.set(cb, setTimeout(cb, 100)); 48 | }; 49 | 50 | export const bindEvt = (el, arg, handler) => { 51 | const types = arg.split(/[ ,]+/g); 52 | let type; 53 | for (let i = 0; i < types.length; i += 1) { 54 | type = types[i]; 55 | if (el.addEventListener) { 56 | el.addEventListener(type, handler, false); 57 | } else if (el.attachEvent) { 58 | el.attachEvent(type, handler); 59 | } 60 | } 61 | }; 62 | 63 | export const unbindEvt = (el, arg, handler) => { 64 | const types = arg.split(/[ ,]+/g); 65 | let type; 66 | for (let i = 0; i < types.length; i += 1) { 67 | type = types[i]; 68 | if (el.removeEventListener) { 69 | el.removeEventListener(type, handler); 70 | } else if (el.detachEvent) { 71 | el.detachEvent(type, handler); 72 | } 73 | } 74 | }; 75 | 76 | export const trigger = (el, type, data) => { 77 | const evt = new CustomEvent(type, data); 78 | el.dispatchEvent(evt); 79 | }; 80 | 81 | export const prepareEvent = (evt) => { 82 | evt.preventDefault(); 83 | return evt.type.match(/^touch/) ? evt.changedTouches : evt; 84 | }; 85 | 86 | export const getScroll = () => { 87 | const x = (window.pageXOffset !== undefined) ? 88 | window.pageXOffset : 89 | (document.documentElement || document.body.parentNode || document.body) 90 | .scrollLeft; 91 | 92 | const y = (window.pageYOffset !== undefined) ? 93 | window.pageYOffset : 94 | (document.documentElement || document.body.parentNode || document.body) 95 | .scrollTop; 96 | return { 97 | x: x, 98 | y: y 99 | }; 100 | }; 101 | 102 | export const applyPosition = (el, pos) => { 103 | if (pos.top || pos.right || pos.bottom || pos.left) { 104 | el.style.top = pos.top; 105 | el.style.right = pos.right; 106 | el.style.bottom = pos.bottom; 107 | el.style.left = pos.left; 108 | } else { 109 | el.style.left = pos.x + 'px'; 110 | el.style.top = pos.y + 'px'; 111 | } 112 | }; 113 | 114 | export const getTransitionStyle = (property, values, time) => { 115 | const obj = configStylePropertyObject(property); 116 | for (let i in obj) { 117 | if (obj.hasOwnProperty(i)) { 118 | if (typeof values === 'string') { 119 | obj[i] = values + ' ' + time; 120 | } else { 121 | let st = ''; 122 | for (let j = 0, max = values.length; j < max; j += 1) { 123 | st += values[j] + ' ' + time + ', '; 124 | } 125 | obj[i] = st.slice(0, -2); 126 | } 127 | } 128 | } 129 | return obj; 130 | }; 131 | 132 | export const getVendorStyle = (property, value) => { 133 | const obj = configStylePropertyObject(property); 134 | for (let i in obj) { 135 | if (obj.hasOwnProperty(i)) { 136 | obj[i] = value; 137 | } 138 | } 139 | return obj; 140 | }; 141 | 142 | export const configStylePropertyObject = (prop) => { 143 | const obj = {}; 144 | obj[prop] = ''; 145 | const vendors = ['webkit', 'Moz', 'o']; 146 | vendors.forEach(function (vendor) { 147 | obj[vendor + prop.charAt(0).toUpperCase() + prop.slice(1)] = ''; 148 | }); 149 | return obj; 150 | }; 151 | 152 | export const extend = (objA, objB) => { 153 | for (let i in objB) { 154 | if (objB.hasOwnProperty(i)) { 155 | objA[i] = objB[i]; 156 | } 157 | } 158 | return objA; 159 | }; 160 | 161 | // Overwrite only what's already present 162 | export const safeExtend = (objA, objB) => { 163 | const obj = {}; 164 | for (let i in objA) { 165 | if (objA.hasOwnProperty(i) && objB.hasOwnProperty(i)) { 166 | obj[i] = objB[i]; 167 | } else if (objA.hasOwnProperty(i)) { 168 | obj[i] = objA[i]; 169 | } 170 | } 171 | return obj; 172 | }; 173 | 174 | // Map for array or unique item. 175 | export const map = (ar, fn) => { 176 | if (ar.length) { 177 | for (let i = 0, max = ar.length; i < max; i += 1) { 178 | fn(ar[i]); 179 | } 180 | } else { 181 | fn(ar); 182 | } 183 | }; 184 | 185 | // Clamp position within the range 186 | export const clamp = (pos, nipplePos, size) => ({ 187 | // left-clamping right-clamping 188 | x: Math.min(Math.max(pos.x, nipplePos.x - size), nipplePos.x + size), 189 | // top-clamping bottom-clamping 190 | y: Math.min(Math.max(pos.y, nipplePos.y - size), nipplePos.y + size) 191 | }); 192 | -------------------------------------------------------------------------------- /test/nipplejs.casper.js: -------------------------------------------------------------------------------- 1 | /* global casper */ 2 | var mouse = require('mouse').create(casper); 3 | var u = require('utils'); 4 | 5 | var NB_TESTS = 10; 6 | var nippleIndex = 0; 7 | var collectionIndex = 0; 8 | var showClientLog = true; 9 | 10 | /* 11 | CONFIGURE CASPER 12 | */ 13 | 14 | casper.options.viewportSize = {width: 1100, height: 900}; 15 | casper.options.logLevel = 'debug'; 16 | 17 | /* 18 | LISTEN TO CASPER 19 | */ 20 | 21 | casper.on('remote.message', function(message) { 22 | if (!showClientLog) { 23 | return; 24 | } 25 | if (typeof message === 'string') { 26 | casper.echo('>> [CLIENT] ' + message, 'WARN_BAR'); 27 | } else { 28 | casper.echo('>> [CLIENT] DUMP', 'WARN_BAR'); 29 | u.dump(message); 30 | } 31 | }); 32 | 33 | casper.on('resource.error', function(message) { 34 | if (!showClientLog) { 35 | return; 36 | } 37 | if (typeof message === 'string') { 38 | casper.echo('>> [CLIENT] ' + message, 'RED_BAR'); 39 | } else { 40 | casper.echo('>> [CLIENT] DUMP', 'RED_BAR'); 41 | u.dump(message); 42 | } 43 | }); 44 | 45 | casper.on('page.error', function(message, trace) { 46 | if (!showClientLog) { 47 | return; 48 | } 49 | if (typeof message === 'string') { 50 | casper.echo('>> [CLIENT] ' + message, 'RED_BAR'); 51 | } else { 52 | casper.echo('>> [CLIENT] DUMP', 'RED_BAR'); 53 | u.dump(message); 54 | } 55 | u.dump(trace); 56 | }); 57 | 58 | /* 59 | ACTIONS 60 | */ 61 | 62 | function clickZone () { 63 | mouse.down('.zone.active'); 64 | } 65 | 66 | function assertNipple (test) { 67 | return function () { 68 | test.assertExists( 69 | '#nipple_' + collectionIndex + 70 | '_' + nippleIndex, 71 | 'Nipple ' + nippleIndex + ' from collection ' + 72 | collectionIndex + ' is found' 73 | ); 74 | nippleIndex += 1; 75 | }; 76 | } 77 | 78 | function assertNotNipple (test) { 79 | return function () { 80 | test.assertDoesntExist( 81 | '#nipple_' + collectionIndex + 82 | '_' + nippleIndex, 83 | 'Nipple ' + nippleIndex + ' from collection ' + 84 | collectionIndex + ' is NOT found' 85 | ); 86 | nippleIndex += 1; 87 | }; 88 | } 89 | 90 | casper.test.begin('NippleJS test page loads correctly', NB_TESTS, 91 | function suite(test) { 92 | casper 93 | .start('http://localhost:9000/example/codepen-demo.html') 94 | .then(function () { 95 | // Assert that active zone is here. 96 | test.assertExists('#zone_joystick', 'Active zone is found'); 97 | test.assertExists('.zone.dynamic', 'Dynamic zone is default'); 98 | }) 99 | .then(function () { 100 | // Get position of active zone. 101 | casper.evaluate(function () { 102 | return document.getElementById('zone_joystick') 103 | .getBoundingClientRect(); 104 | }); 105 | }) 106 | /**********************************\ 107 | * 108 | * DYNAMIC 109 | * 110 | \**********************************/ 111 | // Assert we can create more than one nipple on dynamic 112 | .then(clickZone) 113 | .then(assertNipple(test)) 114 | .then(clickZone) 115 | .then(assertNipple(test)) 116 | /**********************************\ 117 | * 118 | * SEMI 119 | * 120 | \**********************************/ 121 | .then(function () { 122 | mouse.click('.button.semi'); 123 | collectionIndex += 1; 124 | }) 125 | .then(function () { 126 | test.assertVisible('.zone.semi', 'Semi zone should be visible'); 127 | }) 128 | // Assert we can't create more than one nipple on semi 129 | .then(clickZone) 130 | .then(assertNipple(test)) 131 | .then(clickZone) 132 | .then(assertNotNipple(test)) 133 | .then(function () { 134 | // Revert increment because last nipple doesn't exist 135 | nippleIndex -= 1; 136 | }) 137 | /**********************************\ 138 | * 139 | * STATIC 140 | * 141 | \**********************************/ 142 | .then(function () { 143 | mouse.click('.button.static'); 144 | collectionIndex += 1; 145 | }) 146 | .then(function () { 147 | test.assertVisible('.zone.static', 148 | 'Static zone should be visible'); 149 | }) 150 | // Assert we can't create more than one nipple on semi 151 | .then(clickZone) 152 | .then(assertNipple(test)) 153 | .then(clickZone) 154 | .then(assertNotNipple(test)) 155 | .then(function () { 156 | // Revert increment because last nipple doesn't exist 157 | nippleIndex -= 1; 158 | }) 159 | .run(function () { 160 | // End tests 161 | test.done(); 162 | }); 163 | } 164 | ); 165 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface JoystickManagerOptions { 2 | /** 3 | * Defaults to `'body'` 4 | * The dom element in which all your joysticks will be injected. 5 | * 6 | * This zone also serve as the mouse/touch events handler. 7 | * 8 | * It represents the zone where all your joysticks will be active. 9 | */ 10 | zone?: HTMLElement; 11 | 12 | /** 13 | * Defaults to `'white'` 14 | * The background color of your joystick’s elements. 15 | * 16 | * Can be any valid CSS color. 17 | */ 18 | color?: string; 19 | 20 | /** 21 | * Defaults to `100` 22 | * The size in pixel of the outer circle. 23 | * 24 | * The inner circle is 50% of this size. 25 | */ 26 | size?: number; 27 | 28 | /** 29 | * This is the strength needed to trigger a directional event. 30 | * 31 | * Basically, the center is 0 and the outer is 1. 32 | * 33 | * You need to at least go to 0.1 to trigger a directional event. 34 | */ 35 | threshold?: number; 36 | 37 | /** 38 | * Defaults to `250` 39 | * 40 | * The time it takes for joystick to fade-out and fade-in when activated or de-activated. 41 | */ 42 | fadeTime?: number; 43 | 44 | /** 45 | * Defaults to `false` 46 | * 47 | * Enable the multitouch capabilities. 48 | * 49 | * If, for reasons, you need to have multiple nipples into the same zone. 50 | * 51 | * Otherwise it will only get one, and all new touches won’t do a thing. 52 | * 53 | * Please note that multitouch is off when in static or semi modes. 54 | */ 55 | multitouch?: boolean; 56 | 57 | /** 58 | * Defaults to `1` 59 | * 60 | * If you need to, you can also control the maximum number of instance that could be created. 61 | * 62 | * Obviously in a multitouch configuration. 63 | */ 64 | maxNumberOfNipples?: number; 65 | 66 | /** 67 | * Defaults to `false` 68 | * 69 | * The library won’t draw anything in the DOM and will only trigger events with data. 70 | */ 71 | dataOnly?: boolean; 72 | 73 | /** 74 | * Defaults to `{top: 0, left: 0}` 75 | * 76 | * An object that will determine the position of a static mode. 77 | * 78 | * You can pass any of the four top, right, bottom and left. 79 | * 80 | * They will be applied as any css property. 81 | */ 82 | position?: { 83 | top?: string; 84 | right?: string; 85 | bottom?: string; 86 | left?: string; 87 | }; 88 | 89 | /** 90 | * Behavioral mode for the joystick 91 | * 92 | * ### 'dynamic': 93 | * a new joystick is created at each new touch. 94 | * the joystick gets destroyed when released. 95 | * can be multitouch. 96 | * 97 | * ### 'semi': 98 | * new joystick is created at each new touch farther than options.catchDistance of any previously created joystick. 99 | * the joystick is faded-out when released but not destroyed. 100 | * when touch is made inside the options.catchDistance a new direction is triggered immediately. 101 | * when touch is made outside the options.catchDistance the previous joystick is destroyed and a new one is created. 102 | * cannot be multitouch. 103 | * 104 | * ### 'static': 105 | * a joystick is positioned immediately at options.position. 106 | * one joystick per zone. 107 | * each new touch triggers a new direction. 108 | * cannot be multitouch. 109 | */ 110 | mode?: 'dynamic' | 'semi' | 'static'; 111 | 112 | /** 113 | * Defaults to `true` 114 | * 115 | * Reset the joystick’s position when it enters the rest state. 116 | */ 117 | restJoystick?: boolean | RestJoystickOption; 118 | 119 | /** 120 | * Defaults to `0.5` 121 | * The opacity to apply when the joystick is in a rest position. 122 | */ 123 | restOpacity?: number; 124 | 125 | /** 126 | * Defaults to `200` 127 | * 128 | * This is only useful in the semi mode, and determine at which distance we recycle the previous joystick. 129 | */ 130 | catchDistance?: number; 131 | 132 | /** 133 | * Defaults to `false` 134 | * 135 | * Locks joystick’s movement to the x (horizontal) axis 136 | */ 137 | lockX?: boolean; 138 | 139 | /** 140 | * Defaults to `false` 141 | * 142 | * Locks joystick’s movement to the y (vertical) axis 143 | */ 144 | lockY?: boolean; 145 | 146 | /** 147 | * Defaults to `false` 148 | * 149 | * Enable if the page has dynamically visible elements such as for Vue, React, Angular or simply some CSS hiding or 150 | * showing some DOM. 151 | */ 152 | dynamicPage?: boolean; 153 | 154 | /** 155 | * Defaults to `circle` 156 | * 157 | * Sets the shape of the joystick 158 | */ 159 | shape?: 'circle' | 'square'; 160 | 161 | /** 162 | * Defaults to `false` 163 | * 164 | * Make the joystick follow the cursor beyond its limits. 165 | */ 166 | follow?: boolean; 167 | } 168 | 169 | export interface RestJoystickOption { 170 | /** 171 | * Defaults to `true` 172 | */ 173 | x?: boolean, 174 | /** 175 | * Defaults to `true` 176 | */ 177 | y?: boolean, 178 | } 179 | 180 | export interface Position { 181 | x: number; 182 | y: number; 183 | } 184 | 185 | export interface Direction { 186 | angle: 'up' | 'down' | 'right' | 'left'; 187 | x: 'left' | 'right'; 188 | y: 'up' | 'down'; 189 | } 190 | 191 | export interface JoystickOutputData { 192 | angle: { 193 | degree: number; 194 | radian: number; 195 | }; 196 | direction: Direction; 197 | vector: { 198 | x: number; 199 | y: number; 200 | }; 201 | raw: { 202 | distance: number; 203 | position: Position; 204 | }; 205 | distance: number; 206 | force: number; 207 | identifier: number; 208 | instance: Joystick; 209 | position: Position; 210 | pressure: number; 211 | } 212 | 213 | export type JoystickEventTypes = 214 | /** 215 | * A joystick is activated. (the user pressed on the active zone) 216 | * Will pass the instance alongside the event. 217 | */ 218 | | 'start' 219 | 220 | /** A joystick is de-activated. (the user released the active zone) 221 | * Will pass the instance alongside the event. 222 | */ 223 | | 'end' 224 | 225 | /** 226 | * A joystick is moved 227 | */ 228 | | 'move' 229 | 230 | /** 231 | * When a direction is reached after the threshold. 232 | * 233 | * Direction are split with a 45° angle. 234 | */ 235 | | 'dir' 236 | | 'dir:up' 237 | | 'dir:down' 238 | | 'dir:right' 239 | | 'dir:left' 240 | 241 | /** 242 | * When a plain direction is reached after the threshold. 243 | * Plain directions are split with a 90° angle. 244 | */ 245 | | 'plain' 246 | 247 | // plain variations 248 | | 'plain:up' 249 | | 'plain:down' 250 | | 'plain:right' 251 | | 'plain:left' 252 | 253 | /** 254 | * Is triggered at the end of the fade-in animation. 255 | * Will pass the instance alongside the event. 256 | * 257 | * Won’t be trigger in a dataOnly configuration. 258 | */ 259 | | 'shown' 260 | 261 | /** 262 | * Is triggered at the end of the fade-out animation. 263 | * 264 | * Will pass the instance alongside the event. 265 | * 266 | * Won’t be trigger in a dataOnly configuration. 267 | */ 268 | | 'hidden' 269 | 270 | /** 271 | * Is triggered at the end of destroy. 272 | * 273 | * Will pass the instance alongside the event. 274 | */ 275 | | 'destroyed' 276 | 277 | /** 278 | * MBP’s Force Touch, iOS’s 3D Touch, Microsoft’s pressure or MDN’s force 279 | * 280 | *Is triggered when the pressure on the joystick is changed. 281 | * 282 | *The value, between 0 and 1, is sent back alongside the event. 283 | */ 284 | | 'pressure'; 285 | 286 | export type ManagerOnlyEventTypes = 287 | /** 288 | * A joystick just got added. 289 | * 290 | * Will pass the instance alongside the event. 291 | */ 292 | | 'added' 293 | 294 | /** 295 | * A joystick just got removed. 296 | * 297 | * Fired at the end of the fade-out animation. 298 | * 299 | * Will pass the instance alongside the event. 300 | * 301 | * Won’t be trigger in a dataOnly configuration. 302 | */ 303 | | 'removed'; 304 | 305 | export type JoystickManagerEventTypes = JoystickEventTypes | ManagerOnlyEventTypes; 306 | 307 | export interface EventData { 308 | type: JoystickEventTypes | ManagerOnlyEventTypes; 309 | target: Collection; 310 | } 311 | 312 | export class JoystickManager { 313 | create(options?: JoystickManagerOptions): JoystickManager; 314 | 315 | on( 316 | type: JoystickManagerEventTypes | JoystickManagerEventTypes[], 317 | handler: (evt: EventData, data: JoystickOutputData) => void 318 | ): void; 319 | off( 320 | type: JoystickManagerEventTypes | JoystickManagerEventTypes[], 321 | handler: (evt: EventData, data: JoystickOutputData) => void 322 | ): void; 323 | get(identifier: number): Joystick; 324 | destroy(): void; 325 | ids: number[]; 326 | id: number; 327 | } 328 | 329 | export interface Collection { 330 | nipples: Joystick[]; 331 | idles: Joystick[]; 332 | actives: Joystick[]; 333 | ids: number[]; 334 | pressureIntervals: {}; 335 | manager: JoystickManager; 336 | id: number; 337 | defaults: JoystickManagerOptions; 338 | parentIsFlex: boolean; 339 | } 340 | 341 | export interface Joystick { 342 | on( 343 | type: JoystickEventTypes | JoystickEventTypes[], 344 | handler: (evt: EventData, data: JoystickOutputData) => void 345 | ): void; 346 | off( 347 | type: JoystickEventTypes | JoystickEventTypes[], 348 | handler: (evt: EventData, data: JoystickOutputData) => void 349 | ): void; 350 | el: HTMLElement; 351 | show(cb?: () => void): void; 352 | hide(cb?: () => void): void; 353 | add(): void; 354 | remove(): void; 355 | destroy(): void; 356 | setPosition(cb: (joystick: Joystick) => void, position: Position): void; 357 | identifier: number; 358 | trigger( 359 | type: JoystickEventTypes | JoystickEventTypes[], 360 | handler: (evt: EventData, data: any) => void 361 | ): void; 362 | position: Position; 363 | frontPosition: Position; 364 | ui: { 365 | el: HTMLElement; 366 | front: HTMLElement; 367 | back: HTMLElement; 368 | }; 369 | options: JoystickManagerOptions; 370 | } 371 | 372 | /** 373 | * A JavaScript library for creating vanillaJS virtual joysticks, for touch capable interfaces. 374 | */ 375 | declare module 'nipplejs' { 376 | /** 377 | * Create a Joystick manager 378 | * @param options for creating a manager instance 379 | * @return manager instance 380 | */ 381 | function create(options: JoystickManagerOptions): JoystickManager; 382 | 383 | /** 384 | * Library's root manger instance. 385 | */ 386 | const factory: JoystickManager; 387 | } 388 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const DEBUG = process.env.NODE_ENV !== 'production'; 4 | const NAME = 'nipplejs'; 5 | 6 | module.exports = { 7 | context: __dirname, 8 | entry: './src/index.js', 9 | mode: DEBUG ? 'development' : 'production', 10 | devServer:{ 11 | contentBase: __dirname, 12 | publicPath: '/dist/', 13 | port: 9000, 14 | }, 15 | output: { 16 | path: path.resolve(__dirname, 'dist'), 17 | filename: `${NAME}.js`, 18 | library: NAME, 19 | libraryExport: 'default', 20 | libraryTarget: 'umd', 21 | umdNamedDefine: true 22 | }, 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.js$/, 27 | exclude: /node_modules/, 28 | use: [ 29 | 'babel-loader', 30 | 'eslint-loader' 31 | ] 32 | }, 33 | ] 34 | } 35 | }; 36 | --------------------------------------------------------------------------------