├── .editorconfig ├── .eslintrc ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── misc.md │ └── question.md └── workflows │ └── ci.yml ├── .gitignore ├── .gitpod.yml ├── EXAMPLES.md ├── LICENSE ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── scripts ├── build.js └── bundles.js ├── src ├── js │ ├── libs │ │ ├── moveable.js │ │ └── selectable.js │ ├── pickr.js │ ├── template.js │ └── utils │ │ ├── color.js │ │ ├── hsvacolor.js │ │ └── utils.js └── scss │ ├── base.scss │ ├── lib │ ├── _mixins.scss │ └── _variables.scss │ └── themes │ ├── classic.scss │ ├── monolith.scss │ └── nano.scss ├── types └── pickr.d.ts ├── webpack.config.js └── www ├── favicon.png ├── index.css └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended" 4 | ], 5 | "env": { 6 | "browser": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 2022, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "new-cap": "off", 14 | "no-cond-assign": "off" 15 | }, 16 | "globals": { 17 | "VERSION": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | ### Issue 4 | 5 | 1. Try [master](https://github.com/Simonwep/pickr/tree/master)-branch, perhaps the problem has been solved. 6 | 2. [Use the search](https://github.com/Simonwep/pickr/search?type=Issues), maybe there is already an answer. 7 | 3. If not found, [create an issue](https://github.com/Simonwep/pickr/issues/new), please dont forget to carefully describe it how to reproduce it / pay attention to the issue-template. If possible, provide a [JSFiddle](https://jsfiddle.net/). 8 | 9 | *** 10 | 11 | ### Pull Request 12 | 13 | 1. Before a Pull request run `npm run build`. 14 | 2. Please take care about basic commit message convetions, see [Writing Good Commit Messages](https://github.com/erlang/otp/wiki/writing-good-commit-messages). 15 | 3. Pull requests only into [master](https://github.com/Simonwep/pickr/tree/master)-branch. 16 | 17 | *** 18 | 19 | ### Setup 20 | 21 | This project requires [npm](https://nodejs.org/en/). 22 | 23 | 1. Fork this repo on [github](https://github.com/Simonwep/pickr). 24 | 2. Clone locally. 25 | 3. From your local repro run `npm install`. 26 | 4. Run lcoal dev server `npm run dev` and go to `http://localhost:8080/` 27 | 28 | ### Online setup with a single click 29 | 30 | You can also use Gitpod (A free online VS Code-like IDE). With a single click it will launch a workspace and automatically: 31 | 32 | - clone the pickr repo. 33 | - install the dependencies. 34 | - run `yarn run dev`. 35 | 36 | So that you can start straight away. 37 | 38 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/Simonwep/pickr) 39 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Simonwep 2 | patreon: simonwep 3 | custom: ["paypal.me/simonreinisch", "buymeacoffee.com/aVc3krbXQ"] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: unconfirmed 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | #### What is the current behavior? 13 | 14 | #### Please provide the steps to reproduce and create a [JSFiddle](https://jsfiddle.net/Simonwep/qx2Lod6r/). 15 | 16 | 17 | #### What is the expected behavior? 18 | 19 | #### Your environment: 20 | ``` 21 | Version (see Pickr.version): 22 | Used bundle (es5 or normal one): 23 | Used theme (default is classic): 24 | Browser-version: 25 | Operating-system: 26 | ``` 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/misc.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: General question or issue 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | #### Your environment: 12 | ``` 13 | Version (see Pickr.version): 14 | Used bundle (es5 or normal one): 15 | Used theme (default is classic): 16 | Browser-version: 17 | Operating-system: 18 | ``` 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: General request of information / help 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Your question 11 | 12 | 13 | #### Your environment: 14 | ``` 15 | Version (see Pickr.version): 16 | Used bundle (es5 or normal one): 17 | Used theme (default is classic): 18 | Browser-version: 19 | Operating-system: 20 | ``` 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ "push", "pull_request" ] 4 | 5 | jobs: 6 | build: 7 | name: Lint and build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v4 12 | 13 | - name: Setup pnpm 14 | uses: pnpm/action-setup@v3 15 | with: 16 | version: 9.0.6 17 | 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 20 22 | cache: 'pnpm' 23 | 24 | - name: Install dependencies 25 | run: pnpm install --frozen-lockfile 26 | 27 | - name: Lint 28 | run: pnpm run lint 29 | 30 | - name: Build 31 | run: pnpm run build 32 | 33 | - name: Bundle files for deployment 34 | run: tar -cvf github-pages.tar dist www index.html 35 | 36 | - name: Upload artifact for deployment 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: github-pages 40 | path: github-pages.tar 41 | 42 | deploy: 43 | name: Deploy to GitHub Pages 44 | needs: build 45 | permissions: 46 | pages: write 47 | id-token: write 48 | environment: 49 | name: github-pages 50 | url: ${{ steps.deployment.outputs.page_url }} 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Deploy to GitHub Pages 54 | uses: actions/deploy-pages@v4 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | 65 | # IntelliJ 66 | *.iml 67 | /.idea 68 | 69 | # My psd files 70 | /_psd 71 | /dist 72 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: npm install 3 | command: npm run dev 4 | ports: 5 | - port: 3005 6 | onOpen: open-preview 7 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | ### Requested features - immediately brought to life by a bit of code 2 | 3 | #### Saving the current color and closing the popup on `Enter` ([#187](https://github.com/Simonwep/pickr/issues/187)) 4 | 5 | ```js 6 | pickr.on('init', instance => { 7 | 8 | // Grab actual input-element 9 | const {result} = instance.getRoot().interaction; 10 | 11 | // Listen to any key-events 12 | result.addEventListener('keydown', e => { 13 | 14 | // Detect whever the user pressed "Enter" on their keyboard 15 | if (e.key === 'Enter') { 16 | instance.applyColor(); // Save the currently selected color 17 | instance.hide(); // Hide modal 18 | } 19 | }, {capture: true}); 20 | }); 21 | ``` 22 | 23 | #### Extending pickr to add / remove a list of swatches ([#241](https://github.com/Simonwep/pickr/issues/241)) 24 | [@GreenFootballs](https://github.com/GreenFootballs) showed in [#241](https://github.com/Simonwep/pickr/issues/241) a way to extend pickr so that you can add or remove a whole list of swatches: 25 | 26 | > Note: Extending prototypes is generally considered bad practice, but in this case its reasonable as [there won't be any new features](https://github.com/Simonwep/pickr#status-of-this-project). 27 | 28 | ```js 29 | Pickr.prototype.getSwatches = function() { 30 | return this._swatchColors.reduce((arr, swatch) => { 31 | arr.push(swatch.color.toRGBA().toString(0)); 32 | return arr; 33 | }, [] ); 34 | } 35 | 36 | Pickr.prototype.setSwatches = function(swatches) { 37 | if (!swatches.length) return; 38 | for (let i = this._swatchColors.length - 1; i > -1; i--) { 39 | this.removeSwatch(i); 40 | } 41 | swatches.forEach(swatch => this.addSwatch(swatch)); 42 | } 43 | ``` 44 | 45 | --- 46 | 47 | 48 | > Feel free to submit a [PR](https://github.com/Simonwep/pickr/compare) or open 49 | > an [issue](https://github.com/Simonwep/pickr/issues/new?assignees=Simonwep&labels=&template=feature_request.md&title=) if 50 | > you got any ideas for more examples! 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 - 2021 Simon Reinisch 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 | Logo 3 |

4 | 5 |

6 | Flat, Simple, Hackable Color-Picker. 7 |

8 | 9 |
10 | Fully Featured Demo 11 |
12 | 13 |
14 | 15 |

16 | Build Status 19 | Download count 22 | No dependencies 23 | JSDelivr download count 26 | Current version 28 | Support me 31 | Gitpod Ready-to-Code 35 |

36 | 37 |

38 | 39 | 40 | 41 | 42 | 43 | 44 |

