PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / assets / js / libraries / coloris.js

coloris.js in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at assets/js/libraries/coloris.js

1,211 lines 38.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /*!
2 * Copyright (c) 2021 Momo Bassit.
3 * Licensed under the MIT License (MIT)
4 * https://github.com/mdbassit/Coloris
5 */
6
7 (function (window, document, Math, undefined) {
8 var ctx = document.createElement('canvas').getContext('2d');
9 var currentColor = { r: 0, g: 0, b: 0, h: 0, s: 0, v: 0, a: 1 };
10 var container, picker, colorArea, colorAreaDims, colorMarker, colorPreview, colorValue, clearButton, closeButton,
11 hueSlider, hueMarker, alphaSlider, alphaMarker, currentEl, currentFormat, oldColor, keyboardNav;
12
13 // Default settings
14 var settings = {
15 el: '[data-coloris]',
16 parent: 'body',
17 theme: 'default',
18 themeMode: 'light',
19 rtl: false,
20 wrap: true,
21 margin: 2,
22 format: 'hex',
23 formatToggle: false,
24 swatches: [],
25 swatchesOnly: false,
26 alpha: true,
27 forceAlpha: false,
28 focusInput: true,
29 selectInput: false,
30 inline: false,
31 defaultColor: '#000000',
32 clearButton: false,
33 clearLabel: 'Clear',
34 closeButton: false,
35 closeLabel: 'Close',
36 onChange: function onChange() {return undefined;},
37 a11y: {
38 open: 'Open color picker',
39 close: 'Close color picker',
40 clear: 'Clear the selected color',
41 marker: 'Saturation: {s}. Brightness: {v}.',
42 hueSlider: 'Hue slider',
43 alphaSlider: 'Opacity slider',
44 input: 'Color value field',
45 format: 'Color format',
46 swatch: 'Color swatch',
47 instruction: 'Saturation and brightness selector. Use up, down, left and right arrow keys to select.' } };
48
49
50
51 // Virtual instances cache
52 var instances = {};
53 var currentInstanceId = '';
54 var defaultInstance = {};
55 var hasInstance = false;
56
57 /**
58 * Configure the color picker.
59 * @param {object} options Configuration options.
60 */
61 function configure(options) {
62 if (typeof options !== 'object') {
63 return;
64 }
65
66 for (var key in options) {
67 switch (key) {
68 case 'el':
69 bindFields(options.el);
70 if (options.wrap !== false) {
71 wrapFields(options.el);
72 }
73 break;
74 case 'parent':
75 container = document.querySelector(options.parent);
76 if (container) {
77 container.appendChild(picker);
78 settings.parent = options.parent;
79
80 // document.body is special
81 if (container === document.body) {
82 container = undefined;
83 }
84 }
85 break;
86 case 'themeMode':
87 settings.themeMode = options.themeMode;
88 if (options.themeMode === 'auto' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
89 settings.themeMode = 'dark';
90 }
91 // The lack of a break statement is intentional
92 case 'theme':
93 if (options.theme) {
94 settings.theme = options.theme;
95 }
96
97 // Set the theme and color scheme
98 picker.className = "clr-picker clr-" + settings.theme + " clr-" + settings.themeMode;
99
100 // Update the color picker's position if inline mode is in use
101 if (settings.inline) {
102 updatePickerPosition();
103 }
104 break;
105 case 'rtl':
106 settings.rtl = !!options.rtl;
107 document.querySelectorAll('.clr-field').forEach(function (field) {return field.classList.toggle('clr-rtl', settings.rtl);});
108 break;
109 case 'margin':
110 options.margin *= 1;
111 settings.margin = !isNaN(options.margin) ? options.margin : settings.margin;
112 break;
113 case 'wrap':
114 if (options.el && options.wrap) {
115 wrapFields(options.el);
116 }
117 break;
118 case 'formatToggle':
119 settings.formatToggle = !!options.formatToggle;
120 getEl('clr-format').style.display = settings.formatToggle ? 'block' : 'none';
121 if (settings.formatToggle) {
122 settings.format = 'auto';
123 }
124 break;
125 case 'swatches':
126 if (Array.isArray(options.swatches)) {(function () {
127 var swatches = [];
128
129 options.swatches.forEach(function (swatch, i) {
130 swatches.push("<button type=\"button\" id=\"clr-swatch-" + i + "\" aria-labelledby=\"clr-swatch-label clr-swatch-" + i + "\" style=\"color: " + swatch + ";\">" + swatch + "</button>");
131 });
132
133 getEl('clr-swatches').innerHTML = swatches.length ? "<div>" + swatches.join('') + "</div>" : '';
134 settings.swatches = options.swatches.slice();})();
135 }
136 break;
137 case 'swatchesOnly':
138 settings.swatchesOnly = !!options.swatchesOnly;
139 picker.setAttribute('data-minimal', settings.swatchesOnly);
140 break;
141 case 'alpha':
142 settings.alpha = !!options.alpha;
143 picker.setAttribute('data-alpha', settings.alpha);
144 break;
145 case 'inline':
146 settings.inline = !!options.inline;
147 picker.setAttribute('data-inline', settings.inline);
148
149 if (settings.inline) {
150 var defaultColor = options.defaultColor || settings.defaultColor;
151
152 currentFormat = getColorFormatFromStr(defaultColor);
153 updatePickerPosition();
154 setColorFromStr(defaultColor);
155 }
156 break;
157 case 'clearButton':
158 // Backward compatibility
159 if (typeof options.clearButton === 'object') {
160 if (options.clearButton.label) {
161 settings.clearLabel = options.clearButton.label;
162 clearButton.innerHTML = settings.clearLabel;
163 }
164
165 options.clearButton = options.clearButton.show;
166 }
167
168 settings.clearButton = !!options.clearButton;
169 clearButton.style.display = settings.clearButton ? 'block' : 'none';
170 break;
171 case 'clearLabel':
172 settings.clearLabel = options.clearLabel;
173 clearButton.innerHTML = settings.clearLabel;
174 break;
175 case 'closeButton':
176 settings.closeButton = !!options.closeButton;
177
178 if (settings.closeButton) {
179 picker.insertBefore(closeButton, colorPreview);
180 } else {
181 colorPreview.appendChild(closeButton);
182 }
183
184 break;
185 case 'closeLabel':
186 settings.closeLabel = options.closeLabel;
187 closeButton.innerHTML = settings.closeLabel;
188 break;
189 case 'a11y':
190 var labels = options.a11y;
191 var update = false;
192
193 if (typeof labels === 'object') {
194 for (var label in labels) {
195 if (labels[label] && settings.a11y[label]) {
196 settings.a11y[label] = labels[label];
197 update = true;
198 }
199 }
200 }
201
202 if (update) {
203 var openLabel = getEl('clr-open-label');
204 var swatchLabel = getEl('clr-swatch-label');
205
206 openLabel.innerHTML = settings.a11y.open;
207 swatchLabel.innerHTML = settings.a11y.swatch;
208 closeButton.setAttribute('aria-label', settings.a11y.close);
209 clearButton.setAttribute('aria-label', settings.a11y.clear);
210 hueSlider.setAttribute('aria-label', settings.a11y.hueSlider);
211 alphaSlider.setAttribute('aria-label', settings.a11y.alphaSlider);
212 colorValue.setAttribute('aria-label', settings.a11y.input);
213 colorArea.setAttribute('aria-label', settings.a11y.instruction);
214 }
215 break;
216 default:
217 settings[key] = options[key];}
218
219 }
220 }
221
222 /**
223 * Add or update a virtual instance.
224 * @param {String} selector The CSS selector of the elements to which the instance is attached.
225 * @param {Object} options Per-instance options to apply.
226 */
227 function setVirtualInstance(selector, options) {
228 if (typeof selector === 'string' && typeof options === 'object') {
229 instances[selector] = options;
230 hasInstance = true;
231 }
232 }
233
234 /**
235 * Remove a virtual instance.
236 * @param {String} selector The CSS selector of the elements to which the instance is attached.
237 */
238 function removeVirtualInstance(selector) {
239 delete instances[selector];
240
241 if (Object.keys(instances).length === 0) {
242 hasInstance = false;
243
244 if (selector === currentInstanceId) {
245 resetVirtualInstance();
246 }
247 }
248 }
249
250 /**
251 * Attach a virtual instance to an element if it matches a selector.
252 * @param {Object} element Target element that will receive a virtual instance if applicable.
253 */
254 function attachVirtualInstance(element) {
255 if (hasInstance) {
256 // These options can only be set globally, not per instance
257 var unsupportedOptions = ['el', 'wrap', 'rtl', 'inline', 'defaultColor', 'a11y'];var _loop = function _loop(
258
259 selector) {
260 var options = instances[selector];
261
262 // If the element matches an instance's CSS selector
263 if (element.matches(selector)) {
264 currentInstanceId = selector;
265 defaultInstance = {};
266
267 // Delete unsupported options
268 unsupportedOptions.forEach(function (option) {return delete options[option];});
269
270 // Back up the default options so we can restore them later
271 for (var option in options) {
272 defaultInstance[option] = Array.isArray(settings[option]) ? settings[option].slice() : settings[option];
273 }
274
275 // Set the instance's options
276 configure(options);
277 return "break";
278 }};for (var selector in instances) {var _ret = _loop(selector);if (_ret === "break") break;
279 }
280 }
281 }
282
283 /**
284 * Revert any per-instance options that were previously applied.
285 */
286 function resetVirtualInstance() {
287 if (Object.keys(defaultInstance).length > 0) {
288 configure(defaultInstance);
289 currentInstanceId = '';
290 defaultInstance = {};
291 }
292 }
293
294 /**
295 * Bind the color picker to input fields that match the selector.
296 * @param {string} selector One or more selectors pointing to input fields.
297 */
298 function bindFields(selector) {
299 // Show the color picker on click on the input fields that match the selector
300 addListener(document, 'click', selector, function (event) {
301 // Skip if inline mode is in use
302 if (settings.inline) {
303 return;
304 }
305
306 // Apply any per-instance options first
307 attachVirtualInstance(event.target);
308
309 currentEl = event.target;
310 oldColor = currentEl.value;
311 currentFormat = getColorFormatFromStr(oldColor);
312 picker.classList.add('clr-open');
313
314 updatePickerPosition();
315 setColorFromStr(oldColor);
316
317 if (settings.focusInput || settings.selectInput) {
318 colorValue.focus({ preventScroll: true });
319 colorValue.setSelectionRange(currentEl.selectionStart, currentEl.selectionEnd);
320 }
321
322 if (settings.selectInput) {
323 colorValue.select();
324 }
325
326 // Always focus the first element when using keyboard navigation
327 if (keyboardNav || settings.swatchesOnly) {
328 getFocusableElements().shift().focus();
329 }
330
331 // Trigger an "open" event
332 currentEl.dispatchEvent(new Event('open', { bubbles: true }));
333 });
334
335 // Update the color preview of the input fields that match the selector
336 addListener(document, 'input', selector, function (event) {
337 var parent = event.target.parentNode;
338
339 // Only update the preview if the field has been previously wrapped
340 if (parent.classList.contains('clr-field')) {
341 parent.style.color = event.target.value;
342 }
343 });
344 }
345
346 /**
347 * Update the color picker's position and the color gradient's offset
348 */
349 function updatePickerPosition() {
350 var parent = container;
351 var scrollY = window.scrollY;
352 var pickerWidth = picker.offsetWidth;
353 var pickerHeight = picker.offsetHeight;
354 var reposition = { left: false, top: false };
355 var parentStyle, parentMarginTop, parentBorderTop;
356 var offset = { x: 0, y: 0 };
357
358 if (parent) {
359 parentStyle = window.getComputedStyle(parent);
360 parentMarginTop = parseFloat(parentStyle.marginTop);
361 parentBorderTop = parseFloat(parentStyle.borderTopWidth);
362
363 offset = parent.getBoundingClientRect();
364 offset.y += parentBorderTop + scrollY;
365 }
366
367 if (!settings.inline) {
368 var coords = currentEl.getBoundingClientRect();
369 var left = coords.x;
370 var top = scrollY + coords.y + coords.height + settings.margin;
371
372 // If the color picker is inside a custom container
373 // set the position relative to it
374 if (parent) {
375 left -= offset.x;
376 top -= offset.y;
377
378 if (left + pickerWidth > parent.clientWidth) {
379 left += coords.width - pickerWidth;
380 reposition.left = true;
381 }
382
383 if (top + pickerHeight > parent.clientHeight - parentMarginTop) {
384 if (pickerHeight + settings.margin <= coords.top - (offset.y - scrollY)) {
385 top -= coords.height + pickerHeight + settings.margin * 2;
386 reposition.top = true;
387 }
388 }
389
390 top += parent.scrollTop;
391
392 // Otherwise set the position relative to the whole document
393 } else {
394 if (left + pickerWidth > document.documentElement.clientWidth) {
395 left += coords.width - pickerWidth;
396 reposition.left = true;
397 }
398
399 if (top + pickerHeight - scrollY > document.documentElement.clientHeight) {
400 if (pickerHeight + settings.margin <= coords.top) {
401 top = scrollY + coords.y - pickerHeight - settings.margin;
402 reposition.top = true;
403 }
404 }
405 }
406
407 picker.classList.toggle('clr-left', reposition.left);
408 picker.classList.toggle('clr-top', reposition.top);
409 picker.style.left = left + "px";
410 picker.style.top = top + "px";
411 offset.x += picker.offsetLeft;
412 offset.y += picker.offsetTop;
413 }
414
415 colorAreaDims = {
416 width: colorArea.offsetWidth,
417 height: colorArea.offsetHeight,
418 x: colorArea.offsetLeft + offset.x,
419 y: colorArea.offsetTop + offset.y };
420
421 }
422
423 /**
424 * Wrap the linked input fields in a div that adds a color preview.
425 * @param {string} selector One or more selectors pointing to input fields.
426 */
427 function wrapFields(selector) {
428 document.querySelectorAll(selector).forEach(function (field) {
429 var parentNode = field.parentNode;
430
431 if (!parentNode.classList.contains('clr-field')) {
432 var wrapper = document.createElement('div');
433 var classes = 'clr-field';
434
435 if (settings.rtl || field.classList.contains('clr-rtl')) {
436 classes += ' clr-rtl';
437 }
438
439 wrapper.innerHTML = "<button type=\"button\" aria-labelledby=\"clr-open-label\"></button>";
440 parentNode.insertBefore(wrapper, field);
441 wrapper.setAttribute('class', classes);
442 wrapper.style.color = field.value;
443 wrapper.appendChild(field);
444 }
445 });
446 }
447
448 /**
449 * Close the color picker.
450 * @param {boolean} [revert] If true, revert the color to the original value.
451 */
452 function closePicker(revert) {
453 if (currentEl && !settings.inline) {
454 var prevEl = currentEl;
455
456 // Revert the color to the original value if needed
457 if (revert) {
458 // This will prevent the "change" event on the colorValue input to execute its handler
459 currentEl = undefined;
460
461 if (oldColor !== prevEl.value) {
462 prevEl.value = oldColor;
463
464 // Trigger an "input" event to force update the thumbnail next to the input field
465 prevEl.dispatchEvent(new Event('input', { bubbles: true }));
466 }
467 }
468
469 // Trigger a "change" event if needed
470 setTimeout(function () {// Add this to the end of the event loop
471 if (oldColor !== prevEl.value) {
472 prevEl.dispatchEvent(new Event('change', { bubbles: true }));
473 }
474 });
475
476 // Hide the picker dialog
477 picker.classList.remove('clr-open');
478
479 // Reset any previously set per-instance options
480 if (hasInstance) {
481 resetVirtualInstance();
482 }
483
484 // Trigger a "close" event
485 prevEl.dispatchEvent(new Event('close', { bubbles: true }));
486
487 if (settings.focusInput) {
488 prevEl.focus({ preventScroll: true });
489 }
490
491 // This essentially marks the picker as closed
492 currentEl = undefined;
493 }
494 }
495
496 /**
497 * Set the active color from a string.
498 * @param {string} str String representing a color.
499 */
500 function setColorFromStr(str) {
501 var rgba = strToRGBA(str);
502 var hsva = RGBAtoHSVA(rgba);
503
504 updateMarkerA11yLabel(hsva.s, hsva.v);
505 updateColor(rgba, hsva);
506
507 // Update the UI
508 hueSlider.value = hsva.h;
509 picker.style.color = "hsl(" + hsva.h + ", 100%, 50%)";
510 hueMarker.style.left = hsva.h / 360 * 100 + "%";
511
512 colorMarker.style.left = colorAreaDims.width * hsva.s / 100 + "px";
513 colorMarker.style.top = colorAreaDims.height - colorAreaDims.height * hsva.v / 100 + "px";
514
515 alphaSlider.value = hsva.a * 100;
516 alphaMarker.style.left = hsva.a * 100 + "%";
517 }
518
519 /**
520 * Guess the color format from a string.
521 * @param {string} str String representing a color.
522 * @return {string} The color format.
523 */
524 function getColorFormatFromStr(str) {
525 var format = str.substring(0, 3).toLowerCase();
526
527 if (format === 'rgb' || format === 'hsl') {
528 return format;
529 }
530
531 return 'hex';
532 }
533
534 /**
535 * Copy the active color to the linked input field.
536 * @param {number} [color] Color value to override the active color.
537 */
538 function pickColor(color) {
539 color = color !== undefined ? color : colorValue.value;
540
541 if (currentEl) {
542 currentEl.value = color;
543 currentEl.dispatchEvent(new Event('input', { bubbles: true }));
544 }
545
546 if (settings.onChange) {
547 settings.onChange.call(window, color, currentEl);
548 }
549
550 document.dispatchEvent(new CustomEvent('coloris:pick', { detail: { color: color, currentEl: currentEl } }));
551 }
552
553 /**
554 * Set the active color based on a specific point in the color gradient.
555 * @param {number} x Left position.
556 * @param {number} y Top position.
557 */
558 function setColorAtPosition(x, y) {
559 var hsva = {
560 h: hueSlider.value * 1,
561 s: x / colorAreaDims.width * 100,
562 v: 100 - y / colorAreaDims.height * 100,
563 a: alphaSlider.value / 100 };
564
565 var rgba = HSVAtoRGBA(hsva);
566
567 updateMarkerA11yLabel(hsva.s, hsva.v);
568 updateColor(rgba, hsva);
569 pickColor();
570 }
571
572 /**
573 * Update the color marker's accessibility label.
574 * @param {number} saturation
575 * @param {number} value
576 */
577 function updateMarkerA11yLabel(saturation, value) {
578 var label = settings.a11y.marker;
579
580 saturation = saturation.toFixed(1) * 1;
581 value = value.toFixed(1) * 1;
582 label = label.replace('{s}', saturation);
583 label = label.replace('{v}', value);
584 colorMarker.setAttribute('aria-label', label);
585 }
586
587 //
588 /**
589 * Get the pageX and pageY positions of the pointer.
590 * @param {object} event The MouseEvent or TouchEvent object.
591 * @return {object} The pageX and pageY positions.
592 */
593 function getPointerPosition(event) {
594 return {
595 pageX: event.changedTouches ? event.changedTouches[0].pageX : event.pageX,
596 pageY: event.changedTouches ? event.changedTouches[0].pageY : event.pageY };
597
598 }
599
600 /**
601 * Move the color marker when dragged.
602 * @param {object} event The MouseEvent object.
603 */
604 function moveMarker(event) {
605 var pointer = getPointerPosition(event);
606 var x = pointer.pageX - colorAreaDims.x;
607 var y = pointer.pageY - colorAreaDims.y;
608
609 if (container) {
610 y += container.scrollTop;
611 }
612
613 setMarkerPosition(x, y);
614
615 // Prevent scrolling while dragging the marker
616 event.preventDefault();
617 event.stopPropagation();
618 }
619
620 /**
621 * Move the color marker when the arrow keys are pressed.
622 * @param {number} offsetX The horizontal amount to move.
623 * @param {number} offsetY The vertical amount to move.
624 */
625 function moveMarkerOnKeydown(offsetX, offsetY) {
626 var x = colorMarker.style.left.replace('px', '') * 1 + offsetX;
627 var y = colorMarker.style.top.replace('px', '') * 1 + offsetY;
628
629 setMarkerPosition(x, y);
630 }
631
632 /**
633 * Set the color marker's position.
634 * @param {number} x Left position.
635 * @param {number} y Top position.
636 */
637 function setMarkerPosition(x, y) {
638 // Make sure the marker doesn't go out of bounds
639 x = x < 0 ? 0 : x > colorAreaDims.width ? colorAreaDims.width : x;
640 y = y < 0 ? 0 : y > colorAreaDims.height ? colorAreaDims.height : y;
641
642 // Set the position
643 colorMarker.style.left = x + "px";
644 colorMarker.style.top = y + "px";
645
646 // Update the color
647 setColorAtPosition(x, y);
648
649 // Make sure the marker is focused
650 colorMarker.focus();
651 }
652
653 /**
654 * Update the color picker's input field and preview thumb.
655 * @param {Object} rgba Red, green, blue and alpha values.
656 * @param {Object} [hsva] Hue, saturation, value and alpha values.
657 */
658 function updateColor(rgba, hsva) {if (rgba === void 0) {rgba = {};}if (hsva === void 0) {hsva = {};}
659 var format = settings.format;
660
661 for (var key in rgba) {
662 currentColor[key] = rgba[key];
663 }
664
665 for (var _key in hsva) {
666 currentColor[_key] = hsva[_key];
667 }
668
669 var hex = RGBAToHex(currentColor);
670 var opaqueHex = hex.substring(0, 7);
671
672 colorMarker.style.color = opaqueHex;
673 alphaMarker.parentNode.style.color = opaqueHex;
674 alphaMarker.style.color = hex;
675 colorPreview.style.color = hex;
676
677 // Force repaint the color and alpha gradients as a workaround for a Google Chrome bug
678 colorArea.style.display = 'none';
679 colorArea.offsetHeight;
680 colorArea.style.display = '';
681 alphaMarker.nextElementSibling.style.display = 'none';
682 alphaMarker.nextElementSibling.offsetHeight;
683 alphaMarker.nextElementSibling.style.display = '';
684
685 if (format === 'mixed') {
686 format = currentColor.a === 1 ? 'hex' : 'rgb';
687 } else if (format === 'auto') {
688 format = currentFormat;
689 }
690
691 switch (format) {
692 case 'hex':
693 colorValue.value = hex;
694 break;
695 case 'rgb':
696 colorValue.value = RGBAToStr(currentColor);
697 break;
698 case 'hsl':
699 colorValue.value = HSLAToStr(HSVAtoHSLA(currentColor));
700 break;}
701
702
703 // Select the current format in the format switcher
704 document.querySelector(".clr-format [value=\"" + format + "\"]").checked = true;
705 }
706
707 /**
708 * Set the hue when its slider is moved.
709 */
710 function setHue() {
711 var hue = hueSlider.value * 1;
712 var x = colorMarker.style.left.replace('px', '') * 1;
713 var y = colorMarker.style.top.replace('px', '') * 1;
714
715 picker.style.color = "hsl(" + hue + ", 100%, 50%)";
716 hueMarker.style.left = hue / 360 * 100 + "%";
717
718 setColorAtPosition(x, y);
719 }
720
721 /**
722 * Set the alpha when its slider is moved.
723 */
724 function setAlpha() {
725 var alpha = alphaSlider.value / 100;
726
727 alphaMarker.style.left = alpha * 100 + "%";
728 updateColor({ a: alpha });
729 pickColor();
730 }
731
732 /**
733 * Convert HSVA to RGBA.
734 * @param {object} hsva Hue, saturation, value and alpha values.
735 * @return {object} Red, green, blue and alpha values.
736 */
737 function HSVAtoRGBA(hsva) {
738 var saturation = hsva.s / 100;
739 var value = hsva.v / 100;
740 var chroma = saturation * value;
741 var hueBy60 = hsva.h / 60;
742 var x = chroma * (1 - Math.abs(hueBy60 % 2 - 1));
743 var m = value - chroma;
744
745 chroma = chroma + m;
746 x = x + m;
747
748 var index = Math.floor(hueBy60) % 6;
749 var red = [chroma, x, m, m, x, chroma][index];
750 var green = [x, chroma, chroma, x, m, m][index];
751 var blue = [m, m, x, chroma, chroma, x][index];
752
753 return {
754 r: Math.round(red * 255),
755 g: Math.round(green * 255),
756 b: Math.round(blue * 255),
757 a: hsva.a };
758
759 }
760
761 /**
762 * Convert HSVA to HSLA.
763 * @param {object} hsva Hue, saturation, value and alpha values.
764 * @return {object} Hue, saturation, lightness and alpha values.
765 */
766 function HSVAtoHSLA(hsva) {
767 var value = hsva.v / 100;
768 var lightness = value * (1 - hsva.s / 100 / 2);
769 var saturation;
770
771 if (lightness > 0 && lightness < 1) {
772 saturation = Math.round((value - lightness) / Math.min(lightness, 1 - lightness) * 100);
773 }
774
775 return {
776 h: hsva.h,
777 s: saturation || 0,
778 l: Math.round(lightness * 100),
779 a: hsva.a };
780
781 }
782
783 /**
784 * Convert RGBA to HSVA.
785 * @param {object} rgba Red, green, blue and alpha values.
786 * @return {object} Hue, saturation, value and alpha values.
787 */
788 function RGBAtoHSVA(rgba) {
789 var red = rgba.r / 255;
790 var green = rgba.g / 255;
791 var blue = rgba.b / 255;
792 var xmax = Math.max(red, green, blue);
793 var xmin = Math.min(red, green, blue);
794 var chroma = xmax - xmin;
795 var value = xmax;
796 var hue = 0;
797 var saturation = 0;
798
799 if (chroma) {
800 if (xmax === red) {hue = (green - blue) / chroma;}
801 if (xmax === green) {hue = 2 + (blue - red) / chroma;}
802 if (xmax === blue) {hue = 4 + (red - green) / chroma;}
803 if (xmax) {saturation = chroma / xmax;}
804 }
805
806 hue = Math.floor(hue * 60);
807
808 return {
809 h: hue < 0 ? hue + 360 : hue,
810 s: Math.round(saturation * 100),
811 v: Math.round(value * 100),
812 a: rgba.a };
813
814 }
815
816 /**
817 * Parse a string to RGBA.
818 * @param {string} str String representing a color.
819 * @return {object} Red, green, blue and alpha values.
820 */
821 function strToRGBA(str) {
822 var regex = /^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i;
823 var match, rgba;
824
825 // Default to black for invalid color strings
826 ctx.fillStyle = '#000';
827
828 // Use canvas to convert the string to a valid color string
829 ctx.fillStyle = str;
830 match = regex.exec(ctx.fillStyle);
831
832 if (match) {
833 rgba = {
834 r: match[3] * 1,
835 g: match[4] * 1,
836 b: match[5] * 1,
837 a: match[6] * 1 };
838
839
840 // Workaround to mitigate a Chromium bug where the alpha value is rounded incorrectly
841 rgba.a = +rgba.a.toFixed(2);
842
843 } else {
844 match = ctx.fillStyle.replace('#', '').match(/.{2}/g).map(function (h) {return parseInt(h, 16);});
845 rgba = {
846 r: match[0],
847 g: match[1],
848 b: match[2],
849 a: 1 };
850
851 }
852
853 return rgba;
854 }
855
856 /**
857 * Convert RGBA to Hex.
858 * @param {object} rgba Red, green, blue and alpha values.
859 * @return {string} Hex color string.
860 */
861 function RGBAToHex(rgba) {
862 var R = rgba.r.toString(16);
863 var G = rgba.g.toString(16);
864 var B = rgba.b.toString(16);
865 var A = '';
866
867 if (rgba.r < 16) {
868 R = '0' + R;
869 }
870
871 if (rgba.g < 16) {
872 G = '0' + G;
873 }
874
875 if (rgba.b < 16) {
876 B = '0' + B;
877 }
878
879 if (settings.alpha && (rgba.a < 1 || settings.forceAlpha)) {
880 var alpha = rgba.a * 255 | 0;
881 A = alpha.toString(16);
882
883 if (alpha < 16) {
884 A = '0' + A;
885 }
886 }
887
888 return '#' + R + G + B + A;
889 }
890
891 /**
892 * Convert RGBA values to a CSS rgb/rgba string.
893 * @param {object} rgba Red, green, blue and alpha values.
894 * @return {string} CSS color string.
895 */
896 function RGBAToStr(rgba) {
897 if (!settings.alpha || rgba.a === 1 && !settings.forceAlpha) {
898 return "rgb(" + rgba.r + ", " + rgba.g + ", " + rgba.b + ")";
899 } else {
900 return "rgba(" + rgba.r + ", " + rgba.g + ", " + rgba.b + ", " + rgba.a + ")";
901 }
902 }
903
904 /**
905 * Convert HSLA values to a CSS hsl/hsla string.
906 * @param {object} hsla Hue, saturation, lightness and alpha values.
907 * @return {string} CSS color string.
908 */
909 function HSLAToStr(hsla) {
910 if (!settings.alpha || hsla.a === 1 && !settings.forceAlpha) {
911 return "hsl(" + hsla.h + ", " + hsla.s + "%, " + hsla.l + "%)";
912 } else {
913 return "hsla(" + hsla.h + ", " + hsla.s + "%, " + hsla.l + "%, " + hsla.a + ")";
914 }
915 }
916
917 /**
918 * Init the color picker.
919 */
920 function init() {
921 // Render the UI
922 container = undefined;
923 picker = document.createElement('div');
924 picker.setAttribute('id', 'clr-picker');
925 picker.className = 'clr-picker';
926 picker.innerHTML =
927 "<input id=\"clr-color-value\" name=\"clr-color-value\" class=\"clr-color\" type=\"text\" value=\"\" spellcheck=\"false\" aria-label=\"" + settings.a11y.input + "\">" + ("<div id=\"clr-color-area\" class=\"clr-gradient\" role=\"application\" aria-label=\"" +
928 settings.a11y.instruction + "\">") +
929 '<div id="clr-color-marker" class="clr-marker" tabindex="0"></div>' +
930 '</div>' +
931 '<div class="clr-hue">' + ("<input id=\"clr-hue-slider\" name=\"clr-hue-slider\" type=\"range\" min=\"0\" max=\"360\" step=\"1\" aria-label=\"" +
932 settings.a11y.hueSlider + "\">") +
933 '<div id="clr-hue-marker"></div>' +
934 '</div>' +
935 '<div class="clr-alpha">' + ("<input id=\"clr-alpha-slider\" name=\"clr-alpha-slider\" type=\"range\" min=\"0\" max=\"100\" step=\"1\" aria-label=\"" +
936 settings.a11y.alphaSlider + "\">") +
937 '<div id="clr-alpha-marker"></div>' +
938 '<span></span>' +
939 '</div>' +
940 '<div id="clr-format" class="clr-format">' +
941 '<fieldset class="clr-segmented">' + ("<legend>" +
942 settings.a11y.format + "</legend>") +
943 '<input id="clr-f1" type="radio" name="clr-format" value="hex">' +
944 '<label for="clr-f1">Hex</label>' +
945 '<input id="clr-f2" type="radio" name="clr-format" value="rgb">' +
946 '<label for="clr-f2">RGB</label>' +
947 '<input id="clr-f3" type="radio" name="clr-format" value="hsl">' +
948 '<label for="clr-f3">HSL</label>' +
949 '<span></span>' +
950 '</fieldset>' +
951 '</div>' +
952 '<div id="clr-swatches" class="clr-swatches"></div>' + ("<button type=\"button\" id=\"clr-clear\" class=\"clr-clear\" aria-label=\"" +
953 settings.a11y.clear + "\">" + settings.clearLabel + "</button>") +
954 '<div id="clr-color-preview" class="clr-preview">' + ("<button type=\"button\" id=\"clr-close\" class=\"clr-close\" aria-label=\"" +
955 settings.a11y.close + "\">" + settings.closeLabel + "</button>") +
956 '</div>' + ("<span id=\"clr-open-label\" hidden>" +
957 settings.a11y.open + "</span>") + ("<span id=\"clr-swatch-label\" hidden>" +
958 settings.a11y.swatch + "</span>");
959
960 // Append the color picker to the DOM
961 document.body.appendChild(picker);
962
963 // Reference the UI elements
964 colorArea = getEl('clr-color-area');
965 colorMarker = getEl('clr-color-marker');
966 clearButton = getEl('clr-clear');
967 closeButton = getEl('clr-close');
968 colorPreview = getEl('clr-color-preview');
969 colorValue = getEl('clr-color-value');
970 hueSlider = getEl('clr-hue-slider');
971 hueMarker = getEl('clr-hue-marker');
972 alphaSlider = getEl('clr-alpha-slider');
973 alphaMarker = getEl('clr-alpha-marker');
974
975 // Bind the picker to the default selector
976 bindFields(settings.el);
977 wrapFields(settings.el);
978
979 addListener(picker, 'mousedown', function (event) {
980 picker.classList.remove('clr-keyboard-nav');
981 event.stopPropagation();
982 });
983
984 addListener(colorArea, 'mousedown', function (event) {
985 addListener(document, 'mousemove', moveMarker);
986 });
987
988 addListener(colorArea, 'touchstart', function (event) {
989 document.addEventListener('touchmove', moveMarker, { passive: false });
990 });
991
992 addListener(colorMarker, 'mousedown', function (event) {
993 addListener(document, 'mousemove', moveMarker);
994 });
995
996 addListener(colorMarker, 'touchstart', function (event) {
997 document.addEventListener('touchmove', moveMarker, { passive: false });
998 });
999
1000 addListener(colorValue, 'change', function (event) {
1001 var value = colorValue.value;
1002
1003 if (currentEl || settings.inline) {
1004 var color = value === '' ? value : setColorFromStr(value);
1005 pickColor(color);
1006 }
1007 });
1008
1009 addListener(clearButton, 'click', function (event) {
1010 pickColor('');
1011 closePicker();
1012 });
1013
1014 addListener(closeButton, 'click', function (event) {
1015 pickColor();
1016 closePicker();
1017 });
1018
1019 addListener(getEl('clr-format'), 'click', '.clr-format input', function (event) {
1020 currentFormat = event.target.value;
1021 updateColor();
1022 pickColor();
1023 });
1024
1025 addListener(picker, 'click', '.clr-swatches button', function (event) {
1026 setColorFromStr(event.target.textContent);
1027 pickColor();
1028
1029 if (settings.swatchesOnly) {
1030 closePicker();
1031 }
1032 });
1033
1034 addListener(document, 'mouseup', function (event) {
1035 document.removeEventListener('mousemove', moveMarker);
1036 });
1037
1038 addListener(document, 'touchend', function (event) {
1039 document.removeEventListener('touchmove', moveMarker);
1040 });
1041
1042 addListener(document, 'mousedown', function (event) {
1043 keyboardNav = false;
1044 picker.classList.remove('clr-keyboard-nav');
1045 closePicker();
1046 });
1047
1048 addListener(document, 'keydown', function (event) {
1049 var key = event.key;
1050 var target = event.target;
1051 var shiftKey = event.shiftKey;
1052 var navKeys = ['Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
1053
1054 if (key === 'Escape') {
1055 closePicker(true);
1056
1057 // Display focus rings when using the keyboard
1058 } else if (navKeys.includes(key)) {
1059 keyboardNav = true;
1060 picker.classList.add('clr-keyboard-nav');
1061 }
1062
1063 // Trap the focus within the color picker while it's open
1064 if (key === 'Tab' && target.matches('.clr-picker *')) {
1065 var focusables = getFocusableElements();
1066 var firstFocusable = focusables.shift();
1067 var lastFocusable = focusables.pop();
1068
1069 if (shiftKey && target === firstFocusable) {
1070 lastFocusable.focus();
1071 event.preventDefault();
1072 } else if (!shiftKey && target === lastFocusable) {
1073 firstFocusable.focus();
1074 event.preventDefault();
1075 }
1076 }
1077 });
1078
1079 addListener(document, 'click', '.clr-field button', function (event) {
1080 // Reset any previously set per-instance options
1081 if (hasInstance) {
1082 resetVirtualInstance();
1083 }
1084
1085 // Open the color picker
1086 event.target.nextElementSibling.dispatchEvent(new Event('click', { bubbles: true }));
1087 });
1088
1089 addListener(colorMarker, 'keydown', function (event) {
1090 var movements = {
1091 ArrowUp: [0, -1],
1092 ArrowDown: [0, 1],
1093 ArrowLeft: [-1, 0],
1094 ArrowRight: [1, 0] };
1095
1096
1097 if (Object.keys(movements).includes(event.key)) {
1098 moveMarkerOnKeydown.apply(void 0, movements[event.key]);
1099 event.preventDefault();
1100 }
1101 });
1102
1103 addListener(colorArea, 'click', moveMarker);
1104 addListener(hueSlider, 'input', setHue);
1105 addListener(alphaSlider, 'input', setAlpha);
1106 }
1107
1108 /**
1109 * Return a list of focusable elements within the color picker.
1110 * @return {array} The list of focusable DOM elemnts.
1111 */
1112 function getFocusableElements() {
1113 var controls = Array.from(picker.querySelectorAll('input, button'));
1114 var focusables = controls.filter(function (node) {return !!node.offsetWidth;});
1115
1116 return focusables;
1117 }
1118
1119 /**
1120 * Shortcut for getElementById to optimize the minified JS.
1121 * @param {string} id The element id.
1122 * @return {object} The DOM element with the provided id.
1123 */
1124 function getEl(id) {
1125 return document.getElementById(id);
1126 }
1127
1128 /**
1129 * Shortcut for addEventListener to optimize the minified JS.
1130 * @param {object} context The context to which the listener is attached.
1131 * @param {string} type Event type.
1132 * @param {(string|function)} selector Event target if delegation is used, event handler if not.
1133 * @param {function} [fn] Event handler if delegation is used.
1134 */
1135 function addListener(context, type, selector, fn) {
1136 var matches = Element.prototype.matches || Element.prototype.msMatchesSelector;
1137
1138 // Delegate event to the target of the selector
1139 if (typeof selector === 'string') {
1140 context.addEventListener(type, function (event) {
1141 if (matches.call(event.target, selector)) {
1142 fn.call(event.target, event);
1143 }
1144 });
1145
1146 // If the selector is not a string then it's a function
1147 // in which case we need a regular event listener
1148 } else {
1149 fn = selector;
1150 context.addEventListener(type, fn);
1151 }
1152 }
1153
1154 /**
1155 * Call a function only when the DOM is ready.
1156 * @param {function} fn The function to call.
1157 * @param {array} [args] Arguments to pass to the function.
1158 */
1159 function DOMReady(fn, args) {
1160 args = args !== undefined ? args : [];
1161
1162 if (document.readyState !== 'loading') {
1163 fn.apply(void 0, args);
1164 } else {
1165 document.addEventListener('DOMContentLoaded', function () {
1166 fn.apply(void 0, args);
1167 });
1168 }
1169 }
1170
1171 // Polyfill for Nodelist.forEach
1172 if (NodeList !== undefined && NodeList.prototype && !NodeList.prototype.forEach) {
1173 NodeList.prototype.forEach = Array.prototype.forEach;
1174 }
1175
1176 // Expose the color picker to the global scope
1177 window.Coloris = function () {
1178 var methods = {
1179 set: configure,
1180 wrap: wrapFields,
1181 close: closePicker,
1182 setInstance: setVirtualInstance,
1183 removeInstance: removeVirtualInstance,
1184 updatePosition: updatePickerPosition };
1185
1186
1187 function Coloris(options) {
1188 DOMReady(function () {
1189 if (options) {
1190 if (typeof options === 'string') {
1191 bindFields(options);
1192 } else {
1193 configure(options);
1194 }
1195 }
1196 });
1197 }var _loop2 = function _loop2(
1198
1199 key) {
1200 Coloris[key] = function () {for (var _len = arguments.length, args = new Array(_len), _key2 = 0; _key2 < _len; _key2++) {args[_key2] = arguments[_key2];}
1201 DOMReady(methods[key], args);
1202 };};for (var key in methods) {_loop2(key);
1203 }
1204
1205 return Coloris;
1206 }();
1207
1208 // Init the color picker when the DOM is ready
1209 DOMReady(init);
1210
1211 })(window, document, Math);