├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── examples ├── bezier.html ├── circular-constraint.html ├── circular-constraint2.html ├── color-picker.html ├── dual-constraint.html └── triangle-constraint.html ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── planar-range.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | bin 4 | demo -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | node_modules 3 | tsconfig.json 4 | bin 5 | examples -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Preet Shihn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # \ 2 | 3 | *\* is a custom-element akin to HTML's standard input range (*2kb gzipped*). Instead of one thumb that moves linearly, this element can have as many thumbs as required, and they move in two dimensions. The element supports all pointer devices. 4 | 5 | Each thumb is represented as a *\* custom-element and can have an `x` and `y` value. The values are always between `0` and `1`. 6 | 7 | [Read full documentation](https://github.com/pshihn/planar-range/wiki/Planar-Range) with links to live playground on glitch. 8 | 9 | ![prange6](https://user-images.githubusercontent.com/833927/79682481-953d5d80-81d7-11ea-9f90-652274a4a6d6.gif) 10 | 11 | ```html 12 | 13 | 14 | 15 | 16 | 17 | 18 | ``` 19 | 20 | ## Styling 21 | 22 | The range element and the thumbs can be sized and styled via CSS. The element doesn't need to be square. The values of `x` and `y` (between 0 and 1) interpolate with the width and height. 23 | 24 | ![Planar range demo](https://user-images.githubusercontent.com/833927/79674189-75854580-8195-11ea-9d45-9cde244d028b.gif) 25 | 26 | ```html 27 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ``` 53 | 54 | ## Uses 55 | 56 | The *planar-range* control itself is simple and kinda low-level. But, you can use it to create more complex UI components like a cubic-bezier creator or a HSL based color picker 57 | 58 | ![cubic bezier](https://user-images.githubusercontent.com/833927/79682226-54dce000-81d5-11ea-9e5f-76fa1683db71.gif) 59 | 60 | ![HSl based color picker](https://user-images.githubusercontent.com/833927/79682298-e1879e00-81d5-11ea-8213-c889e6d60dfb.png) 61 | 62 | Basic demos of these are in the examples folder. 63 | 64 | ## Installing 65 | Available on npm 66 | ``` 67 | npm install --save planar-range 68 | ``` 69 | 70 | Or source it in your page 71 | ```html 72 | 73 | ``` 74 | 75 | ## Events 76 | 77 | Each thumb fires a `change` event. The `event.detail` object gives the `x`, `y` value of the thumb and its `name` attribute. 78 | 79 | ```html 80 | 81 | 82 | 83 | 84 | 85 | 100 | ``` 101 | 102 | ## PlanarRangeThumb *\* properties 103 | 104 | **x**: The numeric `x` value of the thumb. The range is between `[0, 1]`. 105 | 106 | **y**: The numeric `y` value of the thumb. The range is between `[0, 1]`. 107 | 108 | **value**: A 2D array of `[x, y]` value. e.g. `thumb.value = [0.2, 0.6];` 109 | 110 | ## PlanarRange *\* properties 111 | 112 | **values**: Readonly property to get the values of all the thumbs in the range. An array of Objects. Each object has the `x`, `y` value of the thumb, and its `name` attribute. 113 | -------------------------------------------------------------------------------- /examples/bezier.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | planar-range demo 8 | 52 | 53 | 54 | 55 |
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 | 65 | 66 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /examples/circular-constraint.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | planar-range demo 8 | 47 | 48 | 49 | 50 |
51 | 52 | 53 | 54 | 55 |
56 | 57 | 58 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /examples/circular-constraint2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | planar-range demo 8 | 46 | 47 | 48 | 49 |
50 | 51 | 52 | 53 | 54 |
55 | 56 | 57 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /examples/color-picker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | planar-range demo 8 | 49 | 50 | 51 | 52 |
53 |
54 |
55 | 56 | 57 | 58 |
59 | 60 | 61 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /examples/dual-constraint.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | planar-range demo 8 | 77 | 78 | 79 | 80 |
81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 | 89 |
90 | 91 | 92 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /examples/triangle-constraint.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | planar-range demo 8 | 48 | 49 | 50 | 51 |
52 | 53 | 54 | 55 | 56 |
57 | 58 | 59 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "planar-range", 3 | "version": "0.3.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.8.3", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", 10 | "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.8.3" 14 | } 15 | }, 16 | "@babel/helper-validator-identifier": { 17 | "version": "7.9.5", 18 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz", 19 | "integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==", 20 | "dev": true 21 | }, 22 | "@babel/highlight": { 23 | "version": "7.9.0", 24 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", 25 | "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", 26 | "dev": true, 27 | "requires": { 28 | "@babel/helper-validator-identifier": "^7.9.0", 29 | "chalk": "^2.0.0", 30 | "js-tokens": "^4.0.0" 31 | } 32 | }, 33 | "@types/node": { 34 | "version": "13.13.0", 35 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.0.tgz", 36 | "integrity": "sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==", 37 | "dev": true 38 | }, 39 | "@types/resolve": { 40 | "version": "0.0.8", 41 | "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", 42 | "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", 43 | "dev": true, 44 | "requires": { 45 | "@types/node": "*" 46 | } 47 | }, 48 | "ansi-styles": { 49 | "version": "3.2.1", 50 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 51 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 52 | "dev": true, 53 | "requires": { 54 | "color-convert": "^1.9.0" 55 | } 56 | }, 57 | "buffer-from": { 58 | "version": "1.1.1", 59 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 60 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 61 | "dev": true 62 | }, 63 | "builtin-modules": { 64 | "version": "3.1.0", 65 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", 66 | "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", 67 | "dev": true 68 | }, 69 | "chalk": { 70 | "version": "2.4.2", 71 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 72 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 73 | "dev": true, 74 | "requires": { 75 | "ansi-styles": "^3.2.1", 76 | "escape-string-regexp": "^1.0.5", 77 | "supports-color": "^5.3.0" 78 | } 79 | }, 80 | "color-convert": { 81 | "version": "1.9.3", 82 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 83 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 84 | "dev": true, 85 | "requires": { 86 | "color-name": "1.1.3" 87 | } 88 | }, 89 | "color-name": { 90 | "version": "1.1.3", 91 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 92 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 93 | "dev": true 94 | }, 95 | "commander": { 96 | "version": "2.20.3", 97 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 98 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 99 | "dev": true 100 | }, 101 | "escape-string-regexp": { 102 | "version": "1.0.5", 103 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 104 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 105 | "dev": true 106 | }, 107 | "estree-walker": { 108 | "version": "0.6.1", 109 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", 110 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", 111 | "dev": true 112 | }, 113 | "fsevents": { 114 | "version": "2.1.3", 115 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", 116 | "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", 117 | "dev": true, 118 | "optional": true 119 | }, 120 | "has-flag": { 121 | "version": "3.0.0", 122 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 123 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 124 | "dev": true 125 | }, 126 | "is-module": { 127 | "version": "1.0.0", 128 | "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", 129 | "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", 130 | "dev": true 131 | }, 132 | "jest-worker": { 133 | "version": "24.9.0", 134 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", 135 | "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", 136 | "dev": true, 137 | "requires": { 138 | "merge-stream": "^2.0.0", 139 | "supports-color": "^6.1.0" 140 | }, 141 | "dependencies": { 142 | "supports-color": { 143 | "version": "6.1.0", 144 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", 145 | "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", 146 | "dev": true, 147 | "requires": { 148 | "has-flag": "^3.0.0" 149 | } 150 | } 151 | } 152 | }, 153 | "js-tokens": { 154 | "version": "4.0.0", 155 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 156 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 157 | "dev": true 158 | }, 159 | "merge-stream": { 160 | "version": "2.0.0", 161 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 162 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", 163 | "dev": true 164 | }, 165 | "path-parse": { 166 | "version": "1.0.6", 167 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 168 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 169 | "dev": true 170 | }, 171 | "pointer-tracker": { 172 | "version": "2.3.0", 173 | "resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.3.0.tgz", 174 | "integrity": "sha512-2tQaxrI62yNRIE6jFkxCpa/nraqavDWPZH1WVVAEA9TlH7CWjqX6sfCsWwrWeSYcQwAiZ/l3YfqXNDp4aQIOCQ==" 175 | }, 176 | "resolve": { 177 | "version": "1.16.1", 178 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.16.1.tgz", 179 | "integrity": "sha512-rmAglCSqWWMrrBv/XM6sW0NuRFiKViw/W4d9EbC4pt+49H8JwHy+mcGmALTEg504AUDcLTvb1T2q3E9AnmY+ig==", 180 | "dev": true, 181 | "requires": { 182 | "path-parse": "^1.0.6" 183 | } 184 | }, 185 | "rollup": { 186 | "version": "2.7.2", 187 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.7.2.tgz", 188 | "integrity": "sha512-SdtTZVMMVSPe7SNv4exUyPXARe5v/p3TeeG3LRA5WabLPJt4Usi3wVrvVlyAUTG40JJmqS6zbIHt2vWTss2prw==", 189 | "dev": true, 190 | "requires": { 191 | "fsevents": "~2.1.2" 192 | } 193 | }, 194 | "rollup-plugin-node-resolve": { 195 | "version": "5.2.0", 196 | "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", 197 | "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", 198 | "dev": true, 199 | "requires": { 200 | "@types/resolve": "0.0.8", 201 | "builtin-modules": "^3.1.0", 202 | "is-module": "^1.0.0", 203 | "resolve": "^1.11.1", 204 | "rollup-pluginutils": "^2.8.1" 205 | } 206 | }, 207 | "rollup-plugin-terser": { 208 | "version": "5.3.0", 209 | "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.3.0.tgz", 210 | "integrity": "sha512-XGMJihTIO3eIBsVGq7jiNYOdDMb3pVxuzY0uhOE/FM4x/u9nQgr3+McsjzqBn3QfHIpNSZmFnpoKAwHBEcsT7g==", 211 | "dev": true, 212 | "requires": { 213 | "@babel/code-frame": "^7.5.5", 214 | "jest-worker": "^24.9.0", 215 | "rollup-pluginutils": "^2.8.2", 216 | "serialize-javascript": "^2.1.2", 217 | "terser": "^4.6.2" 218 | } 219 | }, 220 | "rollup-pluginutils": { 221 | "version": "2.8.2", 222 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", 223 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", 224 | "dev": true, 225 | "requires": { 226 | "estree-walker": "^0.6.1" 227 | } 228 | }, 229 | "serialize-javascript": { 230 | "version": "2.1.2", 231 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", 232 | "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", 233 | "dev": true 234 | }, 235 | "source-map": { 236 | "version": "0.6.1", 237 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 238 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 239 | "dev": true 240 | }, 241 | "source-map-support": { 242 | "version": "0.5.16", 243 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", 244 | "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", 245 | "dev": true, 246 | "requires": { 247 | "buffer-from": "^1.0.0", 248 | "source-map": "^0.6.0" 249 | } 250 | }, 251 | "supports-color": { 252 | "version": "5.5.0", 253 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 254 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 255 | "dev": true, 256 | "requires": { 257 | "has-flag": "^3.0.0" 258 | } 259 | }, 260 | "terser": { 261 | "version": "4.6.11", 262 | "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.11.tgz", 263 | "integrity": "sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA==", 264 | "dev": true, 265 | "requires": { 266 | "commander": "^2.20.0", 267 | "source-map": "~0.6.1", 268 | "source-map-support": "~0.5.12" 269 | } 270 | }, 271 | "typescript": { 272 | "version": "3.8.3", 273 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 274 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", 275 | "dev": true 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "planar-range", 3 | "version": "0.3.2", 4 | "description": "A 2D range component ", 5 | "main": "lib/planar-range.js", 6 | "module": "lib/planar-range.js", 7 | "types": "lib/planar-range.d.ts", 8 | "scripts": { 9 | "build": "rm -rf lib && tsc && rollup -c", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/pshihn/planar-range.git" 15 | }, 16 | "keywords": [ 17 | "range", 18 | "webcomponent" 19 | ], 20 | "author": "Preet Shihn", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/pshihn/planar-range/issues" 24 | }, 25 | "homepage": "https://github.com/pshihn/planar-range#readme", 26 | "devDependencies": { 27 | "rollup": "^2.7.2", 28 | "rollup-plugin-node-resolve": "^5.2.0", 29 | "rollup-plugin-terser": "^5.3.0", 30 | "typescript": "^3.8.3" 31 | }, 32 | "dependencies": { 33 | "pointer-tracker": "^2.3.0" 34 | } 35 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve'; 2 | import { terser } from "rollup-plugin-terser"; 3 | 4 | export default [ 5 | { 6 | input: 'lib/planar-range.js', 7 | output: { 8 | file: 'lib/planar-range.min.js', 9 | format: 'esm' 10 | }, 11 | plugins: [resolve(), terser()] 12 | } 13 | ]; -------------------------------------------------------------------------------- /src/planar-range.ts: -------------------------------------------------------------------------------- 1 | import PointerTracker from 'pointer-tracker'; 2 | 3 | export interface PlanarRangeThumbValue { 4 | name?: string; 5 | x: number; 6 | y: number; 7 | } 8 | 9 | function fire(element: HTMLElement, name: string, detail?: any) { 10 | element.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail })); 11 | } 12 | 13 | export class PlanarRangeThumb extends HTMLElement { 14 | private _x = 1; 15 | private _y = 1; 16 | 17 | static get observedAttributes() { return ['x', 'y']; } 18 | 19 | constructor() { 20 | super(); 21 | const root = this.attachShadow({ mode: 'open' }); 22 | root.innerHTML = ` 23 | 37 | `; 38 | } 39 | 40 | attributeChangedCallback(name: string, _: string, newValue: string) { 41 | if (name === 'x') { 42 | this.x = +newValue; 43 | } else if (name === 'y') { 44 | this.y = +newValue; 45 | } 46 | } 47 | 48 | connectedCallback() { 49 | this.updatePosition(); 50 | } 51 | 52 | private updatePosition() { 53 | (new Promise(() => { 54 | this.style.left = `${this._x * 100}%`; 55 | this.style.top = `${this._y * 100}%`; 56 | })); 57 | } 58 | 59 | get x(): number { 60 | return this._x; 61 | } 62 | 63 | get y(): number { 64 | return this._y; 65 | } 66 | 67 | set x(value: number) { 68 | value = Math.max(0, Math.min(1, value || 0)); 69 | if (value !== this._x) { 70 | this._x = value; 71 | this.updatePosition(); 72 | } 73 | } 74 | 75 | set y(value: number) { 76 | value = Math.max(0, Math.min(1, value || 0)); 77 | if (value !== this._y) { 78 | this._y = value; 79 | this.updatePosition(); 80 | } 81 | } 82 | 83 | get value(): [number, number] { 84 | return [this._x, this._y]; 85 | } 86 | 87 | set value(c: [number, number]) { 88 | this.x = c[0]; 89 | this.y = c[1]; 90 | } 91 | 92 | setValue(v: [number, number], fireEvent: boolean) { 93 | const [oldx, oldy] = [this._x, this._y]; 94 | this.value = v; 95 | if (fireEvent && (oldx !== this._x || oldy !== this._y)) { 96 | fire(this, 'change', { 97 | name: this.getAttribute('name') || undefined, 98 | x: this._x, 99 | y: this._y 100 | }); 101 | } 102 | } 103 | } 104 | 105 | export class PlanarRange extends HTMLElement { 106 | private root: ShadowRoot; 107 | private _slot?: HTMLSlotElement; 108 | private _container?: HTMLDivElement; 109 | private thumbs: PlanarRangeThumb[] = []; 110 | private pointerMap = new Map(); 111 | validator?: (point: [number, number], previous: [number, number], name: string | null) => [number, number]; 112 | 113 | constructor() { 114 | super(); 115 | this.root = this.attachShadow({ mode: 'open' }); 116 | this.root.innerHTML = ` 117 | 137 |
138 | ` 139 | } 140 | 141 | private get slotElement(): HTMLSlotElement { 142 | if (!this._slot) { 143 | this._slot = this.root.querySelector('slot')!; 144 | } 145 | return this._slot; 146 | } 147 | 148 | private get container(): HTMLDivElement { 149 | if (!this._container) { 150 | this._container = this.root.querySelector('#container') as HTMLDivElement; 151 | } 152 | return this._container; 153 | } 154 | 155 | private updateThumbs() { 156 | const nodes = this.slotElement.assignedNodes().filter((n) => { 157 | return (n.nodeType === Node.ELEMENT_NODE) && ((n as HTMLElement).tagName.toLowerCase() === 'planar-range-thumb'); 158 | }); 159 | const thumbs: PlanarRangeThumb[] = []; 160 | nodes.forEach((n) => { 161 | const t = n as PlanarRangeThumb; 162 | if (this.thumbs.indexOf(t) < 0) { 163 | let viewAnchor = [0, 0, 0, 0]; 164 | const tracker = new PointerTracker(t, { 165 | start: (_, event) => { 166 | event.preventDefault(); 167 | const rect = this.container.getBoundingClientRect(); 168 | viewAnchor = [rect.left || rect.x, rect.top || rect.y, rect.width, rect.height]; 169 | return true; 170 | }, 171 | move: (_, changedPointers) => { 172 | const pointer = changedPointers[0]; 173 | if (pointer) { 174 | const w = viewAnchor[2]; 175 | const h = viewAnchor[3]; 176 | const newX = w ? ((pointer.clientX - viewAnchor[0]) / w) : 0; 177 | const newY = h ? ((pointer.clientY - viewAnchor[1]) / h) : 0; 178 | if ((newX !== t.x) || (newY !== t.y)) { 179 | if (this.validator) { 180 | t.setValue(this.validator([newX, newY], [t.x, t.y], t.getAttribute('name') || null), true); 181 | } else { 182 | t.setValue([newX, newY], true); 183 | } 184 | } 185 | } 186 | } 187 | }); 188 | this.pointerMap.set(t, tracker); 189 | } 190 | thumbs.push(t); 191 | }); 192 | this.thumbs = thumbs; 193 | } 194 | 195 | connectedCallback() { 196 | this.slotElement.addEventListener('slotchange', () => this.updateThumbs()); 197 | this.updateThumbs(); 198 | } 199 | 200 | disconnectedCallback() { 201 | for (const tracker of this.pointerMap.values()) { 202 | tracker.stop(); 203 | } 204 | this.pointerMap.clear(); 205 | } 206 | 207 | get values(): PlanarRangeThumbValue[] { 208 | return this.thumbs.map((thumb) => { 209 | return { 210 | x: thumb.x, 211 | y: thumb.y, 212 | name: thumb.getAttribute('name') || undefined 213 | }; 214 | }); 215 | } 216 | } 217 | 218 | customElements.define('planar-range-thumb', PlanarRangeThumb); 219 | customElements.define('planar-range', PlanarRange); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "resolveJsonModule": true, 7 | "lib": [ 8 | "es2017", 9 | "dom" 10 | ], 11 | "declaration": true, 12 | "outDir": "./lib", 13 | "baseUrl": ".", 14 | "strict": true, 15 | "strictNullChecks": true, 16 | "noImplicitAny": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": [ 23 | "src/**/*.ts" 24 | ] 25 | } --------------------------------------------------------------------------------