45 | 46 |
47 | 48 | ### Features 49 | * 🎨 Themes 50 | * 🔄 Simple usage 51 | * 🚫 Zero dependencies 52 | * 🌈 Multiple color representations 53 | * 🔍 Color comparison 54 | * 🎚️ Opacity control 55 | * 🖱️ Detail adjustments via mouse-wheel 56 | * 📱 Responsive and auto-positioning 57 | * 👆 Supports touch devices 58 | * 🎨 Swatches for quick-selection 59 | * ♿ Fully accessible and i18n 60 | * 🌑 Shadow-dom support 61 | 62 | ### Status of this project 63 | 64 | > [!IMPORTANT] 65 | > This project might continue to get important security- and bug-related updates but its _feature set_ is frozen, and it's highly unlikely that it'll get new features or enhancements. 66 | > 67 | > The reason behind this decision is the way this tool has been build (monolithic, the core is one single file, everything is in plain JS etc.) which makes it incredible hard to maintain, tests become impossible at this stage without a complete rewrite, and the fun is gone at such a level of cramped complexity. 68 | > 69 | > Personally I recommend building these UI-Related "widgets" directly into the app with the framework you're using which takes more time but in return gives you full power of how it should work and look like. Frameworks such as [(p)react](https://preactjs.com/), [vue](https://vuejs.org/) and [svelte](https://svelte.dev/) will make it a breeze to develop such things within a day. 70 | 71 | ### Themes 72 | |Classic|Monolith|Nano| 73 | |-------|--------|----| 74 | |![Classic theme](https://user-images.githubusercontent.com/30767528/59562615-01d35300-902f-11e9-9f07-44c9d16dbb99.png)|![Monolith](https://user-images.githubusercontent.com/30767528/59562603-c9cc1000-902e-11e9-9c84-1a606fa5f206.png)|![Nano](https://user-images.githubusercontent.com/30767528/59562578-8ec9dc80-902e-11e9-9882-2dacad5e6fa5.png)| 75 | 76 | > Nano uses css-grid thus it won't work in older browsers. 77 | 78 | ## Getting Started 79 | ### Node 80 | Note: The readme is always up-to-date with the latest commit. See [Releases](https://github.com/Simonwep/pickr/releases) for installation instructions regarding to the latest version. 81 | 82 | Install via npm: 83 | ```shell 84 | $ npm install @simonwep/pickr 85 | ``` 86 | 87 | Install via yarn: 88 | ```shell 89 | $ yarn add @simonwep/pickr 90 | ``` 91 | 92 | Include code and style: 93 | ```js 94 | 95 | // One of the following themes 96 | import '@simonwep/pickr/dist/themes/classic.min.css'; // 'classic' theme 97 | import '@simonwep/pickr/dist/themes/monolith.min.css'; // 'monolith' theme 98 | import '@simonwep/pickr/dist/themes/nano.min.css'; // 'nano' theme 99 | 100 | // Modern or es5 bundle (pay attention to the note below!) 101 | import Pickr from '@simonwep/pickr'; 102 | import Pickr from '@simonwep/pickr/dist/pickr.es5.min'; 103 | ``` 104 | --- 105 | 106 | > Attention: The es5-bundle (e.g. legacy version) is quite big (around a triple of the modern bundle). 107 | > Please take into consideration to use the modern version and add polyfills later to your final bundle! 108 | > (Or better: give a hint to users that they should use the latest browsers). 109 | > Browsers such as IE are **not supported** (at least not officially). 110 | 111 | ### Browser 112 | 113 | jsdelivr: 114 | ```html 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | ``` 125 | 126 | Be sure to load the `pickr.min.js` (or the es5 version) **after** `pickr.min.css`. Moreover the `script` tag doesn't work with the `defer` attribute. 127 | 128 | ## Usage 129 | ```javascript 130 | // Simple example, see optional options for more configuration. 131 | const pickr = Pickr.create({ 132 | el: '.color-picker', 133 | theme: 'classic', // or 'monolith', or 'nano' 134 | 135 | swatches: [ 136 | 'rgba(244, 67, 54, 1)', 137 | 'rgba(233, 30, 99, 0.95)', 138 | 'rgba(156, 39, 176, 0.9)', 139 | 'rgba(103, 58, 183, 0.85)', 140 | 'rgba(63, 81, 181, 0.8)', 141 | 'rgba(33, 150, 243, 0.75)', 142 | 'rgba(3, 169, 244, 0.7)', 143 | 'rgba(0, 188, 212, 0.7)', 144 | 'rgba(0, 150, 136, 0.75)', 145 | 'rgba(76, 175, 80, 0.8)', 146 | 'rgba(139, 195, 74, 0.85)', 147 | 'rgba(205, 220, 57, 0.9)', 148 | 'rgba(255, 235, 59, 0.95)', 149 | 'rgba(255, 193, 7, 1)' 150 | ], 151 | 152 | components: { 153 | 154 | // Main components 155 | preview: true, 156 | opacity: true, 157 | hue: true, 158 | 159 | // Input / output Options 160 | interaction: { 161 | hex: true, 162 | rgba: true, 163 | hsla: true, 164 | hsva: true, 165 | cmyk: true, 166 | input: true, 167 | clear: true, 168 | save: true 169 | } 170 | } 171 | }); 172 | ``` 173 | 174 | > You can find more examples [here](EXAMPLES.md). 175 | 176 | ## Events 177 | Since version `0.4.x` Pickr is event-driven. Use the `on(event, cb)` and `off(event, cb)` functions to bind / unbind eventlistener. 178 | 179 | | Event | Description | Arguments | 180 | | -------------- | ----------- | --------- | 181 | | `init` | Initialization done - pickr can be used | `PickrInstance` | 182 | | `hide` | Pickr got closed | `PickrInstance` | 183 | | `show` | Pickr got opened | `HSVaColorObject, PickrInstance` | 184 | | `save` | User clicked the save / clear button. Also fired on clear with `null` as color. | `HSVaColorObject or null, PickrInstance` | 185 | | `clear` | User cleared the color. | `PickrInstance` | 186 | | `change` | Color has changed (but not saved). Also fired on `swatchselect` | `HSVaColorObject, eventSource, PickrInstance` | 187 | | `changestop` | User stopped to change the color | `eventSource, PickrInstance` | 188 | | `cancel` | User clicked the cancel button (return to previous color). | `PickrInstance` | 189 | | `swatchselect` | User clicked one of the swatches | `HSVaColorObject, PickrInstance` | 190 | 191 | > Example: 192 | ```js 193 | pickr.on('init', instance => { 194 | console.log('Event: "init"', instance); 195 | }).on('hide', instance => { 196 | console.log('Event: "hide"', instance); 197 | }).on('show', (color, instance) => { 198 | console.log('Event: "show"', color, instance); 199 | }).on('save', (color, instance) => { 200 | console.log('Event: "save"', color, instance); 201 | }).on('clear', instance => { 202 | console.log('Event: "clear"', instance); 203 | }).on('change', (color, source, instance) => { 204 | console.log('Event: "change"', color, source, instance); 205 | }).on('changestop', (source, instance) => { 206 | console.log('Event: "changestop"', source, instance); 207 | }).on('cancel', instance => { 208 | console.log('Event: "cancel"', instance); 209 | }).on('swatchselect', (color, instance) => { 210 | console.log('Event: "swatchselect"', color, instance); 211 | }); 212 | ``` 213 | 214 | Where `source` can be 215 | * `slider` _- Any slider in the UI._ 216 | * `input` _- The user input field._ 217 | * `swatch` _- One of the swatches._ 218 | 219 | ## Options 220 | ```javascript 221 | const pickr = new Pickr({ 222 | 223 | // Selector or element which will be replaced with the actual color-picker. 224 | // Can be a HTMLElement. 225 | el: '.color-picker', 226 | 227 | // Where the pickr-app should be added as child. 228 | container: 'body', 229 | 230 | // Which theme you want to use. Can be 'classic', 'monolith' or 'nano' 231 | theme: 'classic', 232 | 233 | // Nested scrolling is currently not supported and as this would be really sophisticated to add this 234 | // it's easier to set this to true which will hide pickr if the user scrolls the area behind it. 235 | closeOnScroll: false, 236 | 237 | // Custom class which gets added to the pcr-app. Can be used to apply custom styles. 238 | appClass: 'custom-class', 239 | 240 | // Don't replace 'el' Element with the pickr-button, instead use 'el' as a button. 241 | // If true, appendToBody will also be automatically true. 242 | useAsButton: false, 243 | 244 | // Size of gap between pickr (widget) and the corresponding reference (button) in px 245 | padding: 8, 246 | 247 | // If true pickr won't be floating, and instead will append after the in el resolved element. 248 | // It's possible to hide it via .hide() anyway. 249 | inline: false, 250 | 251 | // If true, pickr will be repositioned automatically on page scroll or window resize. 252 | // Can be set to false to make custom positioning easier. 253 | autoReposition: true, 254 | 255 | // Defines the direction in which the knobs of hue and opacity can be moved. 256 | // 'v' => opacity- and hue-slider can both only moved vertically. 257 | // 'hv' => opacity-slider can be moved horizontally and hue-slider vertically. 258 | // Can be used to apply custom layouts 259 | sliders: 'v', 260 | 261 | // Start state. If true 'disabled' will be added to the button's classlist. 262 | disabled: false, 263 | 264 | // If true, the user won't be able to adjust any opacity. 265 | // Opacity will be locked at 1 and the opacity slider will be removed. 266 | // The HSVaColor object also doesn't contain an alpha, so the toString() methods just 267 | // print HSV, HSL, RGB, HEX, etc. 268 | lockOpacity: false, 269 | 270 | // Precision of output string (only effective if components.interaction.input is true) 271 | outputPrecision: 0, 272 | 273 | // Defines change/save behavior: 274 | // - to keep current color in place until Save is pressed, set to `true`, 275 | // - to apply color to button and preview (save) in sync with each change 276 | // (from picker or palette), set to `false`. 277 | comparison: true, 278 | 279 | // Default color. If you're using a named color such as red, white ... set 280 | // a value for defaultRepresentation too as there is no button for named-colors. 281 | default: '#42445a', 282 | 283 | // Optional color swatches. When null, swatches are disabled. 284 | // Types are all those which can be produced by pickr e.g. hex(a), hsv(a), hsl(a), rgb(a), cmyk, and also CSS color names like 'magenta'. 285 | // Example: swatches: ['#F44336', '#E91E63', '#9C27B0', '#673AB7'], 286 | swatches: null, 287 | 288 | // Default color representation of the input/output textbox. 289 | // Valid options are `HEX`, `RGBA`, `HSVA`, `HSLA` and `CMYK`. 290 | defaultRepresentation: 'HEX', 291 | 292 | // Option to keep the color picker always visible. 293 | // You can still hide / show it via 'pickr.hide()' and 'pickr.show()'. 294 | // The save button keeps its functionality, so still fires the onSave event when clicked. 295 | showAlways: false, 296 | 297 | // Close pickr with a keypress. 298 | // Default is 'Escape'. Can be the event key or code. 299 | // (see: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key) 300 | closeWithKey: 'Escape', 301 | 302 | // Defines the position of the color-picker. 303 | // Any combinations of top, left, bottom or right with one of these optional modifiers: start, middle, end 304 | // Examples: top-start / right-end 305 | // If clipping occurs, the color picker will automatically choose its position. 306 | // Pickr uses https://github.com/Simonwep/nanopop as positioning-engine. 307 | position: 'bottom-middle', 308 | 309 | // Enables the ability to change numbers in an input field with the scroll-wheel. 310 | // To use it set the cursor on a position where a number is and scroll, use ctrl to make steps of five 311 | adjustableNumbers: true, 312 | 313 | // Show or hide specific components. 314 | // By default only the palette (and the save button) is visible. 315 | components: { 316 | 317 | // Defines if the palette itself should be visible. 318 | // Will be overwritten with true if preview, opacity or hue are true 319 | palette: true, 320 | 321 | preview: true, // Display comparison between previous state and new color 322 | opacity: true, // Display opacity slider 323 | hue: true, // Display hue slider 324 | 325 | // show or hide components on the bottom interaction bar. 326 | interaction: { 327 | 328 | // Buttons, if you disable one but use the format in default: or setColor() - set the representation-type too! 329 | hex: false, // Display 'input/output format as hex' button (hexadecimal representation of the rgba value) 330 | rgba: false, // Display 'input/output format as rgba' button (red green blue and alpha) 331 | hsla: false, // Display 'input/output format as hsla' button (hue saturation lightness and alpha) 332 | hsva: false, // Display 'input/output format as hsva' button (hue saturation value and alpha) 333 | cmyk: false, // Display 'input/output format as cmyk' button (cyan mangenta yellow key ) 334 | 335 | input: false, // Display input/output textbox which shows the selected color value. 336 | // the format of the input is determined by defaultRepresentation, 337 | // and can be changed by the user with the buttons set by hex, rgba, hsla, etc (above). 338 | cancel: false, // Display Cancel Button, resets the color to the previous state 339 | clear: false, // Display Clear Button; same as cancel, but keeps the window open 340 | save: false, // Display Save Button, 341 | }, 342 | }, 343 | 344 | // Translations, these are the default values. 345 | i18n: { 346 | 347 | // Strings visible in the UI 348 | 'ui:dialog': 'color picker dialog', 349 | 'btn:toggle': 'toggle color picker dialog', 350 | 'btn:swatch': 'color swatch', 351 | 'btn:last-color': 'use previous color', 352 | 'btn:save': 'Save', 353 | 'btn:cancel': 'Cancel', 354 | 'btn:clear': 'Clear', 355 | 356 | // Strings used for aria-labels 357 | 'aria:btn:save': 'save and close', 358 | 'aria:btn:cancel': 'cancel and close', 359 | 'aria:btn:clear': 'clear and close', 360 | 'aria:input': 'color input field', 361 | 'aria:palette': 'color selection area', 362 | 'aria:hue': 'hue selection slider', 363 | 'aria:opacity': 'selection slider' 364 | } 365 | }); 366 | ``` 367 | 368 | ## Selection through a Shadow-DOM 369 | Example setup: 370 | ```html 371 |
372 | #shadow-root 373 |
374 |
375 | #shadow-root 376 |
377 |
378 |
379 |
380 | ``` 381 | 382 | To select the `.pickr` element you can use the custom `>>` shadow-dom-selector in `el`: 383 | ```js 384 | el: '.entry >> .innr .another >> .pickr' 385 | ``` 386 | 387 | Every `ShadowRoot` of the query-result behind a `>>` gets used in the next query selection. 388 | An alternative would be to provide the target-element itself as `el`. 389 | 390 | ## The HSVaColor object 391 | As default color representation is hsva (`hue`, `saturation`, `value` and `alpha`) used, but you can also convert it to other formats as listed below. 392 | 393 | * hsva.toHSVA() _- Converts the object to a hsva array._ 394 | * hsva.toHSLA() _- Converts the object to a hsla array._ 395 | * hsva.toRGBA() _- Converts the object to a rgba array._ 396 | * hsva.toHEXA() _- Converts the object to a hexa-decimal array._ 397 | * hsva.toCMYK() _- Converts the object to a cmyk array._ 398 | * hsva.clone() _- Clones the color object._ 399 | 400 | The `toString()` is overridden, so you can get a color representation string. 401 | 402 | ```javascript 403 | hsva.toRGBA(); // Returns [r, g, b, a] 404 | hsva.toRGBA().toString(); // Returns rgba(r, g, b, a) with highest precision 405 | hsva.toRGBA().toString(3); // Returns rgba(r, g, b, a), rounded to the third decimal 406 | ``` 407 | 408 | ## Methods 409 | * pickr.setHSVA(h`:Number`,s`:Number`,v`:Number`,a`:Float`, silent`:Boolean`) _- Set an color, returns true if the color has been accepted._ 410 | * pickr.setColor(str: `:String | null`, silent`:Boolean`)`:Boolean` _- Parses a string which represents a color (e.g. `#fff`, `rgb(10, 156, 23)`) or name e.g. 'magenta', returns true if the color has been accepted. `null` will clear the color._ 411 | 412 | If `silent` is true (Default is false), the button won't change the current color. 413 | 414 | * pickr.on(event`:String`, cb`:Function`)`:Pickr` _- Appends an event listener to the given corresponding event-name (see section Events)._ 415 | * pickr.off(event`:String`, cb`:Function`)`:Pickr` _- Removes an event listener from the given corresponding event-name (see section Events)._ 416 | * pickr.show()`:Pickr` _- Shows the color-picker._ 417 | * pickr.hide()`:Pickr` _- Hides the color-picker._ 418 | * pickr.disable()`:Pickr` _- Disables pickr and adds the `disabled` class to the button._ 419 | * pickr.enable()`:Pickr` _- Enables pickr and removes the `disabled` class from the button._ 420 | * pickr.isOpen()`:Pickr` _- Returns true if the color picker is currently open._ 421 | * pickr.getRoot()`:Object` _- Returns the dom-tree of pickr as tree-structure._ 422 | * pickr.getColor()`:HSVaColor` _- Returns the current HSVaColor object._ 423 | * pickr.getSelectedColor()`:HSVaColor` _- Returns the currently applied color._ 424 | * pickr.destroy() _- Destroys all functionality._ 425 | * pickr.destroyAndRemove() _- Destroys all functionality and removes the pickr element including the button._ 426 | * pickr.setColorRepresentation(type`:String`)`:Boolean` _- Change the current color-representation. Valid options are `HEX`, `RGBA`, `HSVA`, `HSLA` and `CMYK`, returns false if type was invalid._ 427 | * pickr.getColorRepresentation()`:String` _- Returns the currently used color-representation (eg. `HEXA`, `RGBA`...)_ 428 | * pickr.applyColor(silent`:Boolean`)`:Pickr` _- Same as pressing the save button. If silent is true the `onSave` event won't be called._ 429 | * pickr.addSwatch(color`:String`)`:Boolean` _- Adds a color to the swatch palette. Returns `true` if the color has been successful added to the palette._ 430 | * pickr.removeSwatch(index`:Number`)`:Boolean`_- Removes a color from the swatch palette by its index, returns true if successful._ 431 | 432 | ## Static methods 433 | **Pickr** 434 | * create(options`:Object`)`:Pickr` _- Creates a new instance._ 435 | 436 | **Pickr.utils** 437 | * once(element`:HTMLElement`, event`:String`, fn`:Function`[, options `:Object`]) _- Attach an event handle which will be fired only once_ 438 | * on(elements`:HTMLElement(s)`, events`:String(s)`, fn`:Function`[, options `:Object`]) _- Attach an event handler function._ 439 | * off(elements`:HTMLElement(s)`, event`:String(s)`, fn`:Function`[, options `:Object`]) _- Remove an event handler._ 440 | * createElementFromString(html`:String`)`:HTMLElement` _- Creates an new HTML Element out of this string._ 441 | * eventPath(evt`:Event`)`:[HTMLElement]` _- A polyfill for the event-path event propery._ 442 | * createFromTemplate(str`:String`) _- See [inline doumentation](https://github.com/Simonwep/pickr/blob/master/src/js/lib/utils.js#L88)._ 443 | * resolveElement(val`:String|HTMLElement`) _- Resolves a `HTMLElement`, supports `>>>` as shadow dom selector._ 444 | * adjustableInputNumbers(el`:InputElement`, mapper`:Function`) _- Creates the possibility to change the numbers in an inputfield via mouse scrolling. 445 | The mapper function takes three arguments: the matched number, an multiplier and the index of the match._ 446 | 447 | Use this utils carefully, it's not for sure that they will stay forever! 448 | 449 | ## Static properties 450 | * version _- The current version._ 451 | * I18N_DEFAULTS _- i18n default values._ 452 | * DEFAULT_OPTIONS _- Default options (Do not override this property itself, only change properties of it!)._ 453 | 454 | ## FAQ 455 | > How do I initialize multiple pickr's? Can I access the instance via `class` or `id`? 456 | 457 | No, you can't. You need to keep track of your instance variables - pickr is (not yet) a web-component. 458 | The best option would be to create new elements via `document.createElement` and directly pass it as `el`. 459 | [example](https://jsfiddle.net/Simonwep/9ghk71c3/). 460 | 461 | > I want to use pickr in a form, how can I do that? 462 | 463 | You can use `useAsButton: true` and pass a reference (or selector) of your input-element as `el`. Then you can update the input-element whenever a change was made. [example](https://jsfiddle.net/Simonwep/wL1zyqcd/). 464 | 465 | > I want to update options after mounting pickr, is that possible? 466 | 467 | Unfortunately not. The core-code of this project is rather old (over 2 years), and I made it in my early js-days - the widget is not able to dynamically re-render itself in that way. 468 | You have to destroy and re-initialize it. 469 | 470 | ## Contributing 471 | If you want to open a issue, create a Pull Request or simply want to know how you can run it on your local machine, please read the [Contributing guide](https://github.com/Simonwep/pickr/blob/master/.github/CONTRIBUTING.md). 472 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Pickr 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |

