├── .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 | [](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 |
3 |
4 |
5 |
6 | Flat, Simple, Hackable Color-Picker.
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
19 |
22 |
23 |
26 |
28 |
31 |
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 | ||||
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 |
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 | ` `
627 | );
628 |
629 | // Append element and save swatch data
630 | _root.swatches.appendChild(el);
631 | _swatchColors.push({el, color});
632 |
633 | // Bind event
634 | this._eventBindings.push(
635 | _.on(el, 'click', () => {
636 | this.setHSVA(...color.toHSVA(), true);
637 | this._emit('swatchselect', color);
638 | this._emit('change', color, 'swatch', this);
639 | })
640 | );
641 |
642 | return true;
643 | }
644 |
645 | return false;
646 | }
647 |
648 | /**
649 | * Removes a swatch color by it's index
650 | * @param index
651 | * @returns {boolean}
652 | */
653 | removeSwatch(index) {
654 | const swatchColor = this._swatchColors[index];
655 |
656 | // Check swatch data
657 | if (swatchColor) {
658 | const {el} = swatchColor;
659 |
660 | // Remove HTML child and swatch data
661 | this._root.swatches.removeChild(el);
662 | this._swatchColors.splice(index, 1);
663 | return true;
664 | }
665 |
666 | return false;
667 | }
668 |
669 | applyColor(silent = false) {
670 | const {preview, button} = this._root;
671 |
672 | // Change preview and current color
673 | const cssRGBaString = this._color.toRGBA().toString(0);
674 | preview.lastColor.style.setProperty('--pcr-color', cssRGBaString);
675 |
676 | // Change only the button color if it isn't customized
677 | if (!this.options.useAsButton) {
678 | button.style.setProperty('--pcr-color', cssRGBaString);
679 | }
680 |
681 | // User changed the color so remove the clear clas
682 | button.classList.remove('clear');
683 |
684 | // Save last color
685 | this._lastColor = this._color.clone();
686 |
687 | // Fire listener
688 | if (!this._initializingActive && !silent) {
689 | this._emit('save', this._color);
690 | }
691 |
692 | return this;
693 | }
694 |
695 | /**
696 | * Destroy's all functionalitys
697 | */
698 | destroy() {
699 |
700 | // Cancel setup-frame if set
701 | cancelAnimationFrame(this._setupAnimationFrame);
702 |
703 | // Unbind events
704 | this._eventBindings.forEach(args => _.off(...args));
705 |
706 | // Destroy sub-components
707 | Object.keys(this._components)
708 | .forEach(key => this._components[key].destroy());
709 | }
710 |
711 | /**
712 | * Destroy's all functionalitys and removes
713 | * the pickr element.
714 | */
715 | destroyAndRemove() {
716 | this.destroy();
717 | const {root, app} = this._root;
718 |
719 | // Remove element
720 | if (root.parentElement) {
721 | root.parentElement.removeChild(root);
722 | }
723 |
724 | // Remove .pcr-app
725 | app.parentElement.removeChild(app);
726 |
727 | // There are references to various DOM elements stored in the pickr instance
728 | // This cleans all of them to avoid detached DOMs
729 | Object.keys(this)
730 | .forEach(key => this[key] = null);
731 | }
732 |
733 | /**
734 | * Hides the color-picker ui.
735 | */
736 | hide() {
737 | if (this.isOpen()) {
738 | this._root.app.classList.remove('visible');
739 | this._emit('hide');
740 | return true;
741 | }
742 |
743 | return false;
744 | }
745 |
746 | /**
747 | * Shows the color-picker ui.
748 | */
749 | show() {
750 | if (!this.options.disabled && !this.isOpen()) {
751 | this._root.app.classList.add('visible');
752 | this._rePositioningPicker();
753 | this._emit('show', this._color);
754 | return this;
755 | }
756 |
757 | return false;
758 | }
759 |
760 | /**
761 | * @return {boolean} If the color picker is currently open
762 | */
763 | isOpen() {
764 | return this._root.app.classList.contains('visible');
765 | }
766 |
767 | /**
768 | * Set a specific color.
769 | * @param h Hue
770 | * @param s Saturation
771 | * @param v Value
772 | * @param a Alpha channel (0 - 1)
773 | * @param silent If the button should not change the color
774 | * @return boolean if the color has been accepted
775 | */
776 | setHSVA(h = 360, s = 0, v = 0, a = 1, silent = false) {
777 |
778 | // Deactivate color calculation
779 | const recalc = this._recalc; // Save state
780 | this._recalc = false;
781 |
782 | // Validate input
783 | if (h < 0 || h > 360 || s < 0 || s > 100 || v < 0 || v > 100 || a < 0 || a > 1) {
784 | return false;
785 | }
786 |
787 | // Override current color and re-active color calculation
788 | this._color = HSVaColor(h, s, v, a);
789 |
790 | // Update slider and palette
791 | const {hue, opacity, palette} = this._components;
792 | hue.update((h / 360));
793 | opacity.update(a);
794 | palette.update(s / 100, 1 - (v / 100));
795 |
796 | // Check if call is silent
797 | if (!silent) {
798 | this.applyColor();
799 | }
800 |
801 | // Update output if recalculation is enabled
802 | if (recalc) {
803 | this._updateOutput();
804 | }
805 |
806 | // Restore old state
807 | this._recalc = recalc;
808 | return true;
809 | }
810 |
811 | /**
812 | * Tries to parse a string which represents a color.
813 | * Examples: #fff
814 | * rgb 10 10 200
815 | * hsva 10 20 5 0.5
816 | * @param string
817 | * @param silent
818 | */
819 | setColor(string, silent = false) {
820 |
821 | // Check if null
822 | if (string === null) {
823 | this._clearColor(silent);
824 | return true;
825 | }
826 |
827 | const {values, type} = this._parseLocalColor(string);
828 |
829 | // Check if color is ok
830 | if (values) {
831 |
832 | // Change selected color format
833 | const utype = type.toUpperCase();
834 | const {options} = this._root.interaction;
835 | const target = options.find(el => el.getAttribute('data-type') === utype);
836 |
837 | // Auto select only if not hidden
838 | if (target && !target.hidden) {
839 | for (const el of options) {
840 | el.classList[el === target ? 'add' : 'remove']('active');
841 | }
842 | }
843 |
844 | // Update color (fires 'save' event if silent is 'false')
845 | if (!this.setHSVA(...values, silent)) {
846 | return false;
847 | }
848 |
849 | // Update representation (fires 'change' event)
850 | return this.setColorRepresentation(utype);
851 | }
852 |
853 | return false;
854 | }
855 |
856 | /**
857 | * Changes the color _representation.
858 | * Allowed values are HEX, RGB, HSV, HSL and CMYK
859 | * @param type
860 | * @returns {boolean} if the selected type was valid.
861 | */
862 | setColorRepresentation(type) {
863 |
864 | // Force uppercase to allow a case-sensitiv comparison
865 | type = type.toUpperCase();
866 |
867 | // Find button with given type and trigger click event
868 | return !!this._root.interaction.options
869 | .find(v => v.getAttribute('data-type').startsWith(type) && !v.click());
870 | }
871 |
872 | /**
873 | * Returns the current color representaion. See setColorRepresentation
874 | * @returns {*}
875 | */
876 | getColorRepresentation() {
877 | return this._representation;
878 | }
879 |
880 | /**
881 | * @returns HSVaColor Current HSVaColor object.
882 | */
883 | getColor() {
884 | return this._color;
885 | }
886 |
887 | /**
888 | * Returns the currently selected color.
889 | * @returns {{a, toHSVA, toHEXA, s, v, h, clone, toCMYK, toHSLA, toRGBA}}
890 | */
891 | getSelectedColor() {
892 | return this._lastColor;
893 | }
894 |
895 | /**
896 | * @returns The root HTMLElement with all his components.
897 | */
898 | getRoot() {
899 | return this._root;
900 | }
901 |
902 | /**
903 | * Disable pickr
904 | */
905 | disable() {
906 | this.hide();
907 | this.options.disabled = true;
908 | this._root.button.classList.add('disabled');
909 | return this;
910 | }
911 |
912 | /**
913 | * Enable pickr
914 | */
915 | enable() {
916 | this.options.disabled = false;
917 | this._root.button.classList.remove('disabled');
918 | return this;
919 | }
920 | }
921 |
--------------------------------------------------------------------------------
/src/js/template.js:
--------------------------------------------------------------------------------
1 | import * as _ from './utils/utils';
2 |
3 | export default instance => {
4 |
5 | const {
6 | components,
7 | useAsButton,
8 | inline,
9 | appClass,
10 | theme,
11 | lockOpacity
12 | } = instance.options;
13 |
14 | // Utils
15 | const hidden = con => con ? '' : 'style="display:none" hidden';
16 | const t = str => instance._t(str);
17 |
18 | const root = _.createFromTemplate(`
19 |
20 |
21 | ${useAsButton ? '' : '
'}
22 |
23 |
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 |
--------------------------------------------------------------------------------