├── .gitignore ├── LICENSE ├── README.md ├── bin ├── emoji-slider.d.ts └── emoji-slider.js ├── demo └── index.html ├── package-lock.json ├── package.json ├── src └── emoji-slider.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | node_modules 4 | demo/demo.html -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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 | # emoji-slider 2 | A slider control which uses an emoji as the thumb. 3 | 4 | This is built as a web component, so it's usable anywhere with HTML. 5 | 6 | [View live demo.](https://pshihn.github.io/emoji-slider/demo/) 7 | 8 | ![Emoji Slider](https://i.imgur.com/RyyBB6B.png) 9 | 10 | ## Usage 11 | 12 | Get the compoent 13 | 14 | ``` 15 | npm install --save emoji-slider 16 | ``` 17 | 18 | Import in a JavaScript module: 19 | 20 | ``` javascript 21 | import 'emoji-slider'; 22 | ``` 23 | 24 | Or in an HTML page: 25 | ```html 26 | 27 | ``` 28 | 29 | And then use it in HTML 30 | 31 | ``` 32 | 33 | ``` 34 | 35 | More about using web components [here](https://lit-element.polymer-project.org/guide/use). 36 | 37 | ## Properties 38 | 39 | **value:** The numeric value of the slider between 0 and 1. 40 | 41 | **emoji:** The emoji character to use in the thumb of the slider. If not set, a circular thumb is used. 42 | 43 | **step:** The change in value when controlling the slider with keyboard e.g., *Right Arrow Key* will increate the value by 0.1. Default value of *step* is 0.1 44 | 45 | ## Events 46 | **change** event is fired as the slider value changes. the current value can be obtained from the event details: `event.detail.value` 47 | 48 | ## Styling 49 | The slider bar color (and active color) be styled using CSS properties. For example, 50 | 51 | ```css 52 | emoji-slider { 53 | --emoji-slider-bar-color: red; 54 | --emoji-slider-bar-active-color: green; 55 | } 56 | ``` 57 | 58 | ## License 59 | [MIT License](https://github.com/pshihn/emoji-slider/blob/master/LICENSE) (c) [Preet Shihn](https://twitter.com/preetster) 60 | 61 | -------------------------------------------------------------------------------- /bin/emoji-slider.d.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, TemplateResult } from 'lit-element'; 2 | export declare class EmojiSlider extends LitElement { 3 | emoji?: string; 4 | step: number; 5 | private trackBar?; 6 | private cursor?; 7 | private valueBar?; 8 | private pctValue; 9 | private dragging; 10 | private upHandler; 11 | private downHandler; 12 | private trackHandler; 13 | private keyHandler; 14 | static styles: import("lit-element").CSSResult; 15 | render(): TemplateResult; 16 | firstUpdated(): void; 17 | disconnectedCallback(): void; 18 | private attachTrackHandlers; 19 | private detachTrackHandlers; 20 | private onUp; 21 | private onDown; 22 | private onTrack; 23 | private trackStart; 24 | private trackEnd; 25 | private trackX; 26 | private setValue; 27 | private fireEvent; 28 | value: number; 29 | private updateValue; 30 | private handleKeyDown; 31 | } 32 | -------------------------------------------------------------------------------- /bin/emoji-slider.js: -------------------------------------------------------------------------------- 1 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { 2 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; 3 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); 4 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; 5 | return c > 3 && r && Object.defineProperty(target, key, r), r; 6 | }; 7 | var __metadata = (this && this.__metadata) || function (k, v) { 8 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); 9 | }; 10 | import { LitElement, html, css, property, customElement, query } from 'lit-element'; 11 | import { addListener, removeListener } from '@polymer/polymer/lib/utils/gestures'; 12 | let EmojiSlider = class EmojiSlider extends LitElement { 13 | constructor() { 14 | super(...arguments); 15 | this.step = 0.1; 16 | this.pctValue = 0; 17 | this.dragging = false; 18 | this.upHandler = () => this.onUp(); 19 | this.downHandler = (e) => this.onDown(e); 20 | this.trackHandler = (e) => this.onTrack(e); 21 | this.keyHandler = (e) => this.handleKeyDown(e); 22 | } 23 | render() { 24 | const emojiChar = (this.emoji && this.emoji.length) ? [...this.emoji][0] : ''; 25 | return html ` 26 |
27 |
28 |
29 |
30 |
31 | ${emojiChar} 32 |
33 |
34 | `; 35 | } 36 | firstUpdated() { 37 | this.attachTrackHandlers(); 38 | // aria 39 | this.setAttribute('role', 'slider'); 40 | this.setAttribute('aria-valuemax', '1'); 41 | this.setAttribute('aria-valuemin', '0'); 42 | this.tabIndex = +(this.getAttribute('tabindex') || 0); 43 | } 44 | disconnectedCallback() { 45 | super.disconnectedCallback(); 46 | this.detachTrackHandlers(); 47 | } 48 | attachTrackHandlers() { 49 | this.detachTrackHandlers(); 50 | const bar = this.trackBar; 51 | addListener(bar, 'up', this.upHandler); 52 | addListener(bar, 'down', this.downHandler); 53 | addListener(bar, 'track', this.trackHandler); 54 | this.addEventListener('keydown', this.keyHandler); 55 | } 56 | detachTrackHandlers() { 57 | const bar = this.trackBar; 58 | removeListener(bar, 'up', this.upHandler); 59 | removeListener(bar, 'down', this.downHandler); 60 | removeListener(bar, 'track', this.trackHandler); 61 | this.removeEventListener('keydown', this.keyHandler); 62 | } 63 | onUp() { 64 | this.trackEnd(); 65 | } 66 | onDown(e) { 67 | const event = e; 68 | event.preventDefault(); 69 | this.setValue(event.detail.x); 70 | this.cursor.classList.add('active'); 71 | this.focus(); 72 | } 73 | onTrack(e) { 74 | const event = e; 75 | event.stopPropagation(); 76 | switch (event.detail.state) { 77 | case 'start': 78 | this.trackStart(); 79 | break; 80 | case 'track': 81 | this.trackX(event); 82 | break; 83 | case 'end': 84 | this.trackEnd(); 85 | break; 86 | } 87 | } 88 | trackStart() { 89 | this.dragging = true; 90 | this.cursor.classList.add('active'); 91 | } 92 | trackEnd() { 93 | this.dragging = false; 94 | this.cursor.classList.remove('active'); 95 | } 96 | trackX(event) { 97 | if (!this.dragging) { 98 | this.trackStart(); 99 | } 100 | this.setValue(event.detail.x); 101 | } 102 | setValue(x) { 103 | const rect = this.trackBar.getBoundingClientRect(); 104 | const pct = Math.max(0, Math.min(rect.width ? ((x - rect.left) / rect.width) : 0, 1)); 105 | if (this.pctValue !== pct) { 106 | this.pctValue = pct; 107 | this.updateValue(); 108 | this.fireEvent('change'); 109 | } 110 | } 111 | fireEvent(name) { 112 | this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: { value: this.value } })); 113 | } 114 | set value(v) { 115 | this.pctValue = v; 116 | this.updateValue(); 117 | } 118 | get value() { 119 | return this.pctValue; 120 | } 121 | updateValue() { 122 | const offset = `${this.pctValue * 100}%`; 123 | if (this.cursor) 124 | this.cursor.style.left = offset; 125 | if (this.valueBar) { 126 | this.valueBar.style.width = offset; 127 | } 128 | } 129 | handleKeyDown(e) { 130 | switch (e.keyCode) { 131 | case 38: 132 | case 39: { 133 | const newValue = Math.min(1, this.value + this.step); 134 | if (newValue !== this.value) { 135 | this.value = newValue; 136 | this.fireEvent('change'); 137 | } 138 | break; 139 | } 140 | case 37: 141 | case 40: { 142 | const newValue = Math.max(0, this.value - this.step); 143 | if (newValue !== this.value) { 144 | this.value = newValue; 145 | this.fireEvent('change'); 146 | } 147 | break; 148 | } 149 | case 36: 150 | if (this.value !== 0) { 151 | this.setValue(0); 152 | this.fireEvent('change'); 153 | } 154 | break; 155 | case 35: 156 | if (this.value !== 1) { 157 | this.setValue(1); 158 | this.fireEvent('change'); 159 | } 160 | break; 161 | } 162 | } 163 | }; 164 | EmojiSlider.styles = css ` 165 | :host { 166 | display: block; 167 | position: relative; 168 | outline: none; 169 | } 170 | #bar { 171 | position: relative; 172 | cursor: pointer; 173 | padding: 10px 0; 174 | } 175 | #barLine { 176 | background: var(--emoji-slider-bar-color, #e5e5e5); 177 | height: 4px; 178 | position: relative; 179 | } 180 | #valueBar { 181 | background: var(--emoji-slider-bar-active-color, #2196f3); 182 | position: absolute; 183 | left: 0; 184 | top: 0; 185 | height: 100%; 186 | } 187 | #cursor { 188 | position: absolute; 189 | top: 4px; 190 | left: 0px; 191 | transform: translate3d(-8px, 0, 0); 192 | transition: transform 0.28s ease, box-shadow 0.28s ease; 193 | } 194 | #cursor.noemoji { 195 | background: var(--emoji-slider-cursor-color, #d32f2f); 196 | border-radius: 10px; 197 | width: 16px; 198 | height: 16px; 199 | } 200 | #cursor.noemoji span { 201 | display: none; 202 | } 203 | #cursor.emoji { 204 | background: none; 205 | width: auto; 206 | height: auto; 207 | font-size: var(--emoji-slider-font-size, 24px); 208 | line-height: 1.17; 209 | transform: translate3d(-8px, -0.25em, 0); 210 | } 211 | #cursor.active { 212 | transform: translate3d(-8px, 0, 0) scale(1.5); 213 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); 214 | } 215 | #cursor.emoji.active { 216 | transform: translate3d(-8px, -0.25em, 0) scale(1.5); 217 | box-shadow: none; 218 | } 219 | `; 220 | __decorate([ 221 | property(), 222 | __metadata("design:type", String) 223 | ], EmojiSlider.prototype, "emoji", void 0); 224 | __decorate([ 225 | property({ type: Number }), 226 | __metadata("design:type", Object) 227 | ], EmojiSlider.prototype, "step", void 0); 228 | __decorate([ 229 | query('#bar'), 230 | __metadata("design:type", HTMLDivElement) 231 | ], EmojiSlider.prototype, "trackBar", void 0); 232 | __decorate([ 233 | query('#cursor'), 234 | __metadata("design:type", HTMLDivElement) 235 | ], EmojiSlider.prototype, "cursor", void 0); 236 | __decorate([ 237 | query('#valueBar'), 238 | __metadata("design:type", HTMLDivElement) 239 | ], EmojiSlider.prototype, "valueBar", void 0); 240 | EmojiSlider = __decorate([ 241 | customElement('emoji-slider') 242 | ], EmojiSlider); 243 | export { EmojiSlider }; 244 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Emoji Slider 8 | 9 | 40 | 41 | 42 | 43 | 44 |
45 |
46 | 47 | 48 |
49 | 50 |
51 |
Customized bar colors:
52 | 53 |
54 | 55 |
56 |
Change emoji by value:
57 | 58 |
59 | 60 |
61 | View on GitHub
67 | 68 |
69 | 70 | 71 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emoji-slider", 3 | "version": "0.2.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.5.5", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", 10 | "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.0.0" 14 | } 15 | }, 16 | "@babel/highlight": { 17 | "version": "7.5.0", 18 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", 19 | "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", 20 | "dev": true, 21 | "requires": { 22 | "chalk": "^2.0.0", 23 | "esutils": "^2.0.2", 24 | "js-tokens": "^4.0.0" 25 | } 26 | }, 27 | "@polymer/polymer": { 28 | "version": "3.3.0", 29 | "resolved": "https://registry.npmjs.org/@polymer/polymer/-/polymer-3.3.0.tgz", 30 | "integrity": "sha512-rij7suomS7DxdBamnwr/Xa0V5hpypf7I9oYKseF2FWz5Xh2a3wJNpVjgJy1adXVCxqIyPhghsrthnfCt7EblsQ==", 31 | "requires": { 32 | "@webcomponents/shadycss": "^1.9.1" 33 | } 34 | }, 35 | "@webcomponents/shadycss": { 36 | "version": "1.9.1", 37 | "resolved": "https://registry.npmjs.org/@webcomponents/shadycss/-/shadycss-1.9.1.tgz", 38 | "integrity": "sha512-IaZOnWOKXHghqk/WfPNDRIgDBi3RsVPY2IFAw6tYiL9UBGvQRy5R6uC+Fk7qTZsReTJ0xh5MTT8yAcb3MUR4mQ==" 39 | }, 40 | "ansi-styles": { 41 | "version": "3.2.1", 42 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 43 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 44 | "dev": true, 45 | "requires": { 46 | "color-convert": "^1.9.0" 47 | } 48 | }, 49 | "argparse": { 50 | "version": "1.0.10", 51 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 52 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 53 | "dev": true, 54 | "requires": { 55 | "sprintf-js": "~1.0.2" 56 | } 57 | }, 58 | "balanced-match": { 59 | "version": "1.0.0", 60 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 61 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 62 | "dev": true 63 | }, 64 | "brace-expansion": { 65 | "version": "1.1.11", 66 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 67 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 68 | "dev": true, 69 | "requires": { 70 | "balanced-match": "^1.0.0", 71 | "concat-map": "0.0.1" 72 | } 73 | }, 74 | "builtin-modules": { 75 | "version": "1.1.1", 76 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 77 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 78 | "dev": true 79 | }, 80 | "chalk": { 81 | "version": "2.4.2", 82 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 83 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 84 | "dev": true, 85 | "requires": { 86 | "ansi-styles": "^3.2.1", 87 | "escape-string-regexp": "^1.0.5", 88 | "supports-color": "^5.3.0" 89 | } 90 | }, 91 | "color-convert": { 92 | "version": "1.9.3", 93 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 94 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 95 | "dev": true, 96 | "requires": { 97 | "color-name": "1.1.3" 98 | } 99 | }, 100 | "color-name": { 101 | "version": "1.1.3", 102 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 103 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 104 | "dev": true 105 | }, 106 | "commander": { 107 | "version": "2.20.0", 108 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", 109 | "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", 110 | "dev": true 111 | }, 112 | "concat-map": { 113 | "version": "0.0.1", 114 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 115 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 116 | "dev": true 117 | }, 118 | "diff": { 119 | "version": "3.5.0", 120 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 121 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 122 | "dev": true 123 | }, 124 | "escape-string-regexp": { 125 | "version": "1.0.5", 126 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 127 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 128 | "dev": true 129 | }, 130 | "esprima": { 131 | "version": "4.0.1", 132 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 133 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 134 | "dev": true 135 | }, 136 | "esutils": { 137 | "version": "2.0.3", 138 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 139 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 140 | "dev": true 141 | }, 142 | "fs.realpath": { 143 | "version": "1.0.0", 144 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 145 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 146 | "dev": true 147 | }, 148 | "glob": { 149 | "version": "7.1.4", 150 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", 151 | "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", 152 | "dev": true, 153 | "requires": { 154 | "fs.realpath": "^1.0.0", 155 | "inflight": "^1.0.4", 156 | "inherits": "2", 157 | "minimatch": "^3.0.4", 158 | "once": "^1.3.0", 159 | "path-is-absolute": "^1.0.0" 160 | } 161 | }, 162 | "has-flag": { 163 | "version": "3.0.0", 164 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 165 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 166 | "dev": true 167 | }, 168 | "inflight": { 169 | "version": "1.0.6", 170 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 171 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 172 | "dev": true, 173 | "requires": { 174 | "once": "^1.3.0", 175 | "wrappy": "1" 176 | } 177 | }, 178 | "inherits": { 179 | "version": "2.0.4", 180 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 181 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 182 | "dev": true 183 | }, 184 | "js-tokens": { 185 | "version": "4.0.0", 186 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 187 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 188 | "dev": true 189 | }, 190 | "js-yaml": { 191 | "version": "3.13.1", 192 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", 193 | "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", 194 | "dev": true, 195 | "requires": { 196 | "argparse": "^1.0.7", 197 | "esprima": "^4.0.0" 198 | } 199 | }, 200 | "lit-element": { 201 | "version": "2.2.1", 202 | "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.2.1.tgz", 203 | "integrity": "sha512-ipDcgQ1EpW6Va2Z6dWm79jYdimVepO5GL0eYkZrFvdr0OD/1N260Q9DH+K5HXHFrRoC7dOg+ZpED2XE0TgGdXw==", 204 | "requires": { 205 | "lit-html": "^1.0.0" 206 | } 207 | }, 208 | "lit-html": { 209 | "version": "1.1.2", 210 | "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.1.2.tgz", 211 | "integrity": "sha512-FFlUMKHKi+qG1x1iHNZ1hrtc/zHmfYTyrSvs3/wBTvaNtpZjOZGWzU7efGYVpgp6KvWeKF6ql9/KsCq6Z/mEDA==" 212 | }, 213 | "minimatch": { 214 | "version": "3.0.4", 215 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 216 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 217 | "dev": true, 218 | "requires": { 219 | "brace-expansion": "^1.1.7" 220 | } 221 | }, 222 | "minimist": { 223 | "version": "0.0.8", 224 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 225 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 226 | "dev": true 227 | }, 228 | "mkdirp": { 229 | "version": "0.5.1", 230 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 231 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 232 | "dev": true, 233 | "requires": { 234 | "minimist": "0.0.8" 235 | } 236 | }, 237 | "once": { 238 | "version": "1.4.0", 239 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 240 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 241 | "dev": true, 242 | "requires": { 243 | "wrappy": "1" 244 | } 245 | }, 246 | "path-is-absolute": { 247 | "version": "1.0.1", 248 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 249 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 250 | "dev": true 251 | }, 252 | "path-parse": { 253 | "version": "1.0.6", 254 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 255 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 256 | "dev": true 257 | }, 258 | "resolve": { 259 | "version": "1.12.0", 260 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", 261 | "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", 262 | "dev": true, 263 | "requires": { 264 | "path-parse": "^1.0.6" 265 | } 266 | }, 267 | "semver": { 268 | "version": "5.7.1", 269 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 270 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 271 | "dev": true 272 | }, 273 | "sprintf-js": { 274 | "version": "1.0.3", 275 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 276 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 277 | "dev": true 278 | }, 279 | "supports-color": { 280 | "version": "5.5.0", 281 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 282 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 283 | "dev": true, 284 | "requires": { 285 | "has-flag": "^3.0.0" 286 | } 287 | }, 288 | "tslib": { 289 | "version": "1.10.0", 290 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", 291 | "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", 292 | "dev": true 293 | }, 294 | "tslint": { 295 | "version": "5.18.0", 296 | "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.18.0.tgz", 297 | "integrity": "sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w==", 298 | "dev": true, 299 | "requires": { 300 | "@babel/code-frame": "^7.0.0", 301 | "builtin-modules": "^1.1.1", 302 | "chalk": "^2.3.0", 303 | "commander": "^2.12.1", 304 | "diff": "^3.2.0", 305 | "glob": "^7.1.1", 306 | "js-yaml": "^3.13.1", 307 | "minimatch": "^3.0.4", 308 | "mkdirp": "^0.5.1", 309 | "resolve": "^1.3.2", 310 | "semver": "^5.3.0", 311 | "tslib": "^1.8.0", 312 | "tsutils": "^2.29.0" 313 | } 314 | }, 315 | "tsutils": { 316 | "version": "2.29.0", 317 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", 318 | "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", 319 | "dev": true, 320 | "requires": { 321 | "tslib": "^1.8.1" 322 | } 323 | }, 324 | "typescript": { 325 | "version": "3.5.3", 326 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", 327 | "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", 328 | "dev": true 329 | }, 330 | "wrappy": { 331 | "version": "1.0.2", 332 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 333 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 334 | "dev": true 335 | } 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emoji-slider", 3 | "version": "0.2.0", 4 | "description": "A slider control using emojis", 5 | "main": "./bin/emoji-slider.js", 6 | "module": "./bin/emoji-slider.js", 7 | "types": "./bin/emoji-slider.d.ts", 8 | "scripts": { 9 | "build": "tsc", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/pshihn/emoji-slider.git" 15 | }, 16 | "keywords": [ 17 | "slider", 18 | "webcomponent", 19 | "emoji", 20 | "range" 21 | ], 22 | "author": "Preet Shihn ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/pshihn/emoji-slider/issues" 26 | }, 27 | "homepage": "https://github.com/pshihn/emoji-slider#readme", 28 | "devDependencies": { 29 | "tslint": "^5.18.0", 30 | "typescript": "^3.5.3" 31 | }, 32 | "dependencies": { 33 | "@polymer/polymer": "^3.3.0", 34 | "lit-element": "^2.2.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/emoji-slider.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css, property, customElement, TemplateResult, query } from 'lit-element'; 2 | import { addListener, removeListener } from '@polymer/polymer/lib/utils/gestures'; 3 | 4 | @customElement('emoji-slider') 5 | export class EmojiSlider extends LitElement { 6 | @property() emoji?: string; 7 | @property({ type: Number }) step = 0.1; 8 | 9 | @query('#bar') private trackBar?: HTMLDivElement; 10 | @query('#cursor') private cursor?: HTMLDivElement; 11 | @query('#valueBar') private valueBar?: HTMLDivElement; 12 | 13 | private pctValue = 0; 14 | private dragging = false; 15 | private upHandler = () => this.onUp(); 16 | private downHandler = (e: Event) => this.onDown(e); 17 | private trackHandler = (e: Event) => this.onTrack(e); 18 | private keyHandler = (e: KeyboardEvent) => this.handleKeyDown(e); 19 | 20 | static styles = css` 21 | :host { 22 | display: block; 23 | position: relative; 24 | outline: none; 25 | } 26 | #bar { 27 | position: relative; 28 | cursor: pointer; 29 | padding: 10px 0; 30 | } 31 | #barLine { 32 | background: var(--emoji-slider-bar-color, #e5e5e5); 33 | height: 4px; 34 | position: relative; 35 | } 36 | #valueBar { 37 | background: var(--emoji-slider-bar-active-color, #2196f3); 38 | position: absolute; 39 | left: 0; 40 | top: 0; 41 | height: 100%; 42 | } 43 | #cursor { 44 | position: absolute; 45 | top: 4px; 46 | left: 0px; 47 | transform: translate3d(-8px, 0, 0); 48 | transition: transform 0.28s ease, box-shadow 0.28s ease; 49 | } 50 | #cursor.noemoji { 51 | background: var(--emoji-slider-cursor-color, #d32f2f); 52 | border-radius: 10px; 53 | width: 16px; 54 | height: 16px; 55 | } 56 | #cursor.noemoji span { 57 | display: none; 58 | } 59 | #cursor.emoji { 60 | background: none; 61 | width: auto; 62 | height: auto; 63 | font-size: var(--emoji-slider-font-size, 24px); 64 | line-height: 1.17; 65 | transform: translate3d(-8px, -0.25em, 0); 66 | } 67 | #cursor.active { 68 | transform: translate3d(-8px, 0, 0) scale(1.5); 69 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); 70 | } 71 | #cursor.emoji.active { 72 | transform: translate3d(-8px, -0.25em, 0) scale(1.5); 73 | box-shadow: none; 74 | } 75 | `; 76 | 77 | render(): TemplateResult { 78 | const emojiChar = (this.emoji && this.emoji.length) ? [...this.emoji][0] : ''; 79 | return html` 80 |
81 |
82 |
83 |
84 |
85 | ${emojiChar} 86 |
87 |
88 | `; 89 | } 90 | 91 | firstUpdated() { 92 | this.attachTrackHandlers(); 93 | 94 | // aria 95 | this.setAttribute('role', 'slider'); 96 | this.setAttribute('aria-valuemax', '1'); 97 | this.setAttribute('aria-valuemin', '0'); 98 | this.tabIndex = +(this.getAttribute('tabindex') || 0); 99 | } 100 | 101 | disconnectedCallback() { 102 | super.disconnectedCallback(); 103 | this.detachTrackHandlers(); 104 | } 105 | 106 | private attachTrackHandlers() { 107 | this.detachTrackHandlers(); 108 | const bar = this.trackBar!; 109 | addListener(bar, 'up', this.upHandler); 110 | addListener(bar, 'down', this.downHandler); 111 | addListener(bar, 'track', this.trackHandler); 112 | this.addEventListener('keydown', this.keyHandler); 113 | } 114 | 115 | private detachTrackHandlers() { 116 | const bar = this.trackBar!; 117 | removeListener(bar, 'up', this.upHandler); 118 | removeListener(bar, 'down', this.downHandler); 119 | removeListener(bar, 'track', this.trackHandler); 120 | this.removeEventListener('keydown', this.keyHandler); 121 | } 122 | 123 | private onUp() { 124 | this.trackEnd(); 125 | } 126 | 127 | private onDown(e: Event) { 128 | const event = e as CustomEvent; 129 | event.preventDefault(); 130 | this.setValue(event.detail.x); 131 | this.cursor!.classList.add('active'); 132 | this.focus(); 133 | } 134 | 135 | private onTrack(e: Event) { 136 | const event = e as CustomEvent; 137 | event.stopPropagation(); 138 | switch (event.detail.state) { 139 | case 'start': 140 | this.trackStart(); 141 | break; 142 | case 'track': 143 | this.trackX(event); 144 | break; 145 | case 'end': 146 | this.trackEnd(); 147 | break; 148 | } 149 | } 150 | 151 | private trackStart() { 152 | this.dragging = true; 153 | this.cursor!.classList.add('active'); 154 | } 155 | 156 | private trackEnd() { 157 | this.dragging = false; 158 | this.cursor!.classList.remove('active'); 159 | } 160 | 161 | private trackX(event: CustomEvent) { 162 | if (!this.dragging) { 163 | this.trackStart(); 164 | } 165 | this.setValue(event.detail.x); 166 | } 167 | 168 | private setValue(x: number) { 169 | const rect = this.trackBar!.getBoundingClientRect(); 170 | const pct = Math.max(0, Math.min(rect.width ? ((x - rect.left) / rect.width) : 0, 1)); 171 | if (this.pctValue !== pct) { 172 | this.pctValue = pct; 173 | this.updateValue(); 174 | this.fireEvent('change'); 175 | } 176 | } 177 | 178 | private fireEvent(name: string) { 179 | this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: { value: this.value } })); 180 | } 181 | 182 | set value(v: number) { 183 | this.pctValue = v; 184 | this.updateValue(); 185 | } 186 | 187 | get value(): number { 188 | return this.pctValue; 189 | } 190 | 191 | private updateValue() { 192 | const offset = `${this.pctValue * 100}%` 193 | if (this.cursor) 194 | this.cursor.style.left = offset; 195 | if (this.valueBar) { 196 | this.valueBar.style.width = offset; 197 | } 198 | } 199 | 200 | private handleKeyDown(e: KeyboardEvent) { 201 | switch (e.keyCode) { 202 | case 38: 203 | case 39: { 204 | const newValue = Math.min(1, this.value + this.step); 205 | if (newValue !== this.value) { 206 | this.value = newValue; 207 | this.fireEvent('change'); 208 | } 209 | break; 210 | } 211 | case 37: 212 | case 40: { 213 | const newValue = Math.max(0, this.value - this.step); 214 | if (newValue !== this.value) { 215 | this.value = newValue; 216 | this.fireEvent('change'); 217 | } 218 | break; 219 | } 220 | case 36: 221 | if (this.value !== 0) { 222 | this.setValue(0); 223 | this.fireEvent('change'); 224 | } 225 | break; 226 | case 35: 227 | if (this.value !== 1) { 228 | this.setValue(1); 229 | this.fireEvent('change'); 230 | } 231 | break; 232 | } 233 | } 234 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "es2015", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es2017", 8 | "dom" 9 | ], 10 | "declaration": true, 11 | "outDir": "./bin", 12 | "baseUrl": ".", 13 | "strict": true, 14 | "strictNullChecks": true, 15 | "noImplicitAny": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noImplicitReturns": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "experimentalDecorators": true, 21 | "emitDecoratorMetadata": true 22 | }, 23 | "include": [ 24 | "src/**/*.ts" 25 | ] 26 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-parens": true, 4 | "class-name": true, 5 | "indent": [ 6 | true, 7 | "spaces", 8 | 2 9 | ], 10 | "prefer-const": true, 11 | "no-duplicate-variable": true, 12 | "no-eval": true, 13 | "no-internal-module": true, 14 | "no-trailing-whitespace": false, 15 | "no-var-keyword": true, 16 | "one-line": [ 17 | true, 18 | "check-open-brace", 19 | "check-whitespace" 20 | ], 21 | "quotemark": [ 22 | true, 23 | "single", 24 | "avoid-escape" 25 | ], 26 | "semicolon": [ 27 | true, 28 | "always" 29 | ], 30 | "trailing-comma": [ 31 | true, 32 | "multiline" 33 | ], 34 | "triple-equals": [ 35 | true, 36 | "allow-null-check" 37 | ], 38 | "typedef-whitespace": [ 39 | true, 40 | { 41 | "call-signature": "nospace", 42 | "index-signature": "nospace", 43 | "parameter": "nospace", 44 | "property-declaration": "nospace", 45 | "variable-declaration": "nospace" 46 | } 47 | ], 48 | "variable-name": [ 49 | true, 50 | "ban-keywords" 51 | ], 52 | "whitespace": [ 53 | true, 54 | "check-branch", 55 | "check-decl", 56 | "check-operator", 57 | "check-separator", 58 | "check-type" 59 | ] 60 | } 61 | } --------------------------------------------------------------------------------