Pickr. Keep it simple.

57 | VIEW ON GITHUB 58 |
59 | 60 |
61 |
62 |
63 |

(Tap it)

64 |
65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simonwep/pickr", 3 | "version": "1.9.1", 4 | "license": "MIT", 5 | "author": "Simon Reinisch ", 6 | "description": "Flat, Simple, Hackable Color-Picker.", 7 | "keywords": [ 8 | "ux", 9 | "pickr", 10 | "color", 11 | "color-picker" 12 | ], 13 | "main": "./dist/pickr.min.js", 14 | "types": "./types/pickr.d.ts", 15 | "module": "./dist/pickr.min.js", 16 | "scripts": { 17 | "build": "node ./scripts/build.js", 18 | "dev": "webpack serve --mode development", 19 | "lint": "eslint ./src/**/*.js", 20 | "lint:fix": "npm run lint -- --fix", 21 | "test:ci": "npm run lint:fix && npm run build" 22 | }, 23 | "homepage": "https://github.com/Simonwep/pickr#readme", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/Simonwep/pickr.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/Simonwep/pickr/issues" 30 | }, 31 | "files": [ 32 | "types", 33 | "dist", 34 | "src/scss" 35 | ], 36 | "devDependencies": { 37 | "@babel/core": "7.24.5", 38 | "@babel/preset-env": "7.24.5", 39 | "autoprefixer": "10.4.19", 40 | "babel-loader": "9.1.3", 41 | "css-loader": "7.1.1", 42 | "eslint": "8.57.0", 43 | "eslint-webpack-plugin": "4.1.0", 44 | "mini-css-extract-plugin": "2.9.0", 45 | "postcss-loader": "8.1.1", 46 | "sass": "1.77.0", 47 | "sass-loader": "14.2.1", 48 | "terser-webpack-plugin": "5.3.10", 49 | "webpack": "5.91.0", 50 | "webpack-cli": "5.1.4", 51 | "webpack-dev-server": "5.0.4", 52 | "webpack-remove-empty-scripts": "1.0.4" 53 | }, 54 | "dependencies": { 55 | "core-js": "3.37.0", 56 | "nanopop": "2.4.2" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const {version} = require('../package'); 5 | const bundles = require('./bundles'); 6 | const util = require('util'); 7 | const webpack = util.promisify(require('webpack')); 8 | const path = require('path'); 9 | 10 | (async () => { 11 | const banner = new webpack.BannerPlugin(`Pickr ${version} MIT | https://github.com/Simonwep/pickr`); 12 | 13 | // CSS 14 | console.log('Bundle themes'); 15 | await webpack({ 16 | mode: 'production', 17 | entry: { 18 | 'classic': path.resolve('./src/scss/themes/classic.scss'), 19 | 'monolith': path.resolve('./src/scss/themes/monolith.scss'), 20 | 'nano': path.resolve('./src/scss/themes/nano.scss') 21 | }, 22 | 23 | output: { 24 | path: path.resolve('./dist/themes') 25 | }, 26 | 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.scss$/, 31 | use: [ 32 | MiniCssExtractPlugin.loader, 33 | 'css-loader', 34 | { 35 | loader: 'postcss-loader', 36 | options: { 37 | postcssOptions: { 38 | plugins: [ 39 | require('autoprefixer') 40 | ] 41 | } 42 | }, 43 | }, 44 | 'sass-loader' 45 | ] 46 | } 47 | ] 48 | }, 49 | 50 | plugins: [ 51 | banner, 52 | new RemoveEmptyScriptsPlugin(), 53 | new MiniCssExtractPlugin({ 54 | filename: '[name].min.css' 55 | }) 56 | ] 57 | }); 58 | 59 | // Chaining promises to prevent issues caused by both filename configurations 60 | // writing a minified CSS file; both processes having handles on the files can 61 | // result in strange suffixes that fail to parse due to an extra `ap*/` 62 | for (const {filename, babelConfig} of bundles) { 63 | console.log(`Bundle ${filename}`); 64 | 65 | await webpack({ 66 | mode: 'production', 67 | entry: path.resolve('./src/js/pickr.js'), 68 | 69 | output: { 70 | filename, 71 | path: path.resolve('./dist'), 72 | library: 'Pickr', 73 | libraryExport: 'default', 74 | libraryTarget: 'umd' 75 | }, 76 | 77 | module: { 78 | rules: [ 79 | { 80 | test: /\.m?js$/, 81 | exclude: /@babel(?:\/|\\{1,2})runtime|core-js/, 82 | include: [ 83 | path.join(__dirname, '..', 'node_modules/nanopop'), 84 | path.join(__dirname, '..', 'src') 85 | ], 86 | use: [ 87 | { 88 | loader: 'babel-loader', 89 | options: babelConfig 90 | } 91 | ] 92 | } 93 | ] 94 | }, 95 | 96 | plugins: [ 97 | banner, 98 | new webpack.SourceMapDevToolPlugin({ 99 | filename: `${filename}.map` 100 | }), 101 | new webpack.DefinePlugin({ 102 | VERSION: JSON.stringify(version) 103 | }) 104 | ], 105 | 106 | optimization: { 107 | minimizer: [ 108 | new TerserPlugin({ 109 | extractComments: false 110 | }) 111 | ] 112 | } 113 | }); 114 | } 115 | })(); 116 | -------------------------------------------------------------------------------- /scripts/bundles.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | 'filename': 'pickr.es5.min.js', 4 | 'babelConfig': { 5 | 'babelrc': false, 6 | 'presets': [ 7 | [ 8 | '@babel/preset-env', 9 | { 10 | 'targets': '> 1%, ie 11', 11 | 'useBuiltIns': 'usage', 12 | 'corejs': 3, 13 | 'loose': true 14 | } 15 | ] 16 | ] 17 | } 18 | }, 19 | { 20 | 'filename': 'pickr.min.js', 21 | 'babelConfig': { 22 | 'babelrc': false, 23 | 'presets': [ 24 | [ 25 | '@babel/preset-env', 26 | { 27 | 'targets': '> 1.5%, not dead, not ie <= 11' 28 | } 29 | ] 30 | ] 31 | } 32 | } 33 | ]; 34 | -------------------------------------------------------------------------------- /src/js/libs/moveable.js: -------------------------------------------------------------------------------- 1 | import * as _ from '../utils/utils'; 2 | 3 | const clamp = v => Math.max(Math.min(v, 1), 0); 4 | export default function Moveable(opt) { 5 | 6 | const that = { 7 | 8 | // Assign default values 9 | options: Object.assign({ 10 | lock: null, 11 | onchange: () => 0, 12 | onstop: () => 0 13 | }, opt), 14 | 15 | _keyboard(e) { 16 | const {options} = that; 17 | const {type, key} = e; 18 | 19 | // Check to see if the Movable is focused and then move it based on arrow key inputs 20 | // For improved accessibility 21 | if (document.activeElement === options.wrapper) { 22 | const {lock} = that.options; 23 | const up = key === 'ArrowUp'; 24 | const right = key === 'ArrowRight'; 25 | const down = key === 'ArrowDown'; 26 | const left = key === 'ArrowLeft'; 27 | 28 | if (type === 'keydown' && (up || right || down || left)) { 29 | let xm = 0; 30 | let ym = 0; 31 | 32 | if (lock === 'v') { 33 | xm = (up || right) ? 1 : -1; 34 | } else if (lock === 'h') { 35 | xm = (up || right) ? -1 : 1; 36 | } else { 37 | ym = up ? -1 : (down ? 1 : 0); 38 | xm = left ? -1 : (right ? 1 : 0); 39 | } 40 | 41 | that.update( 42 | clamp(that.cache.x + (0.01 * xm)), 43 | clamp(that.cache.y + (0.01 * ym)) 44 | ); 45 | e.preventDefault(); 46 | } else if (key.startsWith('Arrow')) { 47 | that.options.onstop(); 48 | e.preventDefault(); 49 | } 50 | } 51 | }, 52 | 53 | _tapstart(evt) { 54 | _.on(document, ['mouseup', 'touchend', 'touchcancel'], that._tapstop); 55 | _.on(document, ['mousemove', 'touchmove'], that._tapmove); 56 | 57 | if (evt.cancelable) { 58 | evt.preventDefault(); 59 | } 60 | 61 | // Trigger 62 | that._tapmove(evt); 63 | }, 64 | 65 | _tapmove(evt) { 66 | const {options, cache} = that; 67 | const {lock, element, wrapper} = options; 68 | const b = wrapper.getBoundingClientRect(); 69 | 70 | let x = 0, y = 0; 71 | if (evt) { 72 | const touch = evt && evt.touches && evt.touches[0]; 73 | x = evt ? (touch || evt).clientX : 0; 74 | y = evt ? (touch || evt).clientY : 0; 75 | 76 | // Reset to bounds 77 | if (x < b.left) { 78 | x = b.left; 79 | } else if (x > b.left + b.width) { 80 | x = b.left + b.width; 81 | } 82 | if (y < b.top) { 83 | y = b.top; 84 | } else if (y > b.top + b.height) { 85 | y = b.top + b.height; 86 | } 87 | 88 | // Normalize 89 | x -= b.left; 90 | y -= b.top; 91 | } else if (cache) { 92 | x = cache.x * b.width; 93 | y = cache.y * b.height; 94 | } 95 | 96 | if (lock !== 'h') { 97 | element.style.left = `calc(${x / b.width * 100}% - ${element.offsetWidth / 2}px)`; 98 | } 99 | 100 | if (lock !== 'v') { 101 | element.style.top = `calc(${y / b.height * 100}% - ${element.offsetHeight / 2}px)`; 102 | } 103 | 104 | that.cache = {x: x / b.width, y: y / b.height}; 105 | const cx = clamp(x / b.width); 106 | const cy = clamp(y / b.height); 107 | 108 | switch (lock) { 109 | case 'v': 110 | return options.onchange(cx); 111 | case 'h': 112 | return options.onchange(cy); 113 | default: 114 | return options.onchange(cx, cy); 115 | } 116 | }, 117 | 118 | _tapstop() { 119 | that.options.onstop(); 120 | _.off(document, ['mouseup', 'touchend', 'touchcancel'], that._tapstop); 121 | _.off(document, ['mousemove', 'touchmove'], that._tapmove); 122 | }, 123 | 124 | trigger() { 125 | that._tapmove(); 126 | }, 127 | 128 | update(x = 0, y = 0) { 129 | const {left, top, width, height} = that.options.wrapper.getBoundingClientRect(); 130 | 131 | if (that.options.lock === 'h') { 132 | y = x; 133 | } 134 | 135 | that._tapmove({ 136 | clientX: left + width * x, 137 | clientY: top + height * y 138 | }); 139 | }, 140 | 141 | destroy() { 142 | const {options, _tapstart, _keyboard} = that; 143 | _.off(document, ['keydown', 'keyup'], _keyboard); 144 | _.off([options.wrapper, options.element], 'mousedown', _tapstart); 145 | _.off([options.wrapper, options.element], 'touchstart', _tapstart, { 146 | passive: false 147 | }); 148 | } 149 | }; 150 | 151 | // Initilize 152 | const {options, _tapstart, _keyboard} = that; 153 | _.on([options.wrapper, options.element], 'mousedown', _tapstart); 154 | _.on([options.wrapper, options.element], 'touchstart', _tapstart, { 155 | passive: false 156 | }); 157 | 158 | _.on(document, ['keydown', 'keyup'], _keyboard); 159 | 160 | return that; 161 | } 162 | -------------------------------------------------------------------------------- /src/js/libs/selectable.js: -------------------------------------------------------------------------------- 1 | import * as _ from '../utils/utils'; 2 | 3 | export default function Selectable(opt = {}) { 4 | opt = Object.assign({ 5 | onchange: () => 0, 6 | className: '', 7 | elements: [] 8 | }, opt); 9 | 10 | const onTap = _.on(opt.elements, 'click', evt => { 11 | opt.elements.forEach(e => 12 | e.classList[evt.target === e ? 'add' : 'remove'](opt.className) 13 | ); 14 | 15 | opt.onchange(evt); 16 | 17 | // Fix for https://github.com/Simonwep/pickr/issues/243 18 | evt.stopPropagation(); 19 | }); 20 | 21 | return { 22 | destroy: () => _.off(...onTap) 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/js/pickr.js: -------------------------------------------------------------------------------- 1 | import * as _ from './utils/utils'; 2 | import {parseToHSVA} from './utils/color'; 3 | import {HSVaColor} from './utils/hsvacolor'; 4 | import Moveable from './libs/moveable'; 5 | import Selectable from './libs/selectable'; 6 | import buildPickr from './template'; 7 | import {createPopper} from 'nanopop'; 8 | 9 | export default class Pickr { 10 | 11 | // Expose pickr utils 12 | static utils = _; 13 | 14 | // Assign version and export 15 | static version = VERSION; 16 | 17 | // Default strings 18 | static I18N_DEFAULTS = { 19 | 20 | // Strings visible in the UI 21 | 'ui:dialog': 'color picker dialog', 22 | 'btn:toggle': 'toggle color picker dialog', 23 | 'btn:swatch': 'color swatch', 24 | 'btn:last-color': 'use previous color', 25 | 'btn:save': 'Save', 26 | 'btn:cancel': 'Cancel', 27 | 'btn:clear': 'Clear', 28 | 29 | // Strings used for aria-labels 30 | 'aria:btn:save': 'save and close', 31 | 'aria:btn:cancel': 'cancel and close', 32 | 'aria:btn:clear': 'clear and close', 33 | 'aria:input': 'color input field', 34 | 'aria:palette': 'color selection area', 35 | 'aria:hue': 'hue selection slider', 36 | 'aria:opacity': 'selection slider' 37 | }; 38 | 39 | // Default options 40 | static DEFAULT_OPTIONS = { 41 | appClass: null, 42 | theme: 'classic', 43 | useAsButton: false, 44 | padding: 8, 45 | disabled: false, 46 | comparison: true, 47 | closeOnScroll: false, 48 | outputPrecision: 0, 49 | lockOpacity: false, 50 | autoReposition: true, 51 | container: 'body', 52 | 53 | components: { 54 | interaction: {} 55 | }, 56 | 57 | i18n: {}, 58 | swatches: null, 59 | inline: false, 60 | sliders: null, 61 | 62 | default: '#42445a', 63 | defaultRepresentation: null, 64 | position: 'bottom-middle', 65 | adjustableNumbers: true, 66 | showAlways: false, 67 | 68 | closeWithKey: 'Escape' 69 | }; 70 | 71 | // Will be used to prevent specific actions during initilization 72 | _initializingActive = true; 73 | 74 | // If the current color value should be recalculated 75 | _recalc = true; 76 | 77 | // Positioning engine and DOM-Tree 78 | _nanopop = null; 79 | _root = null; 80 | 81 | // Current and last color for comparison 82 | _color = HSVaColor(); 83 | _lastColor = HSVaColor(); 84 | _swatchColors = []; 85 | 86 | // Animation frame used for setup. 87 | // Will be cancelled in case of destruction. 88 | _setupAnimationFrame = null; 89 | 90 | // Evenlistener name: [callbacks] 91 | _eventListener = { 92 | init: [], 93 | save: [], 94 | hide: [], 95 | show: [], 96 | clear: [], 97 | change: [], 98 | changestop: [], 99 | cancel: [], 100 | swatchselect: [] 101 | }; 102 | 103 | constructor(opt) { 104 | 105 | // Assign default values 106 | this.options = opt = Object.assign({...Pickr.DEFAULT_OPTIONS}, opt); 107 | 108 | const {swatches, components, theme, sliders, lockOpacity, padding} = opt; 109 | 110 | if (['nano', 'monolith'].includes(theme) && !sliders) { 111 | opt.sliders = 'h'; 112 | } 113 | 114 | // Check interaction section 115 | if (!components.interaction) { 116 | components.interaction = {}; 117 | } 118 | 119 | // Overwrite palette if preview, opacity or hue are true 120 | const {preview, opacity, hue, palette} = components; 121 | components.opacity = (!lockOpacity && opacity); 122 | components.palette = palette || preview || opacity || hue; 123 | 124 | // Initialize picker 125 | this._preBuild(); 126 | this._buildComponents(); 127 | this._bindEvents(); 128 | this._finalBuild(); 129 | 130 | // Append pre-defined swatch colors 131 | if (swatches && swatches.length) { 132 | swatches.forEach(color => this.addSwatch(color)); 133 | } 134 | 135 | // Initialize positioning engine 136 | const {button, app} = this._root; 137 | this._nanopop = createPopper(button, app, { 138 | margin: padding 139 | }); 140 | 141 | // Initialize accessibility 142 | button.setAttribute('role', 'button'); 143 | button.setAttribute('aria-label', this._t('btn:toggle')); 144 | 145 | // Initilization is finish, pickr is visible and ready for usage 146 | const that = this; 147 | this._setupAnimationFrame = requestAnimationFrame((function cb() { 148 | 149 | // TODO: Performance issue due to high call-rate? 150 | if (!app.offsetWidth) { 151 | return requestAnimationFrame(cb); 152 | } 153 | 154 | // Apply default color 155 | that.setColor(opt.default); 156 | that._rePositioningPicker(); 157 | 158 | // Initialize color representation 159 | if (opt.defaultRepresentation) { 160 | that._representation = opt.defaultRepresentation; 161 | that.setColorRepresentation(that._representation); 162 | } 163 | 164 | // Show pickr if locked 165 | if (opt.showAlways) { 166 | that.show(); 167 | } 168 | 169 | // Initialization is done - pickr is usable, fire init event 170 | that._initializingActive = false; 171 | that._emit('init'); 172 | })); 173 | } 174 | 175 | // Create instance via method 176 | static create = options => new Pickr(options); 177 | 178 | // Does only the absolutly basic thing to initialize the components 179 | _preBuild() { 180 | const {options} = this; 181 | 182 | // Resolve elements 183 | for (const type of ['el', 'container']) { 184 | options[type] = _.resolveElement(options[type]); 185 | } 186 | 187 | // Create element and append it to body to 188 | // Prevent initialization errors 189 | this._root = buildPickr(this); 190 | 191 | // Check if a custom button is used 192 | if (options.useAsButton) { 193 | this._root.button = options.el; // Replace button with customized button 194 | } 195 | 196 | options.container.appendChild(this._root.root); 197 | } 198 | 199 | _finalBuild() { 200 | const opt = this.options; 201 | const root = this._root; 202 | 203 | // Remove from body 204 | opt.container.removeChild(root.root); 205 | 206 | if (opt.inline) { 207 | const parent = opt.el.parentElement; 208 | 209 | if (opt.el.nextSibling) { 210 | parent.insertBefore(root.app, opt.el.nextSibling); 211 | } else { 212 | parent.appendChild(root.app); 213 | } 214 | } else { 215 | opt.container.appendChild(root.app); 216 | } 217 | 218 | // Don't replace the the element if a custom button is used 219 | if (!opt.useAsButton) { 220 | 221 | // Replace element with actual color-picker 222 | opt.el.parentNode.replaceChild(root.root, opt.el); 223 | } else if (opt.inline) { 224 | opt.el.remove(); 225 | } 226 | 227 | // Check if it should be immediatly disabled 228 | if (opt.disabled) { 229 | this.disable(); 230 | } 231 | 232 | // Check if color comparison is disabled, if yes - remove transitions so everything keeps smoothly 233 | if (!opt.comparison) { 234 | root.button.style.transition = 'none'; 235 | 236 | if (!opt.useAsButton) { 237 | root.preview.lastColor.style.transition = 'none'; 238 | } 239 | } 240 | 241 | this.hide(); 242 | } 243 | 244 | _buildComponents() { 245 | 246 | // Instance reference 247 | const inst = this; 248 | const cs = this.options.components; 249 | const sliders = (inst.options.sliders || 'v').repeat(2); 250 | const [so, sh] = sliders.match(/^[vh]+$/g) ? sliders : []; 251 | 252 | // Re-assign if null 253 | const getColor = () => 254 | this._color || (this._color = this._lastColor.clone()); 255 | 256 | const components = { 257 | 258 | palette: Moveable({ 259 | element: inst._root.palette.picker, 260 | wrapper: inst._root.palette.palette, 261 | 262 | onstop: () => inst._emit('changestop', 'slider', inst), 263 | onchange(x, y) { 264 | if (!cs.palette) { 265 | return; 266 | } 267 | 268 | const color = getColor(); 269 | const {_root, options} = inst; 270 | const {lastColor, currentColor} = _root.preview; 271 | 272 | // Update the input field only if the user is currently not typing 273 | if (inst._recalc) { 274 | 275 | // Calculate saturation based on the position 276 | color.s = x * 100; 277 | 278 | // Calculate the value 279 | color.v = 100 - y * 100; 280 | 281 | // Prevent falling under zero 282 | color.v < 0 ? color.v = 0 : 0; 283 | inst._updateOutput('slider'); 284 | } 285 | 286 | // Set picker and gradient color 287 | const cssRGBaString = color.toRGBA().toString(0); 288 | this.element.style.background = cssRGBaString; 289 | this.wrapper.style.background = ` 290 | linear-gradient(to top, rgba(0, 0, 0, ${color.a}), transparent), 291 | linear-gradient(to left, hsla(${color.h}, 100%, 50%, ${color.a}), rgba(255, 255, 255, ${color.a})) 292 | `; 293 | 294 | // Check if color is locked 295 | if (!options.comparison) { 296 | _root.button.style.setProperty('--pcr-color', cssRGBaString); 297 | 298 | // If the user changes the color, remove the cleared icon 299 | _root.button.classList.remove('clear'); 300 | } else if (!options.useAsButton && !inst._lastColor) { 301 | 302 | // Apply color to both the last and current color since the current state is cleared 303 | lastColor.style.setProperty('--pcr-color', cssRGBaString); 304 | } 305 | 306 | // Check if there's a swatch which color matches the current one 307 | const hexa = color.toHEXA().toString(); 308 | for (const {el, color} of inst._swatchColors) { 309 | el.classList[hexa === color.toHEXA().toString() ? 'add' : 'remove']('pcr-active'); 310 | } 311 | 312 | // Change current color 313 | currentColor.style.setProperty('--pcr-color', cssRGBaString); 314 | } 315 | }), 316 | 317 | hue: Moveable({ 318 | lock: sh === 'v' ? 'h' : 'v', 319 | element: inst._root.hue.picker, 320 | wrapper: inst._root.hue.slider, 321 | 322 | onstop: () => inst._emit('changestop', 'slider', inst), 323 | onchange(v) { 324 | if (!cs.hue || !cs.palette) { 325 | return; 326 | } 327 | 328 | const color = getColor(); 329 | 330 | // Calculate hue 331 | if (inst._recalc) { 332 | color.h = v * 360; 333 | } 334 | 335 | // Update color 336 | this.element.style.backgroundColor = `hsl(${color.h}, 100%, 50%)`; 337 | components.palette.trigger(); 338 | } 339 | }), 340 | 341 | opacity: Moveable({ 342 | lock: so === 'v' ? 'h' : 'v', 343 | element: inst._root.opacity.picker, 344 | wrapper: inst._root.opacity.slider, 345 | 346 | onstop: () => inst._emit('changestop', 'slider', inst), 347 | onchange(v) { 348 | if (!cs.opacity || !cs.palette) { 349 | return; 350 | } 351 | 352 | const color = getColor(); 353 | 354 | // Calculate opacity 355 | if (inst._recalc) { 356 | color.a = Math.round(v * 1e2) / 100; 357 | } 358 | 359 | // Update color 360 | this.element.style.background = `rgba(0, 0, 0, ${color.a})`; 361 | components.palette.trigger(); 362 | } 363 | }), 364 | 365 | selectable: Selectable({ 366 | elements: inst._root.interaction.options, 367 | className: 'active', 368 | 369 | onchange(e) { 370 | inst._representation = e.target.getAttribute('data-type').toUpperCase(); 371 | inst._recalc && inst._updateOutput('swatch'); 372 | } 373 | }) 374 | }; 375 | 376 | this._components = components; 377 | } 378 | 379 | _bindEvents() { 380 | const {_root, options} = this; 381 | 382 | const eventBindings = [ 383 | 384 | // Clear color 385 | _.on(_root.interaction.clear, 'click', () => this._clearColor()), 386 | 387 | // Select last color on click 388 | _.on([ 389 | _root.interaction.cancel, 390 | _root.preview.lastColor 391 | ], 'click', () => { 392 | this.setHSVA(...(this._lastColor || this._color).toHSVA(), true); 393 | this._emit('cancel'); 394 | }), 395 | 396 | // Save color 397 | _.on(_root.interaction.save, 'click', () => { 398 | !this.applyColor() && !options.showAlways && this.hide(); 399 | }), 400 | 401 | // User input 402 | _.on(_root.interaction.result, ['keyup', 'input'], e => { 403 | 404 | // Fire listener if initialization is finish and changed color was valid 405 | if (this.setColor(e.target.value, true) && !this._initializingActive) { 406 | this._emit('change', this._color, 'input', this); 407 | this._emit('changestop', 'input', this); 408 | } 409 | 410 | e.stopImmediatePropagation(); 411 | }), 412 | 413 | // Detect user input and disable auto-recalculation 414 | _.on(_root.interaction.result, ['focus', 'blur'], e => { 415 | this._recalc = e.type === 'blur'; 416 | this._recalc && this._updateOutput(null); 417 | }), 418 | 419 | // Cancel input detection on color change 420 | _.on([ 421 | _root.palette.palette, 422 | _root.palette.picker, 423 | _root.hue.slider, 424 | _root.hue.picker, 425 | _root.opacity.slider, 426 | _root.opacity.picker 427 | ], ['mousedown', 'touchstart'], () => this._recalc = true, {passive: true}) 428 | ]; 429 | 430 | // Provide hiding / showing abilities only if showAlways is false 431 | if (!options.showAlways) { 432 | const ck = options.closeWithKey; 433 | 434 | eventBindings.push( 435 | 436 | // Save and hide / show picker 437 | _.on(_root.button, 'click', () => this.isOpen() ? this.hide() : this.show()), 438 | 439 | // Close with escape key 440 | _.on(document, 'keyup', e => this.isOpen() && (e.key === ck || e.code === ck) && this.hide()), 441 | 442 | // Cancel selecting if the user taps behind the color picker 443 | _.on(document, ['touchstart', 'mousedown'], e => { 444 | if (this.isOpen() && !_.eventPath(e).some(el => el === _root.app || el === _root.button)) { 445 | this.hide(); 446 | } 447 | }, {capture: true}) 448 | ); 449 | } 450 | 451 | // Make input adjustable if enabled 452 | if (options.adjustableNumbers) { 453 | const ranges = { 454 | rgba: [255, 255, 255, 1], 455 | hsva: [360, 100, 100, 1], 456 | hsla: [360, 100, 100, 1], 457 | cmyk: [100, 100, 100, 100] 458 | }; 459 | 460 | _.adjustableInputNumbers(_root.interaction.result, (o, step, index) => { 461 | const range = ranges[this.getColorRepresentation().toLowerCase()]; 462 | 463 | if (range) { 464 | const max = range[index]; 465 | 466 | // Calculate next reasonable number 467 | const nv = o + (max >= 100 ? step * 1000 : step); 468 | 469 | // Apply range of zero up to max, fix floating-point issues 470 | return nv <= 0 ? 0 : Number((nv < max ? nv : max).toPrecision(3)); 471 | } 472 | 473 | return o; 474 | }); 475 | } 476 | 477 | if (options.autoReposition && !options.inline) { 478 | let timeout = null; 479 | const that = this; 480 | 481 | // Re-calc position on window resize, scroll and wheel 482 | eventBindings.push( 483 | _.on(window, ['scroll', 'resize'], () => { 484 | if (that.isOpen()) { 485 | 486 | if (options.closeOnScroll) { 487 | that.hide(); 488 | } 489 | 490 | if (timeout === null) { 491 | timeout = setTimeout(() => timeout = null, 100); 492 | 493 | // Update position on every frame 494 | requestAnimationFrame(function rs() { 495 | that._rePositioningPicker(); 496 | (timeout !== null) && requestAnimationFrame(rs); 497 | }); 498 | } else { 499 | clearTimeout(timeout); 500 | timeout = setTimeout(() => timeout = null, 100); 501 | } 502 | } 503 | }, {capture: true}) 504 | ); 505 | } 506 | 507 | // Save bindings 508 | this._eventBindings = eventBindings; 509 | } 510 | 511 | _rePositioningPicker() { 512 | const {options} = this; 513 | 514 | // No repositioning needed if inline 515 | if (!options.inline) { 516 | const success = this._nanopop.update({ 517 | container: document.body.getBoundingClientRect(), 518 | position: options.position 519 | }); 520 | 521 | if (!success) { 522 | const el = this._root.app; 523 | const eb = el.getBoundingClientRect(); 524 | el.style.top = `${(window.innerHeight - eb.height) / 2}px`; 525 | el.style.left = `${(window.innerWidth - eb.width) / 2}px`; 526 | } 527 | } 528 | } 529 | 530 | _updateOutput(eventSource) { 531 | const {_root, _color, options} = this; 532 | 533 | // Check if component is present 534 | if (_root.interaction.type()) { 535 | 536 | // Construct function name and call if present 537 | const method = `to${_root.interaction.type().getAttribute('data-type')}`; 538 | _root.interaction.result.value = typeof _color[method] === 'function' ? 539 | _color[method]().toString(options.outputPrecision) : ''; 540 | } 541 | 542 | // Fire listener if initialization is finish 543 | if (!this._initializingActive && this._recalc) { 544 | this._emit('change', _color, eventSource, this); 545 | } 546 | } 547 | 548 | _clearColor(silent = false) { 549 | const {_root, options} = this; 550 | 551 | // Change only the button color if it isn't customized 552 | if (!options.useAsButton) { 553 | _root.button.style.setProperty('--pcr-color', 'rgba(0, 0, 0, 0.15)'); 554 | } 555 | 556 | _root.button.classList.add('clear'); 557 | 558 | if (!options.showAlways) { 559 | this.hide(); 560 | } 561 | 562 | this._lastColor = null; 563 | if (!this._initializingActive && !silent) { 564 | 565 | // Fire listener 566 | this._emit('save', null); 567 | this._emit('clear'); 568 | } 569 | } 570 | 571 | _parseLocalColor(str) { 572 | const {values, type, a} = parseToHSVA(str); 573 | const {lockOpacity} = this.options; 574 | const alphaMakesAChange = a !== undefined && a !== 1; 575 | 576 | // If no opacity is applied, add undefined at the very end which gets 577 | // Set to 1 in setHSVA 578 | if (values && values.length === 3) { 579 | values[3] = undefined; 580 | } 581 | 582 | return { 583 | values: (!values || (lockOpacity && alphaMakesAChange)) ? null : values, 584 | type 585 | }; 586 | } 587 | 588 | _t(key) { 589 | return this.options.i18n[key] || Pickr.I18N_DEFAULTS[key]; 590 | } 591 | 592 | _emit(event, ...args) { 593 | this._eventListener[event].forEach(cb => cb(...args, this)); 594 | } 595 | 596 | on(event, cb) { 597 | this._eventListener[event].push(cb); 598 | return this; 599 | } 600 | 601 | off(event, cb) { 602 | const callBacks = (this._eventListener[event] || []); 603 | const index = callBacks.indexOf(cb); 604 | 605 | if (~index) { 606 | callBacks.splice(index, 1); 607 | } 608 | 609 | return this; 610 | } 611 | 612 | /** 613 | * Appends a color to the swatch palette 614 | * @param color 615 | * @returns {boolean} 616 | */ 617 | addSwatch(color) { 618 | const {values} = this._parseLocalColor(color); 619 | 620 | if (values) { 621 | const {_swatchColors, _root} = this; 622 | const color = HSVaColor(...values); 623 | 624 | // Create new swatch HTMLElement 625 | const el = _.createElementFromString( 626 | `'} 22 | 23 |
24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 |
44 |
45 | 46 |
47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
61 |
62 | 63 | `); 64 | 65 | const int = root.interaction; 66 | 67 | // Select option which is not hidden 68 | int.options.find(o => !o.hidden && !o.classList.add('active')); 69 | 70 | // Append method to find currently active option 71 | int.type = () => int.options.find(e => e.classList.contains('active')); 72 | return root; 73 | }; 74 | -------------------------------------------------------------------------------- /src/js/utils/color.js: -------------------------------------------------------------------------------- 1 | // Shorthands 2 | const {min, max, floor, round} = Math; 3 | 4 | /** 5 | * Tries to convert a color name to rgb/a hex representation 6 | * @param name 7 | * @returns {string | CanvasGradient | CanvasPattern} 8 | */ 9 | function standardizeColor(name) { 10 | 11 | // Since invalid color's will be parsed as black, filter them out 12 | if (name.toLowerCase() === 'black') { 13 | return '#000'; 14 | } 15 | 16 | const ctx = document.createElement('canvas').getContext('2d'); 17 | ctx.fillStyle = name; 18 | return ctx.fillStyle === '#000' ? null : ctx.fillStyle; 19 | } 20 | 21 | /** 22 | * Convert HSV spectrum to RGB. 23 | * @param h Hue 24 | * @param s Saturation 25 | * @param v Value 26 | * @returns {number[]} Array with rgb values. 27 | */ 28 | export function hsvToRgb(h, s, v) { 29 | h = (h / 360) * 6; 30 | s /= 100; 31 | v /= 100; 32 | 33 | const i = floor(h); 34 | 35 | const f = h - i; 36 | const p = v * (1 - s); 37 | const q = v * (1 - f * s); 38 | const t = v * (1 - (1 - f) * s); 39 | 40 | const mod = i % 6; 41 | const r = [v, q, p, p, t, v][mod]; 42 | const g = [t, v, v, q, p, p][mod]; 43 | const b = [p, p, t, v, v, q][mod]; 44 | 45 | return [ 46 | r * 255, 47 | g * 255, 48 | b * 255 49 | ]; 50 | } 51 | 52 | /** 53 | * Convert HSV spectrum to Hex. 54 | * @param h Hue 55 | * @param s Saturation 56 | * @param v Value 57 | * @returns {string[]} Hex values 58 | */ 59 | export function hsvToHex(h, s, v) { 60 | return hsvToRgb(h, s, v).map(v => 61 | round(v).toString(16).padStart(2, '0') 62 | ); 63 | } 64 | 65 | /** 66 | * Convert HSV spectrum to CMYK. 67 | * @param h Hue 68 | * @param s Saturation 69 | * @param v Value 70 | * @returns {number[]} CMYK values 71 | */ 72 | export function hsvToCmyk(h, s, v) { 73 | const rgb = hsvToRgb(h, s, v); 74 | const r = rgb[0] / 255; 75 | const g = rgb[1] / 255; 76 | const b = rgb[2] / 255; 77 | 78 | const k = min(1 - r, 1 - g, 1 - b); 79 | const c = k === 1 ? 0 : (1 - r - k) / (1 - k); 80 | const m = k === 1 ? 0 : (1 - g - k) / (1 - k); 81 | const y = k === 1 ? 0 : (1 - b - k) / (1 - k); 82 | 83 | return [ 84 | c * 100, 85 | m * 100, 86 | y * 100, 87 | k * 100 88 | ]; 89 | } 90 | 91 | /** 92 | * Convert HSV spectrum to HSL. 93 | * @param h Hue 94 | * @param s Saturation 95 | * @param v Value 96 | * @returns {number[]} HSL values 97 | */ 98 | export function hsvToHsl(h, s, v) { 99 | s /= 100; 100 | v /= 100; 101 | 102 | const l = (2 - s) * v / 2; 103 | 104 | if (l !== 0) { 105 | if (l === 1) { 106 | s = 0; 107 | } else if (l < 0.5) { 108 | s = s * v / (l * 2); 109 | } else { 110 | s = s * v / (2 - l * 2); 111 | } 112 | } 113 | 114 | return [ 115 | h, 116 | s * 100, 117 | l * 100 118 | ]; 119 | } 120 | 121 | /** 122 | * Convert RGB to HSV. 123 | * @param r Red 124 | * @param g Green 125 | * @param b Blue 126 | * @return {number[]} HSV values. 127 | */ 128 | function rgbToHsv(r, g, b) { 129 | r /= 255; 130 | g /= 255; 131 | b /= 255; 132 | 133 | const minVal = min(r, g, b); 134 | const maxVal = max(r, g, b); 135 | const delta = maxVal - minVal; 136 | 137 | let h, s; 138 | const v = maxVal; 139 | if (delta === 0) { 140 | h = s = 0; 141 | } else { 142 | s = delta / maxVal; 143 | const dr = (((maxVal - r) / 6) + (delta / 2)) / delta; 144 | const dg = (((maxVal - g) / 6) + (delta / 2)) / delta; 145 | const db = (((maxVal - b) / 6) + (delta / 2)) / delta; 146 | 147 | if (r === maxVal) { 148 | h = db - dg; 149 | } else if (g === maxVal) { 150 | h = (1 / 3) + dr - db; 151 | } else if (b === maxVal) { 152 | h = (2 / 3) + dg - dr; 153 | } 154 | 155 | if (h < 0) { 156 | h += 1; 157 | } else if (h > 1) { 158 | h -= 1; 159 | } 160 | } 161 | 162 | return [ 163 | h * 360, 164 | s * 100, 165 | v * 100 166 | ]; 167 | } 168 | 169 | /** 170 | * Convert CMYK to HSV. 171 | * @param c Cyan 172 | * @param m Magenta 173 | * @param y Yellow 174 | * @param k Key (Black) 175 | * @return {number[]} HSV values. 176 | */ 177 | function cmykToHsv(c, m, y, k) { 178 | c /= 100; 179 | m /= 100; 180 | y /= 100; 181 | k /= 100; 182 | 183 | const r = (1 - min(1, c * (1 - k) + k)) * 255; 184 | const g = (1 - min(1, m * (1 - k) + k)) * 255; 185 | const b = (1 - min(1, y * (1 - k) + k)) * 255; 186 | 187 | return [...rgbToHsv(r, g, b)]; 188 | } 189 | 190 | /** 191 | * Convert HSL to HSV. 192 | * @param h Hue 193 | * @param s Saturation 194 | * @param l Lightness 195 | * @return {number[]} HSV values. 196 | */ 197 | function hslToHsv(h, s, l) { 198 | s /= 100; 199 | l /= 100; 200 | s *= l < 0.5 ? l : 1 - l; 201 | 202 | const ns = (2 * s / (l + s)) * 100; 203 | const v = (l + s) * 100; 204 | return [h, isNaN(ns) ? 0 : ns, v]; 205 | } 206 | 207 | /** 208 | * Convert HEX to HSV. 209 | * @param hex Hexadecimal string of rgb colors, can have length 3 or 6. 210 | * @return {number[]} HSV values. 211 | */ 212 | function hexToHsv(hex) { 213 | return rgbToHsv(...hex.match(/.{2}/g).map(v => parseInt(v, 16))); 214 | } 215 | 216 | /** 217 | * Try's to parse a string which represents a color to a HSV array. 218 | * Current supported types are cmyk, rgba, hsla and hexadecimal. 219 | * @param str 220 | * @return {*} 221 | */ 222 | export function parseToHSVA(str) { 223 | 224 | // Check if string is a color-name 225 | str = str.match(/^[a-zA-Z]+$/) ? standardizeColor(str) : str; 226 | 227 | // Regular expressions to match different types of color represention 228 | const regex = { 229 | cmyk: /^cmyk\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)/i, 230 | rgba: /^rgba?\D+([\d.]+)(%?)\D+([\d.]+)(%?)\D+([\d.]+)(%?)\D*?(([\d.]+)(%?)|$)/i, 231 | hsla: /^hsla?\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D*?(([\d.]+)(%?)|$)/i, 232 | hsva: /^hsva?\D+([\d.]+)\D+([\d.]+)\D+([\d.]+)\D*?(([\d.]+)(%?)|$)/i, 233 | hexa: /^#?(([\dA-Fa-f]{3,4})|([\dA-Fa-f]{6})|([\dA-Fa-f]{8}))$/i 234 | }; 235 | 236 | /** 237 | * Takes an Array of any type, convert strings which represents 238 | * a number to a number an anything else to undefined. 239 | * @param array 240 | * @return {*} 241 | */ 242 | const numarize = array => array.map(v => /^(|\d+)\.\d+|\d+$/.test(v) ? Number(v) : undefined); 243 | 244 | let match; 245 | invalid: for (const type in regex) { 246 | 247 | // Check if current scheme passed 248 | if (!(match = regex[type].exec(str))) { 249 | continue; 250 | } 251 | 252 | // Try to convert 253 | switch (type) { 254 | case 'cmyk': { 255 | const [, c, m, y, k] = numarize(match); 256 | 257 | if (c > 100 || m > 100 || y > 100 || k > 100) { 258 | break invalid; 259 | } 260 | 261 | return {values: cmykToHsv(c, m, y, k), type}; 262 | } 263 | case 'rgba': { 264 | let [, r, , g, , b, , , a] = numarize(match); 265 | 266 | r = match[2] === '%' ? (r / 100) * 255 : r; 267 | g = match[4] === '%' ? (g / 100) * 255 : g; 268 | b = match[6] === '%' ? (b / 100) * 255 : b; 269 | a = match[9] === '%' ? (a / 100) : a; 270 | 271 | if (r > 255 || g > 255 || b > 255 || a < 0 || a > 1) { 272 | break invalid; 273 | } 274 | 275 | return {values: [...rgbToHsv(r, g, b), a], a, type}; 276 | } 277 | case 'hexa': { 278 | let [, hex] = match; 279 | 280 | if (hex.length === 4 || hex.length === 3) { 281 | hex = hex.split('').map(v => v + v).join(''); 282 | } 283 | 284 | const raw = hex.substring(0, 6); 285 | let a = hex.substring(6); 286 | 287 | // Convert 0 - 255 to 0 - 1 for opacity 288 | a = a ? (parseInt(a, 16) / 255) : undefined; 289 | 290 | return {values: [...hexToHsv(raw), a], a, type}; 291 | } 292 | case 'hsla': { 293 | let [, h, s, l, , a] = numarize(match); 294 | a = match[6] === '%' ? (a / 100) : a; 295 | 296 | if (h > 360 || s > 100 || l > 100 || a < 0 || a > 1) { 297 | break invalid; 298 | } 299 | 300 | return {values: [...hslToHsv(h, s, l), a], a, type}; 301 | } 302 | case 'hsva': { 303 | let [, h, s, v, , a] = numarize(match); 304 | a = match[6] === '%' ? (a / 100) : a; 305 | 306 | if (h > 360 || s > 100 || v > 100 || a < 0 || a > 1) { 307 | break invalid; 308 | } 309 | 310 | return {values: [h, s, v, a], a, type}; 311 | } 312 | } 313 | } 314 | 315 | return {values: null, type: null}; 316 | } 317 | -------------------------------------------------------------------------------- /src/js/utils/hsvacolor.js: -------------------------------------------------------------------------------- 1 | import {hsvToCmyk, hsvToHex, hsvToHsl, hsvToRgb} from './color'; 2 | 3 | /** 4 | * Simple class which holds the properties 5 | * of the color represention model hsla (hue saturation lightness alpha) 6 | */ 7 | export function HSVaColor(h = 0, s = 0, v = 0, a = 1) { 8 | const mapper = (original, next) => (precision = -1) => { 9 | return next(~precision ? original.map(v => Number(v.toFixed(precision))) : original); 10 | }; 11 | 12 | const that = { 13 | h, s, v, a, 14 | 15 | toHSVA() { 16 | const hsva = [that.h, that.s, that.v, that.a]; 17 | hsva.toString = mapper(hsva, arr => `hsva(${arr[0]}, ${arr[1]}%, ${arr[2]}%, ${that.a})`); 18 | return hsva; 19 | }, 20 | 21 | toHSLA() { 22 | const hsla = [...hsvToHsl(that.h, that.s, that.v), that.a]; 23 | hsla.toString = mapper(hsla, arr => `hsla(${arr[0]}, ${arr[1]}%, ${arr[2]}%, ${that.a})`); 24 | return hsla; 25 | }, 26 | 27 | toRGBA() { 28 | const rgba = [...hsvToRgb(that.h, that.s, that.v), that.a]; 29 | rgba.toString = mapper(rgba, arr => `rgba(${arr[0]}, ${arr[1]}, ${arr[2]}, ${that.a})`); 30 | return rgba; 31 | }, 32 | 33 | toCMYK() { 34 | const cmyk = hsvToCmyk(that.h, that.s, that.v); 35 | cmyk.toString = mapper(cmyk, arr => `cmyk(${arr[0]}%, ${arr[1]}%, ${arr[2]}%, ${arr[3]}%)`); 36 | return cmyk; 37 | }, 38 | 39 | toHEXA() { 40 | const hex = hsvToHex(that.h, that.s, that.v); 41 | 42 | // Check if alpha channel make sense, convert it to 255 number space, convert 43 | // To hex and pad it with zeros if needet. 44 | const alpha = that.a >= 1 ? '' : Number((that.a * 255).toFixed(0)) 45 | .toString(16) 46 | .toUpperCase().padStart(2, '0'); 47 | 48 | alpha && hex.push(alpha); 49 | hex.toString = () => `#${hex.join('').toUpperCase()}`; 50 | return hex; 51 | }, 52 | 53 | clone: () => HSVaColor(that.h, that.s, that.v, that.a) 54 | }; 55 | 56 | return that; 57 | } 58 | -------------------------------------------------------------------------------- /src/js/utils/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-rest-params */ 2 | function eventListener(method, elements, events, fn, options = {}) { 3 | 4 | // Normalize array 5 | if (elements instanceof HTMLCollection || elements instanceof NodeList) { 6 | elements = Array.from(elements); 7 | } else if (!Array.isArray(elements)) { 8 | elements = [elements]; 9 | } 10 | 11 | if (!Array.isArray(events)) { 12 | events = [events]; 13 | } 14 | 15 | for (const el of elements) { 16 | for (const ev of events) { 17 | el[method](ev, fn, {capture: false, ...options}); 18 | } 19 | } 20 | 21 | return Array.prototype.slice.call(arguments, 1); 22 | } 23 | 24 | /** 25 | * Add event(s) to element(s). 26 | * @param elements DOM-Elements 27 | * @param events Event names 28 | * @param fn Callback 29 | * @param options Optional options 30 | * @return Array passed arguments 31 | */ 32 | export const on = eventListener.bind(null, 'addEventListener'); 33 | 34 | /** 35 | * Remove event(s) from element(s). 36 | * @param elements DOM-Elements 37 | * @param events Event names 38 | * @param fn Callback 39 | * @param options Optional options 40 | * @return Array passed arguments 41 | */ 42 | export const off = eventListener.bind(null, 'removeEventListener'); 43 | 44 | /** 45 | * Creates an DOM-Element out of a string (Single element). 46 | * @param html HTML representing a single element 47 | * @returns {Element | null} The element. 48 | */ 49 | export function createElementFromString(html) { 50 | const div = document.createElement('div'); 51 | div.innerHTML = html.trim(); 52 | return div.firstElementChild; 53 | } 54 | 55 | /** 56 | * Creates a new html element, every element which has 57 | * a ':ref' attribute will be saved in a object (which will be returned) 58 | * where the value of ':ref' is the object-key and the value the HTMLElement. 59 | * 60 | * It's possible to create a hierarchy if you add a ':obj' attribute. Every 61 | * sibling will be added to the object which will get the name from the 'data-con' attribute. 62 | * 63 | * If you want to create an Array out of multiple elements, you can use the ':arr' attribute, 64 | * the value defines the key and all elements, which has the same parent and the same 'data-arr' attribute, 65 | * would be added to it. 66 | * 67 | * @param str - The HTML String. 68 | */ 69 | 70 | export function createFromTemplate(str) { 71 | 72 | // Removes an attribute from a HTMLElement and returns the value. 73 | const removeAttribute = (el, name) => { 74 | const value = el.getAttribute(name); 75 | el.removeAttribute(name); 76 | return value; 77 | }; 78 | 79 | // Recursive function to resolve template 80 | const resolve = (element, base = {}) => { 81 | 82 | // Check key and container attribute 83 | const con = removeAttribute(element, ':obj'); 84 | const key = removeAttribute(element, ':ref'); 85 | const subtree = con ? (base[con] = {}) : base; 86 | 87 | // Check and save element 88 | key && (base[key] = element); 89 | for (const child of Array.from(element.children)) { 90 | const arr = removeAttribute(child, ':arr'); 91 | const sub = resolve(child, arr ? {} : subtree); 92 | 93 | if (arr) { 94 | 95 | // Check if there is already an array and add element 96 | (subtree[arr] || (subtree[arr] = [])) 97 | .push(Object.keys(sub).length ? sub : child); 98 | } 99 | } 100 | 101 | return base; 102 | }; 103 | 104 | return resolve(createElementFromString(str)); 105 | } 106 | 107 | /** 108 | * Polyfill for safari & firefox for the eventPath event property. 109 | * @param evt The event object. 110 | * @return [String] event path. 111 | */ 112 | export function eventPath(evt) { 113 | let path = evt.path || (evt.composedPath && evt.composedPath()); 114 | if (path) { 115 | return path; 116 | } 117 | 118 | let el = evt.target.parentElement; 119 | path = [evt.target, el]; 120 | while (el = el.parentElement) { 121 | path.push(el); 122 | } 123 | 124 | path.push(document, window); 125 | return path; 126 | } 127 | 128 | /** 129 | * Resolves a HTMLElement by query. 130 | * @param val 131 | * @returns {null|Document|Element} 132 | */ 133 | export function resolveElement(val) { 134 | if (val instanceof Element) { 135 | return val; 136 | } else if (typeof val === 'string') { 137 | return val.split(/>>/g).reduce((pv, cv, ci, a) => { 138 | pv = pv.querySelector(cv); 139 | return ci < a.length - 1 ? pv.shadowRoot : pv; 140 | }, document); 141 | } 142 | 143 | return null; 144 | } 145 | 146 | /** 147 | * Creates the ability to change numbers in an input field with the scroll-wheel. 148 | * @param el 149 | * @param mapper 150 | */ 151 | export function adjustableInputNumbers(el, mapper = v => v) { 152 | 153 | function handleScroll(e) { 154 | const inc = ([0.001, 0.01, 0.1])[Number(e.shiftKey || e.ctrlKey * 2)] * (e.deltaY < 0 ? 1 : -1); 155 | 156 | let index = 0; 157 | let off = el.selectionStart; 158 | el.value = el.value.replace(/[\d.]+/g, (v, i) => { 159 | 160 | // Check if number is in cursor range and increase it 161 | if (i <= off && i + v.length >= off) { 162 | off = i; 163 | return mapper(Number(v), inc, index); 164 | } 165 | 166 | index++; 167 | return v; 168 | }); 169 | 170 | el.focus(); 171 | el.setSelectionRange(off, off); 172 | 173 | // Prevent default and trigger input event 174 | e.preventDefault(); 175 | el.dispatchEvent(new Event('input')); 176 | } 177 | 178 | // Bind events 179 | on(el, 'focus', () => on(window, 'wheel', handleScroll, {passive: false})); 180 | on(el, 'blur', () => off(window, 'wheel', handleScroll)); 181 | } 182 | -------------------------------------------------------------------------------- /src/scss/base.scss: -------------------------------------------------------------------------------- 1 | @import 'lib/variables'; 2 | @import 'lib/mixins'; 3 | 4 | .pickr { 5 | position: relative; 6 | overflow: visible; 7 | transform: translateY(0); // Create local transform space 8 | 9 | * { 10 | box-sizing: border-box; 11 | outline: none; 12 | border: none; 13 | -webkit-appearance: none; 14 | } 15 | } 16 | 17 | .pickr .pcr-button { 18 | @include transparency-background; 19 | position: relative; 20 | height: 2em; 21 | width: 2em; 22 | padding: 0.5em; 23 | cursor: pointer; 24 | font-family: $font-family; 25 | border-radius: $border-radius-mid; 26 | background: $icon-x no-repeat center; 27 | background-size: 0; 28 | transition: all 0.3s; 29 | 30 | &::before { 31 | z-index: initial; 32 | } 33 | 34 | &::after { 35 | @include pseudo-reset; 36 | height: 100%; 37 | width: 100%; 38 | transition: background 0.3s; 39 | background: var(--pcr-color); 40 | border-radius: $border-radius-mid; 41 | } 42 | 43 | &.clear { 44 | background-size: 70%; 45 | 46 | &::before { 47 | opacity: 0; 48 | } 49 | 50 | &:focus { 51 | @include focus(var(--pcr-color)); 52 | } 53 | } 54 | 55 | &.disabled { 56 | cursor: not-allowed; 57 | } 58 | } 59 | 60 | .pickr, 61 | .pcr-app { 62 | 63 | * { 64 | box-sizing: border-box; 65 | outline: none; 66 | border: none; 67 | -webkit-appearance: none; 68 | } 69 | 70 | input, 71 | button { 72 | &:focus, 73 | &.pcr-active { 74 | @include focus(var(--pcr-color)); 75 | } 76 | } 77 | 78 | .pcr-palette, 79 | .pcr-slider { 80 | transition: box-shadow 0.3s; 81 | 82 | &:focus { 83 | @include focus(rgba(black, 0.25)); 84 | } 85 | } 86 | } 87 | 88 | .pcr-app { 89 | position: fixed; 90 | display: flex; 91 | flex-direction: column; 92 | z-index: 10000; 93 | border-radius: 0.1em; 94 | background: #fff; 95 | opacity: 0; 96 | visibility: hidden; 97 | transition: opacity 0.3s, visibility 0s 0.3s; 98 | font-family: $font-family; 99 | box-shadow: $box-shadow-app; 100 | left: 0; 101 | top: 0; 102 | 103 | &.visible { 104 | transition: opacity 0.3s; 105 | visibility: visible; 106 | opacity: 1; 107 | } 108 | 109 | .pcr-swatches { 110 | 111 | // Flex fallback 112 | display: flex; 113 | flex-wrap: wrap; 114 | margin-top: 0.75em; 115 | 116 | &.pcr-last { 117 | margin: 0; 118 | } 119 | 120 | @supports (display: grid) { 121 | display: grid; 122 | align-items: center; 123 | grid-template-columns: repeat(auto-fit, 1.75em); 124 | } 125 | 126 | > button { 127 | @include transparency-background(6px); 128 | font-size: 1em; 129 | position: relative; 130 | width: calc(1.75em - 5px); 131 | height: calc(1.75em - 5px); 132 | border-radius: 0.15em; 133 | cursor: pointer; 134 | margin: 2.5px; 135 | flex-shrink: 0; 136 | justify-self: center; 137 | transition: all 0.15s; 138 | overflow: hidden; 139 | background: transparent; 140 | z-index: 1; 141 | 142 | &::after { 143 | content: ''; 144 | position: absolute; 145 | top: 0; 146 | left: 0; 147 | width: 100%; 148 | height: 100%; 149 | background: var(--pcr-color); 150 | border: 1px solid rgba(black, 0.05); 151 | border-radius: 0.15em; 152 | box-sizing: border-box; 153 | } 154 | 155 | &:hover { 156 | filter: brightness(1.05); 157 | } 158 | 159 | &:not(.pcr-active) { 160 | box-shadow: none; 161 | } 162 | } 163 | } 164 | 165 | .pcr-interaction { 166 | display: flex; 167 | flex-wrap: wrap; 168 | align-items: center; 169 | margin: 0 -0.2em 0 -0.2em; 170 | 171 | > * { 172 | margin: 0 0.2em; 173 | } 174 | 175 | input { 176 | letter-spacing: 0.07em; 177 | font-size: 0.75em; 178 | text-align: center; 179 | cursor: pointer; 180 | color: $palette-darkgray; 181 | background: $palette-snow-white; 182 | border-radius: $border-radius-mid; 183 | transition: all 0.15s; 184 | padding: 0.45em 0.5em; 185 | margin-top: 0.75em; 186 | 187 | &:hover { 188 | filter: brightness(0.975); 189 | } 190 | 191 | &:focus { 192 | @include focus(); 193 | } 194 | } 195 | 196 | .pcr-result { 197 | color: $palette-darkgray; 198 | text-align: left; 199 | flex: 1 1 8em; 200 | min-width: 8em; 201 | transition: all 0.2s; 202 | border-radius: $border-radius-mid; 203 | background: $palette-snow-white; 204 | cursor: text; 205 | 206 | &::selection { 207 | background: $palette-cloud-blue; 208 | color: #fff; 209 | } 210 | } 211 | 212 | .pcr-type.active { 213 | color: #fff; 214 | background: $palette-cloud-blue; 215 | } 216 | 217 | .pcr-save, 218 | .pcr-cancel, 219 | .pcr-clear { 220 | color: #fff; 221 | width: auto; 222 | } 223 | 224 | .pcr-save, 225 | .pcr-cancel, 226 | .pcr-clear { 227 | color: #fff; 228 | 229 | &:hover { 230 | filter: brightness(0.925); 231 | } 232 | } 233 | 234 | .pcr-save { 235 | background: $palette-cloud-blue; 236 | } 237 | 238 | .pcr-clear, 239 | .pcr-cancel { 240 | background: $palette-soft-red; 241 | 242 | &:focus { 243 | @include focus(rgba($palette-soft-red, 0.75)); 244 | } 245 | } 246 | } 247 | 248 | .pcr-selection { 249 | 250 | .pcr-picker { 251 | position: absolute; 252 | height: 18px; 253 | width: 18px; 254 | border: 2px solid #fff; 255 | border-radius: 100%; 256 | user-select: none; 257 | } 258 | 259 | .pcr-color-palette, 260 | .pcr-color-chooser, 261 | .pcr-color-opacity { 262 | position: relative; 263 | user-select: none; 264 | display: flex; 265 | flex-direction: column; 266 | cursor: grab; 267 | cursor: -moz-grab; 268 | cursor: -webkit-grab; 269 | 270 | &:active { 271 | cursor: grabbing; 272 | cursor: -moz-grabbing; 273 | cursor: -webkit-grabbing; 274 | } 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/scss/lib/_mixins.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | 3 | // Pseudo style reset 4 | @mixin pseudo-reset { 5 | position: absolute; 6 | content: ''; 7 | top: 0; 8 | left: 0; 9 | } 10 | 11 | @mixin transparency-background($size: 0.5em) { 12 | &::before { 13 | @include pseudo-reset; 14 | width: 100%; 15 | height: 100%; 16 | background: $icon-transparency; 17 | background-size: $size; 18 | border-radius: $border-radius-mid; 19 | z-index: -1; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/scss/lib/_variables.scss: -------------------------------------------------------------------------------- 1 | // Font family 2 | $font-family: -apple-system, 3 | BlinkMacSystemFont, 4 | "Segoe UI", 5 | "Roboto", 6 | "Helvetica Neue", Arial, sans-serif, !default; 7 | 8 | // Colors 9 | $palette-cloud-blue: #4285f4 !default; 10 | $palette-soft-red: #f44250 !default; 11 | $palette-snow-white: #f1f3f4 !default; 12 | $palette-lightgray: #c4c4c4 !default; 13 | $palette-darkgray: #75797e !default; 14 | 15 | // Constants 16 | $box-shadow-app: 0 0.15em 1.5em 0 rgba(0, 0, 0, 0.1), 0 0 1em 0 rgba(0, 0, 0, 0.03) !default; 17 | @mixin focus($color: rgba($palette-cloud-blue, 0.75)) { 18 | box-shadow: 0 0 0 1px rgba(white, 0.85), 0 0 0 3px $color; 19 | } 20 | 21 | @function colorRainbow($dir: to bottom) { 22 | @return linear-gradient($dir, 23 | hsl(0, 100%, 50%), 24 | hsl(60, 100%, 50%), 25 | hsl(120, 100%, 50%), 26 | hsl(180, 100%, 50%), 27 | hsl(240, 100%, 50%), 28 | hsl(300, 100%, 50%), 29 | hsl(360, 100%, 50%)); 30 | } 31 | 32 | 33 | // Box shadows 34 | $box-shadow-small: 0 1px 2px 0 rgba(0, 0, 0, 0.2) !default; 35 | 36 | // Border radius 37 | $border-radius-mid: 0.15em !default; 38 | 39 | // Inline SVG muster 40 | $icon-transparency: url('data:image/svg+xml;utf8, ') !default; 41 | $icon-x: url('data:image/svg+xml;utf8, ') !default; 42 | -------------------------------------------------------------------------------- /src/scss/themes/classic.scss: -------------------------------------------------------------------------------- 1 | @import '../lib/variables'; 2 | @import '../lib/mixins'; 3 | @import '../base'; 4 | 5 | .pcr-app[data-theme='classic'] { 6 | width: 28.5em; 7 | max-width: 95vw; 8 | padding: 0.8em; 9 | 10 | .pcr-selection { 11 | display: flex; 12 | justify-content: space-between; 13 | flex-grow: 1; 14 | 15 | .pcr-color-preview { 16 | @include transparency-background; 17 | position: relative; 18 | z-index: 1; 19 | width: 2em; 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: space-between; 23 | margin-right: 0.75em; 24 | 25 | .pcr-last-color { 26 | cursor: pointer; 27 | border-radius: 0.15em 0.15em 0 0; 28 | z-index: 2; 29 | } 30 | 31 | .pcr-current-color { 32 | border-radius: 0 0 0.15em 0.15em; 33 | } 34 | 35 | .pcr-last-color, 36 | .pcr-current-color { 37 | background: var(--pcr-color); 38 | width: 100%; 39 | height: 50%; 40 | } 41 | } 42 | 43 | .pcr-color-palette { 44 | width: 100%; 45 | height: 8em; 46 | z-index: 1; 47 | 48 | .pcr-palette { 49 | flex-grow: 1; 50 | border-radius: $border-radius-mid; 51 | @include transparency-background; 52 | } 53 | } 54 | 55 | .pcr-color-chooser, 56 | .pcr-color-opacity { 57 | margin-left: 0.75em; 58 | 59 | .pcr-picker { 60 | left: 50%; 61 | transform: translateX(-50%); 62 | } 63 | 64 | .pcr-slider { 65 | width: 8px; 66 | flex-grow: 1; 67 | border-radius: 50em; 68 | } 69 | } 70 | 71 | .pcr-color-chooser .pcr-slider { 72 | background: colorRainbow(); 73 | } 74 | 75 | .pcr-color-opacity .pcr-slider { 76 | background: linear-gradient(to bottom, transparent, black), $icon-transparency; 77 | background-size: 100%, 50%; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/scss/themes/monolith.scss: -------------------------------------------------------------------------------- 1 | @import '../lib/variables'; 2 | @import '../lib/mixins'; 3 | @import '../base'; 4 | 5 | .pcr-app[data-theme='monolith'] { 6 | width: 14.25em; 7 | max-width: 95vw; 8 | padding: 0.8em; 9 | 10 | .pcr-selection { 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: space-between; 14 | flex-grow: 1; 15 | 16 | .pcr-color-preview { 17 | @include transparency-background; 18 | position: relative; 19 | z-index: 1; 20 | width: 100%; 21 | height: 1em; 22 | display: flex; 23 | flex-direction: row; 24 | justify-content: space-between; 25 | margin-bottom: 0.5em; 26 | 27 | .pcr-last-color { 28 | cursor: pointer; 29 | transition: background-color 0.3s, box-shadow 0.3s; 30 | border-radius: 0.15em 0 0 0.15em; 31 | z-index: 2; 32 | } 33 | 34 | .pcr-current-color { 35 | border-radius: 0 0.15em 0.15em 0; 36 | } 37 | 38 | .pcr-last-color, 39 | .pcr-current-color { 40 | background: var(--pcr-color); 41 | width: 50%; 42 | height: 100%; 43 | } 44 | } 45 | 46 | .pcr-color-palette { 47 | width: 100%; 48 | height: 8em; 49 | z-index: 1; 50 | 51 | .pcr-palette { 52 | border-radius: $border-radius-mid; 53 | @include transparency-background; 54 | width: 100%; 55 | height: 100%; 56 | } 57 | } 58 | 59 | .pcr-color-chooser, 60 | .pcr-color-opacity { 61 | height: 0.5em; 62 | margin-top: 0.75em; 63 | 64 | .pcr-picker { 65 | top: 50%; 66 | transform: translateY(-50%); 67 | } 68 | 69 | .pcr-slider { 70 | flex-grow: 1; 71 | border-radius: 50em; 72 | } 73 | } 74 | 75 | .pcr-color-chooser .pcr-slider { 76 | background: colorRainbow(to right); 77 | } 78 | 79 | .pcr-color-opacity .pcr-slider { 80 | background: linear-gradient(to right, transparent, black), $icon-transparency; 81 | background-size: 100%, 0.25em; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/scss/themes/nano.scss: -------------------------------------------------------------------------------- 1 | @import '../lib/variables'; 2 | @import '../lib/mixins'; 3 | @import '../base'; 4 | 5 | $padding: 0.6em; 6 | $spacing: 0.6em; 7 | 8 | .pcr-app[data-theme='nano'] { 9 | width: 14.25em; 10 | max-width: 95vw; 11 | 12 | .pcr-swatches { 13 | margin-top: $spacing; 14 | padding: 0 $padding; 15 | } 16 | 17 | .pcr-interaction { 18 | padding: 0 $padding $padding $padding; 19 | } 20 | 21 | .pcr-selection { 22 | display: grid; 23 | grid-gap: $spacing; 24 | grid-template-columns: 1fr 4fr; 25 | grid-template-rows: 5fr auto auto; 26 | align-items: center; 27 | height: 10.5em; 28 | width: 100%; 29 | align-self: flex-start; 30 | 31 | .pcr-color-preview { 32 | grid-area: 2 / 1 / 4 / 1; 33 | height: 100%; 34 | width: 100%; 35 | display: flex; 36 | flex-direction: row; 37 | justify-content: center; 38 | margin-left: $padding; 39 | 40 | .pcr-last-color { 41 | display: none; 42 | } 43 | 44 | .pcr-current-color { 45 | @include transparency-background; 46 | position: relative; 47 | background: var(--pcr-color); 48 | width: 2em; 49 | height: 2em; 50 | border-radius: 50em; 51 | overflow: hidden; 52 | } 53 | } 54 | 55 | .pcr-color-palette { 56 | grid-area: 1 / 1 / 2 / 3; 57 | width: 100%; 58 | height: 100%; 59 | z-index: 1; 60 | 61 | .pcr-palette { 62 | border-radius: $border-radius-mid; 63 | @include transparency-background; 64 | width: 100%; 65 | height: 100%; 66 | } 67 | } 68 | 69 | .pcr-color-chooser { 70 | grid-area: 2 / 2 / 2 / 2; 71 | } 72 | 73 | .pcr-color-opacity { 74 | grid-area: 3 / 2 / 3 / 2; 75 | } 76 | 77 | .pcr-color-chooser, 78 | .pcr-color-opacity { 79 | height: 0.5em; 80 | margin: 0 $padding; 81 | 82 | .pcr-picker { 83 | top: 50%; 84 | transform: translateY(-50%); 85 | } 86 | 87 | .pcr-slider { 88 | flex-grow: 1; 89 | border-radius: 50em; 90 | } 91 | } 92 | 93 | .pcr-color-chooser .pcr-slider { 94 | background: colorRainbow(to right); 95 | } 96 | 97 | .pcr-color-opacity .pcr-slider { 98 | background: linear-gradient(to right, transparent, black), $icon-transparency; 99 | background-size: 100%, 0.25em; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /types/pickr.d.ts: -------------------------------------------------------------------------------- 1 | declare class Pickr { 2 | 3 | static version: string; // Current version 4 | static utils: any; // See docs 5 | static libs: any; // See docs 6 | 7 | constructor(options: Pickr.Options); 8 | 9 | static create(options: Pickr.Options): Pickr; 10 | 11 | setHSVA(h?: number, s?: number, v?: number, a?: number, silent?: boolean): boolean 12 | 13 | setColor(str: string | null, silent?: boolean): boolean; 14 | 15 | on(event: Pickr.EventType, cb: Function): Pickr; 16 | 17 | off(event: Pickr.EventType, cb: Function): Pickr; 18 | 19 | show(): Pickr; 20 | 21 | hide(): Pickr; 22 | 23 | disable(): Pickr; 24 | 25 | enable(): Pickr; 26 | 27 | isOpen(): boolean; 28 | 29 | getRoot(): object; 30 | 31 | getColor(): Pickr.HSVaColor; 32 | 33 | getSelectedColor(): Pickr.HSVaColor; 34 | 35 | destroy(): void; 36 | 37 | destroyAndRemove(): void; 38 | 39 | setColorRepresentation(type: Pickr.Representation): boolean; 40 | 41 | getColorRepresentation(): Pickr.Representation; 42 | 43 | applyColor(silent?: boolean): Pickr; 44 | 45 | addSwatch(color: string): boolean; 46 | 47 | removeSwatch(index: number): boolean; 48 | } 49 | 50 | declare namespace Pickr { 51 | 52 | interface Options { 53 | el: string | HTMLElement; 54 | container?: string | HTMLElement; 55 | theme?: Theme; 56 | closeOnScroll?: boolean; 57 | appClass?: string; 58 | useAsButton?: boolean; 59 | padding?: number; 60 | inline?: boolean; 61 | autoReposition?: boolean; 62 | sliders?: Slider; 63 | disabled?: boolean; 64 | lockOpacity?: boolean; 65 | outputPrecision?: number; 66 | comparison?: boolean; 67 | default?: string; 68 | swatches?: Array | null; 69 | defaultRepresentation?: Representation; 70 | showAlways?: boolean; 71 | closeWithKey?: string; 72 | position?: Position; 73 | adjustableNumbers?: boolean; 74 | 75 | components?: { 76 | palette?: boolean; 77 | preview?: boolean; 78 | opacity?: boolean; 79 | hue?: boolean; 80 | 81 | interaction?: { 82 | hex?: boolean; 83 | rgba?: boolean; 84 | hsla?: boolean; 85 | hsva?: boolean; 86 | cmyk?: boolean; 87 | input?: boolean; 88 | cancel?: boolean; 89 | clear?: boolean; 90 | save?: boolean; 91 | }; 92 | }; 93 | 94 | i18n?: { 95 | 'ui:dialog'?: string; 96 | 'btn:toggle'?: string; 97 | 'btn:swatch'?: string; 98 | 'btn:last-color'?: string; 99 | 'btn:save'?: string; 100 | 'btn:cancel'?: string; 101 | 'btn:clear'?: string; 102 | 'aria:btn:save'?: string; 103 | 'aria:btn:cancel'?: string; 104 | 'aria:btn:clear'?: string; 105 | 'aria:input'?: string; 106 | 'aria:palette'?: string; 107 | 'aria:hue'?: string; 108 | 'aria:opacity'?: string; 109 | } 110 | } 111 | 112 | interface RoundableNumberArray extends Omit, 'toString'> { 113 | 114 | /** 115 | * Uses Number.toFixed to truncate each value to the n-th decimal place. 116 | * @param precision Optional precision / decimal place at which point it should be truncated. 117 | */ 118 | toString(precision?: number): string; 119 | } 120 | 121 | interface HSVaColor { 122 | toHSVA(): RoundableNumberArray; 123 | 124 | toHSLA(): RoundableNumberArray; 125 | 126 | toRGBA(): RoundableNumberArray; 127 | 128 | toCMYK(): RoundableNumberArray; 129 | 130 | toHEXA(): RoundableNumberArray; 131 | 132 | clone(): HSVaColor; 133 | } 134 | 135 | type EventType = 136 | 'init' | 137 | 'hide' | 138 | 'show' | 139 | 'save' | 140 | 'clear' | 141 | 'change' | 142 | 'changestop' | 143 | 'cancel' | 144 | 'swatchselect'; 145 | 146 | type Theme = 'classic' | 'monolith' | 'nano'; 147 | 148 | type Position = 149 | 'top-start' | 150 | 'top-middle' | 151 | 'top-end' | 152 | 'right-start' | 153 | 'right-middle' | 154 | 'right-end' | 155 | 'bottom-start' | 156 | 'bottom-middle' | 157 | 'bottom-end' | 158 | 'left-start' | 159 | 'left-middle' | 160 | 'left-end'; 161 | 162 | type Representation = 163 | 'HEXA' | 164 | 'RGBA' | 165 | 'HSVA' | 166 | 'HSLA' | 167 | 'CMYK'; 168 | 169 | type Slider = 'v' | 'h'; 170 | } 171 | 172 | export default Pickr; 173 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const ESLintPlugin = require('eslint-webpack-plugin'); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | const {version} = require('./package.json'); 4 | const webpack = require('webpack'); 5 | 6 | module.exports = { 7 | entry: { 8 | 'dist/pickr.es5.min': './src/js/pickr.js', 9 | 'dist/themes/classic.min': './src/scss/themes/classic.scss', 10 | 'dist/themes/nano.min': './src/scss/themes/nano.scss', 11 | 'dist/themes/monolith.min': './src/scss/themes/monolith.scss' 12 | }, 13 | 14 | output: { 15 | filename: '[name].js', 16 | library: { 17 | type: 'umd', 18 | name: 'Pickr', 19 | export: 'default', 20 | umdNamedDefine: true 21 | } 22 | }, 23 | 24 | devServer: { 25 | static: '.', 26 | host: '0.0.0.0', 27 | port: 3006 28 | }, 29 | 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.scss$/, 34 | use: [ 35 | MiniCssExtractPlugin.loader, 36 | 'css-loader', 37 | 'sass-loader' 38 | ] 39 | } 40 | ] 41 | }, 42 | 43 | plugins: [ 44 | new ESLintPlugin(), 45 | new MiniCssExtractPlugin({ 46 | filename: '[name].css' 47 | }), 48 | new webpack.DefinePlugin({ 49 | VERSION: JSON.stringify(version) 50 | }) 51 | ] 52 | }; 53 | -------------------------------------------------------------------------------- /www/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonwep/pickr/345091af645c42ee5e021d7cc3f70e71878ead95/www/favicon.png -------------------------------------------------------------------------------- /www/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | -webkit-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | 8 | body, 9 | html { 10 | height: 100%; 11 | width: 100%; 12 | background: white; 13 | font-family: 'Montserrat', serif; 14 | } 15 | 16 | body { 17 | background: linear-gradient(to bottom, #f0f8ff, #ffffff); 18 | } 19 | 20 | body header { 21 | position: relative; 22 | padding: 10vh 0; 23 | text-align: center; 24 | color: #36425b; 25 | } 26 | 27 | body header h1 { 28 | font-size: 3.5em; 29 | font-weight: 300; 30 | font-family: 'Montserrat', sans-serif; 31 | } 32 | 33 | body header a { 34 | display: inline-block; 35 | text-decoration: none; 36 | font-weight: 500; 37 | font-size: 0.8em; 38 | color: white; 39 | margin-top: 5vh; 40 | padding: 0.75em 1.25em; 41 | transition: 0.3s all; 42 | background: #4285f4; 43 | border-radius: 50em; 44 | box-shadow: 0 0.15em 0.5em rgba(66, 133, 244, 0.75); 45 | font-family: 'Montserrat', sans-serif; 46 | } 47 | 48 | body header a:hover { 49 | background: #4291f6; 50 | } 51 | 52 | body main { 53 | margin: 0 auto; 54 | display: flex; 55 | align-items: center; 56 | flex-direction: column; 57 | } 58 | 59 | .theme-container button { 60 | font-family: 'Montserrat', sans-serif; 61 | font-weight: 500; 62 | font-size: 0.95em; 63 | color: #36425b; 64 | outline: none; 65 | background: #e4f1ff; 66 | border: none; 67 | border-bottom: 2px solid rgba(80, 139, 234, 0.67); 68 | padding: 0.6em 0.8em 0.5em; 69 | cursor: pointer; 70 | transition: all 0.3s; 71 | margin: 0 0.5em; 72 | opacity: 0.45; 73 | text-transform: capitalize; 74 | } 75 | 76 | .theme-container button.active { 77 | opacity: 1; 78 | } 79 | 80 | .theme-container h3 { 81 | font-weight: 500; 82 | color: #36425b; 83 | } 84 | 85 | .pickr-container { 86 | margin-top: 2em; 87 | } 88 | 89 | main > p { 90 | margin-top: 0.35em; 91 | font-size: 0.75em; 92 | font-weight: 500; 93 | color: #42445a; 94 | } 95 | 96 | @-webkit-keyframes fadeIn { 97 | from { 98 | opacity: 0; 99 | } 100 | 101 | to { 102 | opacity: 1; 103 | } 104 | } 105 | 106 | @keyframes fadeIn { 107 | from { 108 | opacity: 0; 109 | } 110 | 111 | to { 112 | opacity: 1; 113 | } 114 | } 115 | 116 | @media screen and (max-width: 1000px) { 117 | body header { 118 | font-size: 0.6em; 119 | padding: 7vh 0; 120 | } 121 | 122 | body header a { 123 | padding: 1em 2em; 124 | font-weight: 600; 125 | font-size: 1.05em; 126 | } 127 | 128 | main > section { 129 | min-width: 90%; 130 | } 131 | 132 | main > section h2 { 133 | font-size: 1em; 134 | } 135 | 136 | main > section pre { 137 | font-size: 0.9em; 138 | } 139 | 140 | main section.demo .hint svg { 141 | height: 1.2em; 142 | } 143 | 144 | main section.demo .hint span { 145 | transform: translate3d(-3em, -1.4em, 0); 146 | font-size: 0.6em; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /www/index.js: -------------------------------------------------------------------------------- 1 | const pickrContainer = document.querySelector('.pickr-container'); 2 | const themeContainer = document.querySelector('.theme-container'); 3 | const themes = [ 4 | [ 5 | 'classic', 6 | { 7 | swatches: [ 8 | 'rgba(244, 67, 54, 1)', 9 | 'rgba(233, 30, 99, 0.95)', 10 | 'rgba(156, 39, 176, 0.9)', 11 | 'rgba(103, 58, 183, 0.85)', 12 | 'rgba(63, 81, 181, 0.8)', 13 | 'rgba(33, 150, 243, 0.75)', 14 | 'rgba(3, 169, 244, 0.7)', 15 | 'rgba(0, 188, 212, 0.7)', 16 | 'rgba(0, 150, 136, 0.75)', 17 | 'rgba(76, 175, 80, 0.8)', 18 | 'rgba(139, 195, 74, 0.85)', 19 | 'rgba(205, 220, 57, 0.9)', 20 | 'rgba(255, 235, 59, 0.95)', 21 | 'rgba(255, 193, 7, 1)' 22 | ], 23 | 24 | components: { 25 | preview: true, 26 | opacity: true, 27 | hue: true, 28 | 29 | interaction: { 30 | hex: true, 31 | rgba: true, 32 | hsva: true, 33 | input: true, 34 | clear: true, 35 | save: true 36 | } 37 | } 38 | } 39 | ], 40 | [ 41 | 'monolith', 42 | { 43 | swatches: [ 44 | 'rgba(244, 67, 54, 1)', 45 | 'rgba(233, 30, 99, 0.95)', 46 | 'rgba(156, 39, 176, 0.9)', 47 | 'rgba(103, 58, 183, 0.85)', 48 | 'rgba(63, 81, 181, 0.8)', 49 | 'rgba(33, 150, 243, 0.75)', 50 | 'rgba(3, 169, 244, 0.7)' 51 | ], 52 | 53 | defaultRepresentation: 'HEXA', 54 | components: { 55 | preview: true, 56 | opacity: true, 57 | hue: true, 58 | 59 | interaction: { 60 | hex: false, 61 | rgba: false, 62 | hsva: false, 63 | input: true, 64 | clear: true, 65 | save: true 66 | } 67 | } 68 | } 69 | ], 70 | [ 71 | 'nano', 72 | { 73 | swatches: [ 74 | 'rgba(244, 67, 54, 1)', 75 | 'rgba(233, 30, 99, 0.95)', 76 | 'rgba(156, 39, 176, 0.9)', 77 | 'rgba(103, 58, 183, 0.85)', 78 | 'rgba(63, 81, 181, 0.8)', 79 | 'rgba(33, 150, 243, 0.75)', 80 | 'rgba(3, 169, 244, 0.7)' 81 | ], 82 | 83 | defaultRepresentation: 'HEXA', 84 | components: { 85 | preview: true, 86 | opacity: true, 87 | hue: true, 88 | 89 | interaction: { 90 | hex: false, 91 | rgba: false, 92 | hsva: false, 93 | input: true, 94 | clear: true, 95 | save: true 96 | } 97 | } 98 | } 99 | ] 100 | ]; 101 | 102 | const buttons = []; 103 | let pickr = null; 104 | 105 | for (const [theme, config] of themes) { 106 | const button = document.createElement('button'); 107 | button.innerHTML = theme; 108 | buttons.push(button); 109 | 110 | button.addEventListener('click', () => { 111 | const el = document.createElement('p'); 112 | pickrContainer.appendChild(el); 113 | 114 | // Delete previous instance 115 | if (pickr) { 116 | pickr.destroyAndRemove(); 117 | } 118 | 119 | // Apply active class 120 | for (const btn of buttons) { 121 | btn.classList[btn === button ? 'add' : 'remove']('active'); 122 | } 123 | 124 | // Create fresh instance 125 | pickr = new Pickr(Object.assign({ 126 | el, theme, 127 | default: '#42445a' 128 | }, config)); 129 | 130 | // Set events 131 | pickr.on('init', instance => { 132 | console.log('Event: "init"', instance); 133 | }).on('hide', instance => { 134 | console.log('Event: "hide"', instance); 135 | }).on('show', (color, instance) => { 136 | console.log('Event: "show"', color, instance); 137 | }).on('save', (color, instance) => { 138 | console.log('Event: "save"', color, instance); 139 | }).on('clear', instance => { 140 | console.log('Event: "clear"', instance); 141 | }).on('change', (color, source, instance) => { 142 | console.log('Event: "change"', color, source, instance); 143 | }).on('changestop', (source, instance) => { 144 | console.log('Event: "changestop"', source, instance); 145 | }).on('cancel', instance => { 146 | console.log('cancel', pickr.getColor().toRGBA().toString(0)); 147 | }).on('swatchselect', (color, instance) => { 148 | console.log('Event: "swatchselect"', color, instance); 149 | }); 150 | }); 151 | 152 | themeContainer.appendChild(button); 153 | } 154 | 155 | buttons[0].click(); 156 | --------------------------------------------------------------------------------