@thednp/color-picker 0.0.1-alpha1 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +63 -26
  3. package/dist/css/color-picker.css +504 -337
  4. package/dist/css/color-picker.min.css +2 -0
  5. package/dist/css/color-picker.rtl.css +529 -0
  6. package/dist/css/color-picker.rtl.min.css +2 -0
  7. package/dist/js/color-picker-element-esm.js +3851 -2
  8. package/dist/js/color-picker-element-esm.min.js +2 -0
  9. package/dist/js/color-picker-element.js +2086 -1278
  10. package/dist/js/color-picker-element.min.js +2 -2
  11. package/dist/js/color-picker-esm.js +3742 -0
  12. package/dist/js/color-picker-esm.min.js +2 -0
  13. package/dist/js/color-picker.js +2030 -1286
  14. package/dist/js/color-picker.min.js +2 -2
  15. package/package.json +18 -9
  16. package/src/js/color-palette.js +71 -0
  17. package/src/js/color-picker-element.js +62 -16
  18. package/src/js/color-picker.js +734 -618
  19. package/src/js/color.js +621 -358
  20. package/src/js/index.js +0 -9
  21. package/src/js/util/colorNames.js +2 -152
  22. package/src/js/util/colorPickerLabels.js +22 -0
  23. package/src/js/util/getColorControls.js +103 -0
  24. package/src/js/util/getColorForm.js +26 -19
  25. package/src/js/util/getColorMenu.js +88 -0
  26. package/src/js/util/isValidJSON.js +13 -0
  27. package/src/js/util/nonColors.js +5 -0
  28. package/src/js/util/roundPart.js +9 -0
  29. package/src/js/util/setCSSProperties.js +12 -0
  30. package/src/js/util/tabindex.js +3 -0
  31. package/src/js/util/templates.js +1 -0
  32. package/src/scss/color-picker.rtl.scss +23 -0
  33. package/src/scss/color-picker.scss +449 -0
  34. package/types/cp.d.ts +263 -162
  35. package/types/index.d.ts +9 -2
  36. package/types/source/source.ts +2 -1
  37. package/types/source/types.d.ts +28 -5
  38. package/dist/js/color-picker.esm.js +0 -2998
  39. package/dist/js/color-picker.esm.min.js +0 -2
  40. package/src/js/util/getColorControl.js +0 -49
  41. package/src/js/util/init.js +0 -14
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * ColorPicker v0.0.1alpha1 (http://thednp.github.io/color-picker)
2
+ * ColorPicker v0.0.1 (http://thednp.github.io/color-picker)
3
3
  * Copyright 2022 © thednp
4
4
  * Licensed under MIT (https://github.com/thednp/color-picker/blob/master/LICENSE)
5
5
  */
@@ -9,37 +9,6 @@
9
9
  (global = global || self, global.ColorPicker = factory());
10
10
  }(this, (function () { 'use strict';
11
11
 
12
- /**
13
- * Returns the `document` or the `#document` element.
14
- * @see https://github.com/floating-ui/floating-ui
15
- * @param {(Node | HTMLElement | Element | globalThis)=} node
16
- * @returns {Document}
17
- */
18
- function getDocument(node) {
19
- if (node instanceof HTMLElement) return node.ownerDocument;
20
- if (node instanceof Window) return node.document;
21
- return window.document;
22
- }
23
-
24
- /**
25
- * A global array of possible `ParentNode`.
26
- */
27
- const parentNodes = [Document, Element, HTMLElement];
28
-
29
- /**
30
- * A shortcut for `(document|Element).querySelectorAll`.
31
- *
32
- * @param {string} selector the input selector
33
- * @param {(HTMLElement | Element | Document | Node)=} parent optional node to look into
34
- * @return {NodeListOf<HTMLElement | Element>} the query result
35
- */
36
- function querySelectorAll(selector, parent) {
37
- const lookUp = parent && parentNodes
38
- .some((x) => parent instanceof x) ? parent : getDocument();
39
- // @ts-ignore -- `ShadowRoot` is also a node
40
- return lookUp.querySelectorAll(selector);
41
- }
42
-
43
12
  /** @type {Record<string, any>} */
44
13
  const EventRegistry = {};
45
14
 
@@ -136,6 +105,12 @@
136
105
  }
137
106
  };
138
107
 
108
+ /**
109
+ * A global namespace for aria-description.
110
+ * @type {string}
111
+ */
112
+ const ariaDescription = 'aria-description';
113
+
139
114
  /**
140
115
  * A global namespace for aria-selected.
141
116
  * @type {string}
@@ -148,6 +123,24 @@
148
123
  */
149
124
  const ariaExpanded = 'aria-expanded';
150
125
 
126
+ /**
127
+ * A global namespace for aria-valuetext.
128
+ * @type {string}
129
+ */
130
+ const ariaValueText = 'aria-valuetext';
131
+
132
+ /**
133
+ * A global namespace for aria-valuenow.
134
+ * @type {string}
135
+ */
136
+ const ariaValueNow = 'aria-valuenow';
137
+
138
+ /**
139
+ * A global namespace for aria-haspopup.
140
+ * @type {string}
141
+ */
142
+ const ariaHasPopup = 'aria-haspopup';
143
+
151
144
  /**
152
145
  * A global namespace for aria-hidden.
153
146
  * @type {string}
@@ -202,6 +195,90 @@
202
195
  */
203
196
  const keyEscape = 'Escape';
204
197
 
198
+ /**
199
+ * A global namespace for `focusin` event.
200
+ * @type {string}
201
+ */
202
+ const focusinEvent = 'focusin';
203
+
204
+ /**
205
+ * A global namespace for `click` event.
206
+ * @type {string}
207
+ */
208
+ const mouseclickEvent = 'click';
209
+
210
+ /**
211
+ * A global namespace for `keydown` event.
212
+ * @type {string}
213
+ */
214
+ const keydownEvent = 'keydown';
215
+
216
+ /**
217
+ * A global namespace for `change` event.
218
+ * @type {string}
219
+ */
220
+ const changeEvent = 'change';
221
+
222
+ /**
223
+ * A global namespace for `touchstart` event.
224
+ * @type {string}
225
+ */
226
+ const touchstartEvent = 'touchstart';
227
+
228
+ /**
229
+ * A global namespace for `touchmove` event.
230
+ * @type {string}
231
+ */
232
+ const touchmoveEvent = 'touchmove';
233
+
234
+ /**
235
+ * A global namespace for `touchend` event.
236
+ * @type {string}
237
+ */
238
+ const touchendEvent = 'touchend';
239
+
240
+ /**
241
+ * A global namespace for `mousedown` event.
242
+ * @type {string}
243
+ */
244
+ const mousedownEvent = 'mousedown';
245
+
246
+ /**
247
+ * A global namespace for `mousemove` event.
248
+ * @type {string}
249
+ */
250
+ const mousemoveEvent = 'mousemove';
251
+
252
+ /**
253
+ * A global namespace for `mouseup` event.
254
+ * @type {string}
255
+ */
256
+ const mouseupEvent = 'mouseup';
257
+
258
+ /**
259
+ * A global namespace for `scroll` event.
260
+ * @type {string}
261
+ */
262
+ const scrollEvent = 'scroll';
263
+
264
+ /**
265
+ * A global namespace for `keyup` event.
266
+ * @type {string}
267
+ */
268
+ const keyupEvent = 'keyup';
269
+
270
+ /**
271
+ * A global namespace for `resize` event.
272
+ * @type {string}
273
+ */
274
+ const resizeEvent = 'resize';
275
+
276
+ /**
277
+ * A global namespace for `focusout` event.
278
+ * @type {string}
279
+ */
280
+ const focusoutEvent = 'focusout';
281
+
205
282
  // @ts-ignore
206
283
  const { userAgentData: uaDATA } = navigator;
207
284
 
@@ -233,7 +310,70 @@
233
310
  */
234
311
  const isMobile = isMobileCheck;
235
312
 
236
- let elementUID = 1;
313
+ /**
314
+ * Returns the `document` or the `#document` element.
315
+ * @see https://github.com/floating-ui/floating-ui
316
+ * @param {(Node | HTMLElement | Element | globalThis)=} node
317
+ * @returns {Document}
318
+ */
319
+ function getDocument(node) {
320
+ if (node instanceof HTMLElement) return node.ownerDocument;
321
+ if (node instanceof Window) return node.document;
322
+ return window.document;
323
+ }
324
+
325
+ /**
326
+ * Returns the `document.documentElement` or the `<html>` element.
327
+ *
328
+ * @param {(Node | HTMLElement | Element | globalThis)=} node
329
+ * @returns {HTMLElement | HTMLHtmlElement}
330
+ */
331
+ function getDocumentElement(node) {
332
+ return getDocument(node).documentElement;
333
+ }
334
+
335
+ /**
336
+ * Returns the `Window` object of a target node.
337
+ * @see https://github.com/floating-ui/floating-ui
338
+ *
339
+ * @param {(Node | HTMLElement | Element | Window)=} node target node
340
+ * @returns {globalThis}
341
+ */
342
+ function getWindow(node) {
343
+ if (node == null) {
344
+ return window;
345
+ }
346
+
347
+ if (!(node instanceof Window)) {
348
+ const { ownerDocument } = node;
349
+ return ownerDocument ? ownerDocument.defaultView || window : window;
350
+ }
351
+
352
+ // @ts-ignore
353
+ return node;
354
+ }
355
+
356
+ /**
357
+ * Shortcut for `window.getComputedStyle(element).propertyName`
358
+ * static method.
359
+ *
360
+ * * If `element` parameter is not an `HTMLElement`, `getComputedStyle`
361
+ * throws a `ReferenceError`.
362
+ *
363
+ * @param {HTMLElement | Element} element target
364
+ * @param {string} property the css property
365
+ * @return {string} the css property value
366
+ */
367
+ function getElementStyle(element, property) {
368
+ const computedStyle = getComputedStyle(element);
369
+
370
+ // @ts-ignore -- must use camelcase strings,
371
+ // or non-camelcase strings with `getPropertyValue`
372
+ return property in computedStyle ? computedStyle[property] : '';
373
+ }
374
+
375
+ let elementUID = 0;
376
+ let elementMapUID = 0;
237
377
  const elementIDMap = new Map();
238
378
 
239
379
  /**
@@ -244,27 +384,25 @@
244
384
  * @returns {number} an existing or new unique ID
245
385
  */
246
386
  function getUID(element, key) {
247
- elementUID += 1;
248
- let elMap = elementIDMap.get(element);
249
- let result = elementUID;
250
-
251
- if (key && key.length) {
252
- if (elMap) {
253
- const elMapId = elMap.get(key);
254
- if (!Number.isNaN(elMapId)) {
255
- result = elMapId;
256
- } else {
257
- elMap.set(key, result);
258
- }
259
- } else {
260
- elementIDMap.set(element, new Map());
261
- elMap = elementIDMap.get(element);
262
- elMap.set(key, result);
387
+ let result = key ? elementUID : elementMapUID;
388
+
389
+ if (key) {
390
+ const elID = getUID(element);
391
+ const elMap = elementIDMap.get(elID) || new Map();
392
+ if (!elementIDMap.has(elID)) {
393
+ elementIDMap.set(elID, elMap);
263
394
  }
264
- } else if (!Number.isNaN(elMap)) {
265
- result = elMap;
395
+ if (!elMap.has(key)) {
396
+ elMap.set(key, result);
397
+ elementUID += 1;
398
+ } else result = elMap.get(key);
266
399
  } else {
267
- elementIDMap.set(element, result);
400
+ const elkey = element.id || element;
401
+
402
+ if (!elementIDMap.has(elkey)) {
403
+ elementIDMap.set(elkey, result);
404
+ elementMapUID += 1;
405
+ } else result = elementIDMap.get(elkey);
268
406
  }
269
407
  return result;
270
408
  }
@@ -303,6 +441,41 @@
303
441
  };
304
442
  }
305
443
 
444
+ /**
445
+ * A global namespace for 'transitionDuration' string.
446
+ * @type {string}
447
+ */
448
+ const transitionDuration = 'transitionDuration';
449
+
450
+ /**
451
+ * A global namespace for `transitionProperty` string for modern browsers.
452
+ *
453
+ * @type {string}
454
+ */
455
+ const transitionProperty = 'transitionProperty';
456
+
457
+ /**
458
+ * Utility to get the computed `transitionDuration`
459
+ * from Element in miliseconds.
460
+ *
461
+ * @param {HTMLElement | Element} element target
462
+ * @return {number} the value in miliseconds
463
+ */
464
+ function getElementTransitionDuration(element) {
465
+ const propertyValue = getElementStyle(element, transitionProperty);
466
+ const durationValue = getElementStyle(element, transitionDuration);
467
+ const durationScale = durationValue.includes('ms') ? 1 : 1000;
468
+ const duration = propertyValue && propertyValue !== 'none'
469
+ ? parseFloat(durationValue) * durationScale : 0;
470
+
471
+ return !Number.isNaN(duration) ? duration : 0;
472
+ }
473
+
474
+ /**
475
+ * A global array of possible `ParentNode`.
476
+ */
477
+ const parentNodes = [Document, Element, HTMLElement];
478
+
306
479
  /**
307
480
  * A global array with `Element` | `HTMLElement`.
308
481
  */
@@ -343,6 +516,20 @@
343
516
  || closest(element.getRootNode().host, selector)) : null;
344
517
  }
345
518
 
519
+ /**
520
+ * Shortcut for `HTMLElement.getElementsByClassName` method. Some `Node` elements
521
+ * like `ShadowRoot` do not support `getElementsByClassName`.
522
+ *
523
+ * @param {string} selector the class name
524
+ * @param {(HTMLElement | Element | Document)=} parent optional Element to look into
525
+ * @return {HTMLCollectionOf<HTMLElement | Element>} the 'HTMLCollection'
526
+ */
527
+ function getElementsByClassName(selector, parent) {
528
+ const lookUp = parent && parentNodes.some((x) => parent instanceof x)
529
+ ? parent : getDocument();
530
+ return lookUp.getElementsByClassName(selector);
531
+ }
532
+
346
533
  /**
347
534
  * Shortcut for `Object.assign()` static method.
348
535
  * @param {Record<string, any>} obj a target object
@@ -480,6 +667,132 @@
480
667
  */
481
668
  const getInstance = (target, component) => Data.get(target, component);
482
669
 
670
+ /**
671
+ * Shortcut for `Object.keys()` static method.
672
+ * @param {Record<string, any>} obj a target object
673
+ * @returns {string[]}
674
+ */
675
+ const ObjectKeys = (obj) => Object.keys(obj);
676
+
677
+ /**
678
+ * Shortcut for multiple uses of `HTMLElement.style.propertyName` method.
679
+ * @param {HTMLElement | Element} element target element
680
+ * @param {Partial<CSSStyleDeclaration>} styles attribute value
681
+ */
682
+ // @ts-ignore
683
+ const setElementStyle = (element, styles) => ObjectAssign(element.style, styles);
684
+
685
+ /**
686
+ * Shortcut for `HTMLElement.getAttribute()` method.
687
+ * @param {HTMLElement | Element} element target element
688
+ * @param {string} attribute attribute name
689
+ * @returns {string?} attribute value
690
+ */
691
+ const getAttribute = (element, attribute) => element.getAttribute(attribute);
692
+
693
+ /**
694
+ * The raw value or a given component option.
695
+ *
696
+ * @typedef {string | HTMLElement | Function | number | boolean | null} niceValue
697
+ */
698
+
699
+ /**
700
+ * Utility to normalize component options
701
+ *
702
+ * @param {any} value the input value
703
+ * @return {niceValue} the normalized value
704
+ */
705
+ function normalizeValue(value) {
706
+ if (value === 'true') { // boolean
707
+ return true;
708
+ }
709
+
710
+ if (value === 'false') { // boolean
711
+ return false;
712
+ }
713
+
714
+ if (!Number.isNaN(+value)) { // number
715
+ return +value;
716
+ }
717
+
718
+ if (value === '' || value === 'null') { // null
719
+ return null;
720
+ }
721
+
722
+ // string / function / HTMLElement / object
723
+ return value;
724
+ }
725
+
726
+ /**
727
+ * Shortcut for `String.toLowerCase()`.
728
+ *
729
+ * @param {string} source input string
730
+ * @returns {string} lowercase output string
731
+ */
732
+ const toLowerCase = (source) => source.toLowerCase();
733
+
734
+ /**
735
+ * Utility to normalize component options.
736
+ *
737
+ * @param {HTMLElement | Element} element target
738
+ * @param {Record<string, any>} defaultOps component default options
739
+ * @param {Record<string, any>} inputOps component instance options
740
+ * @param {string=} ns component namespace
741
+ * @return {Record<string, any>} normalized component options object
742
+ */
743
+ function normalizeOptions(element, defaultOps, inputOps, ns) {
744
+ // @ts-ignore -- our targets are always `HTMLElement`
745
+ const data = { ...element.dataset };
746
+ /** @type {Record<string, any>} */
747
+ const normalOps = {};
748
+ /** @type {Record<string, any>} */
749
+ const dataOps = {};
750
+ const title = 'title';
751
+
752
+ ObjectKeys(data).forEach((k) => {
753
+ const key = ns && k.includes(ns)
754
+ ? k.replace(ns, '').replace(/[A-Z]/, (match) => toLowerCase(match))
755
+ : k;
756
+
757
+ dataOps[key] = normalizeValue(data[k]);
758
+ });
759
+
760
+ ObjectKeys(inputOps).forEach((k) => {
761
+ inputOps[k] = normalizeValue(inputOps[k]);
762
+ });
763
+
764
+ ObjectKeys(defaultOps).forEach((k) => {
765
+ if (k in inputOps) {
766
+ normalOps[k] = inputOps[k];
767
+ } else if (k in dataOps) {
768
+ normalOps[k] = dataOps[k];
769
+ } else {
770
+ normalOps[k] = k === title
771
+ ? getAttribute(element, title)
772
+ : defaultOps[k];
773
+ }
774
+ });
775
+
776
+ return normalOps;
777
+ }
778
+
779
+ /**
780
+ * Utility to force re-paint of an `HTMLElement` target.
781
+ *
782
+ * @param {HTMLElement | Element} element is the target
783
+ * @return {number} the `Element.offsetHeight` value
784
+ */
785
+ // @ts-ignore
786
+ const reflow = (element) => element.offsetHeight;
787
+
788
+ /**
789
+ * Utility to focus an `HTMLElement` target.
790
+ *
791
+ * @param {HTMLElement | Element} element is the target
792
+ */
793
+ // @ts-ignore -- `Element`s resulted from querySelector can focus too
794
+ const focus = (element) => element.focus();
795
+
483
796
  /**
484
797
  * Check class in `HTMLElement.classList`.
485
798
  *
@@ -513,14 +826,6 @@
513
826
  element.classList.remove(classNAME);
514
827
  }
515
828
 
516
- /**
517
- * Shortcut for `HTMLElement.hasAttribute()` method.
518
- * @param {HTMLElement | Element} element target element
519
- * @param {string} attribute attribute name
520
- * @returns {boolean} the query result
521
- */
522
- const hasAttribute = (element, attribute) => element.hasAttribute(attribute);
523
-
524
829
  /**
525
830
  * Shortcut for `HTMLElement.setAttribute()` method.
526
831
  * @param {HTMLElement | Element} element target element
@@ -530,14 +835,6 @@
530
835
  */
531
836
  const setAttribute = (element, attribute, value) => element.setAttribute(attribute, value);
532
837
 
533
- /**
534
- * Shortcut for `HTMLElement.getAttribute()` method.
535
- * @param {HTMLElement | Element} element target element
536
- * @param {string} attribute attribute name
537
- * @returns {string?} attribute value
538
- */
539
- const getAttribute = (element, attribute) => element.getAttribute(attribute);
540
-
541
838
  /**
542
839
  * Shortcut for `HTMLElement.removeAttribute()` method.
543
840
  * @param {HTMLElement | Element} element target element
@@ -546,6 +843,38 @@
546
843
  */
547
844
  const removeAttribute = (element, attribute) => element.removeAttribute(attribute);
548
845
 
846
+ /** @type {Record<string, string>} */
847
+ const colorPickerLabels = {
848
+ pickerLabel: 'Colour Picker',
849
+ appearanceLabel: 'Colour Appearance',
850
+ valueLabel: 'Colour Value',
851
+ toggleLabel: 'Select Colour',
852
+ presetsLabel: 'Colour Presets',
853
+ defaultsLabel: 'Colour Defaults',
854
+ formatLabel: 'Format',
855
+ alphaLabel: 'Alpha',
856
+ hexLabel: 'Hexadecimal',
857
+ hueLabel: 'Hue',
858
+ whitenessLabel: 'Whiteness',
859
+ blacknessLabel: 'Blackness',
860
+ saturationLabel: 'Saturation',
861
+ lightnessLabel: 'Lightness',
862
+ redLabel: 'Red',
863
+ greenLabel: 'Green',
864
+ blueLabel: 'Blue',
865
+ };
866
+
867
+ /**
868
+ * A list of 17 color names used for WAI-ARIA compliance.
869
+ * @type {string[]}
870
+ */
871
+ const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
872
+
873
+ /**
874
+ * A list of explicit default non-color values.
875
+ */
876
+ const nonColors = ['transparent', 'currentColor', 'inherit', 'revert', 'initial'];
877
+
549
878
  /**
550
879
  * Shortcut for `String.toUpperCase()`.
551
880
  *
@@ -557,12 +886,13 @@
557
886
  const vHidden = 'v-hidden';
558
887
 
559
888
  /**
560
- * Returns the color form.
889
+ * Returns the color form for `ColorPicker`.
890
+ *
561
891
  * @param {CP.ColorPicker} self the `ColorPicker` instance
562
- * @returns {HTMLElement | Element}
892
+ * @returns {HTMLElement | Element} a new `<div>` element with color component `<input>`
563
893
  */
564
894
  function getColorForm(self) {
565
- const { format, id } = self;
895
+ const { format, id, componentLabels } = self;
566
896
  const colorForm = createElement({
567
897
  tagName: 'div',
568
898
  className: `color-form ${format}`,
@@ -571,85 +901,174 @@
571
901
  let components = ['hex'];
572
902
  if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
573
903
  else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
904
+ else if (format === 'hwb') components = ['hue', 'whiteness', 'blackness', 'alpha'];
574
905
 
575
906
  components.forEach((c) => {
576
907
  const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
577
908
  const cID = `color_${format}_${c}_${id}`;
909
+ const formatLabel = componentLabels[`${c}Label`];
578
910
  const cInputLabel = createElement({ tagName: 'label' });
579
911
  setAttribute(cInputLabel, 'for', cID);
580
912
  cInputLabel.append(
581
913
  createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
582
- createElement({ tagName: 'span', className: vHidden, innerText: `${c}` }),
914
+ createElement({ tagName: 'span', className: vHidden, innerText: formatLabel }),
583
915
  );
584
916
  const cInput = createElement({
585
917
  tagName: 'input',
586
- id: cID, // name: cID,
918
+ id: cID,
919
+ // name: cID, - prevent saving the value to a form
587
920
  type: format === 'hex' ? 'text' : 'number',
588
- value: c === 'alpha' ? '1' : '0',
921
+ value: c === 'alpha' ? '100' : '0',
589
922
  className: `color-input ${c}`,
590
- autocomplete: 'off',
591
- spellcheck: 'false',
592
923
  });
593
- if (format !== 'hex') {
594
- // alpha
595
- let max = '1';
596
- let step = '0.01';
597
- if (c !== 'alpha') {
598
- if (format === 'rgb') { max = '255'; step = '1'; } else if (c === 'hue') { max = '360'; step = '1'; } else { max = '100'; step = '1'; }
924
+ setAttribute(cInput, 'autocomplete', 'off');
925
+ setAttribute(cInput, 'spellcheck', 'false');
926
+
927
+ // alpha
928
+ let max = '100';
929
+ let step = '1';
930
+ if (c !== 'alpha') {
931
+ if (format === 'rgb') {
932
+ max = '255'; step = '1';
933
+ } else if (c === 'hue') {
934
+ max = '360'; step = '1';
599
935
  }
600
- ObjectAssign(cInput, {
601
- min: '0',
602
- max,
603
- step,
604
- });
605
936
  }
937
+ ObjectAssign(cInput, {
938
+ min: '0',
939
+ max,
940
+ step,
941
+ });
606
942
  colorForm.append(cInputLabel, cInput);
607
943
  });
608
- return colorForm;
944
+ return colorForm;
945
+ }
946
+
947
+ /**
948
+ * A global namespace for aria-label.
949
+ * @type {string}
950
+ */
951
+ const ariaLabel = 'aria-label';
952
+
953
+ /**
954
+ * A global namespace for aria-valuemin.
955
+ * @type {string}
956
+ */
957
+ const ariaValueMin = 'aria-valuemin';
958
+
959
+ /**
960
+ * A global namespace for aria-valuemax.
961
+ * @type {string}
962
+ */
963
+ const ariaValueMax = 'aria-valuemax';
964
+
965
+ const tabIndex = 'tabindex';
966
+
967
+ /**
968
+ * Returns all color controls for `ColorPicker`.
969
+ *
970
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
971
+ * @returns {HTMLElement | Element} color controls
972
+ */
973
+ function getColorControls(self) {
974
+ const { format, componentLabels } = self;
975
+ const {
976
+ hueLabel, alphaLabel, lightnessLabel, saturationLabel,
977
+ whitenessLabel, blacknessLabel,
978
+ } = componentLabels;
979
+
980
+ const max1 = format === 'hsl' ? 360 : 100;
981
+ const max2 = format === 'hsl' ? 100 : 360;
982
+ const max3 = 100;
983
+
984
+ let ctrl1Label = format === 'hsl'
985
+ ? `${hueLabel} & ${lightnessLabel}`
986
+ : `${lightnessLabel} & ${saturationLabel}`;
987
+
988
+ ctrl1Label = format === 'hwb'
989
+ ? `${whitenessLabel} & ${blacknessLabel}`
990
+ : ctrl1Label;
991
+
992
+ const ctrl2Label = format === 'hsl'
993
+ ? `${saturationLabel}`
994
+ : `${hueLabel}`;
995
+
996
+ const colorControls = createElement({
997
+ tagName: 'div',
998
+ className: `color-controls ${format}`,
999
+ });
1000
+
1001
+ const colorPointer = 'color-pointer';
1002
+ const colorSlider = 'color-slider';
1003
+
1004
+ const controls = [
1005
+ {
1006
+ i: 1,
1007
+ c: colorPointer,
1008
+ l: ctrl1Label,
1009
+ min: 0,
1010
+ max: max1,
1011
+ },
1012
+ {
1013
+ i: 2,
1014
+ c: colorSlider,
1015
+ l: ctrl2Label,
1016
+ min: 0,
1017
+ max: max2,
1018
+ },
1019
+ {
1020
+ i: 3,
1021
+ c: colorSlider,
1022
+ l: alphaLabel,
1023
+ min: 0,
1024
+ max: max3,
1025
+ },
1026
+ ];
1027
+
1028
+ controls.forEach((template) => {
1029
+ const {
1030
+ i, c, l, min, max,
1031
+ } = template;
1032
+ const control = createElement({
1033
+ tagName: 'div',
1034
+ className: 'color-control',
1035
+ });
1036
+ setAttribute(control, 'role', 'presentation');
1037
+
1038
+ control.append(
1039
+ createElement({
1040
+ tagName: 'div',
1041
+ className: `visual-control visual-control${i}`,
1042
+ }),
1043
+ );
1044
+
1045
+ const knob = createElement({
1046
+ tagName: 'div',
1047
+ className: `${c} knob`,
1048
+ ariaLive: 'polite',
1049
+ });
1050
+
1051
+ setAttribute(knob, ariaLabel, l);
1052
+ setAttribute(knob, 'role', 'slider');
1053
+ setAttribute(knob, tabIndex, '0');
1054
+ setAttribute(knob, ariaValueMin, `${min}`);
1055
+ setAttribute(knob, ariaValueMax, `${max}`);
1056
+ control.append(knob);
1057
+ colorControls.append(control);
1058
+ });
1059
+
1060
+ return colorControls;
609
1061
  }
610
1062
 
611
1063
  /**
612
- * Returns a new color control `HTMLElement`.
613
- * @param {number} iteration
614
- * @param {number} id
615
- * @param {number} width
616
- * @param {number} height
617
- * @param {string=} labelledby
618
- * @returns {HTMLElement | Element}
1064
+ * Helps setting CSS variables to the color-menu.
1065
+ * @param {HTMLElement} element
1066
+ * @param {Record<string,any>} props
619
1067
  */
620
- function getColorControl(iteration, id, width, height, labelledby) {
621
- const labelID = `appearance${iteration}_${id}`;
622
- const knobClass = iteration === 1 ? 'color-pointer' : 'color-slider';
623
- const control = createElement({
624
- tagName: 'div',
625
- className: 'color-control',
626
- });
627
- setAttribute(control, 'role', 'presentation');
628
-
629
- control.append(
630
- createElement({
631
- id: labelID,
632
- tagName: 'label',
633
- className: `color-label ${vHidden}`,
634
- ariaLive: 'polite',
635
- }),
636
- createElement({
637
- tagName: 'canvas',
638
- className: `visual-control${iteration}`,
639
- ariaHidden: 'true',
640
- width: `${width}`,
641
- height: `${height}`,
642
- }),
643
- );
644
-
645
- const knob = createElement({
646
- tagName: 'div',
647
- className: `${knobClass} knob`,
1068
+ function setCSSProperties(element, props) {
1069
+ ObjectKeys(props).forEach((prop) => {
1070
+ element.style.setProperty(prop, props[prop]);
648
1071
  });
649
- setAttribute(knob, ariaLabelledBy, labelledby || labelID);
650
- setAttribute(knob, 'tabindex', '0');
651
- control.append(knob);
652
- return control;
653
1072
  }
654
1073
 
655
1074
  /**
@@ -663,211 +1082,49 @@
663
1082
  }
664
1083
 
665
1084
  /**
666
- * Shortcut for `window.getComputedStyle(element).propertyName`
667
- * static method.
668
- *
669
- * * If `element` parameter is not an `HTMLElement`, `getComputedStyle`
670
- * throws a `ReferenceError`.
671
- *
672
- * @param {HTMLElement | Element} element target
673
- * @param {string} property the css property
674
- * @return {string} the css property value
1085
+ * Round colour components, for all formats except HEX.
1086
+ * @param {number} v one of the colour components
1087
+ * @returns {number} the rounded number
675
1088
  */
676
- function getElementStyle(element, property) {
677
- const computedStyle = getComputedStyle(element);
678
-
679
- // @ts-ignore -- must use camelcase strings,
680
- // or non-camelcase strings with `getPropertyValue`
681
- return property in computedStyle ? computedStyle[property] : '';
1089
+ function roundPart(v) {
1090
+ const floor = Math.floor(v);
1091
+ return v - floor < 0.5 ? floor : Math.round(v);
682
1092
  }
683
1093
 
684
- /**
685
- * Shortcut for multiple uses of `HTMLElement.style.propertyName` method.
686
- * @param {HTMLElement | Element} element target element
687
- * @param {Partial<CSSStyleDeclaration>} styles attribute value
688
- */
689
- // @ts-ignore
690
- const setElementStyle = (element, styles) => { ObjectAssign(element.style, styles); };
1094
+ // Color supported formats
1095
+ const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsb', 'hwb'];
691
1096
 
692
- /**
693
- * A complete list of web safe colors.
694
- * @see https://github.com/bahamas10/css-color-names/blob/master/css-color-names.json
695
- * @type {string[]}
696
- */
697
- const colorNames = [
698
- 'aliceblue',
699
- 'antiquewhite',
700
- 'aqua',
701
- 'aquamarine',
702
- 'azure',
703
- 'beige',
704
- 'bisque',
705
- 'black',
706
- 'blanchedalmond',
707
- 'blue',
708
- 'blueviolet',
709
- 'brown',
710
- 'burlywood',
711
- 'cadetblue',
712
- 'chartreuse',
713
- 'chocolate',
714
- 'coral',
715
- 'cornflowerblue',
716
- 'cornsilk',
717
- 'crimson',
718
- 'cyan',
719
- 'darkblue',
720
- 'darkcyan',
721
- 'darkgoldenrod',
722
- 'darkgray',
723
- 'darkgreen',
724
- 'darkgrey',
725
- 'darkkhaki',
726
- 'darkmagenta',
727
- 'darkolivegreen',
728
- 'darkorange',
729
- 'darkorchid',
730
- 'darkred',
731
- 'darksalmon',
732
- 'darkseagreen',
733
- 'darkslateblue',
734
- 'darkslategray',
735
- 'darkslategrey',
736
- 'darkturquoise',
737
- 'darkviolet',
738
- 'deeppink',
739
- 'deepskyblue',
740
- 'dimgray',
741
- 'dimgrey',
742
- 'dodgerblue',
743
- 'firebrick',
744
- 'floralwhite',
745
- 'forestgreen',
746
- 'fuchsia',
747
- 'gainsboro',
748
- 'ghostwhite',
749
- 'goldenrod',
750
- 'gold',
751
- 'gray',
752
- 'green',
753
- 'greenyellow',
754
- 'grey',
755
- 'honeydew',
756
- 'hotpink',
757
- 'indianred',
758
- 'indigo',
759
- 'ivory',
760
- 'khaki',
761
- 'lavenderblush',
762
- 'lavender',
763
- 'lawngreen',
764
- 'lemonchiffon',
765
- 'lightblue',
766
- 'lightcoral',
767
- 'lightcyan',
768
- 'lightgoldenrodyellow',
769
- 'lightgray',
770
- 'lightgreen',
771
- 'lightgrey',
772
- 'lightpink',
773
- 'lightsalmon',
774
- 'lightseagreen',
775
- 'lightskyblue',
776
- 'lightslategray',
777
- 'lightslategrey',
778
- 'lightsteelblue',
779
- 'lightyellow',
780
- 'lime',
781
- 'limegreen',
782
- 'linen',
783
- 'magenta',
784
- 'maroon',
785
- 'mediumaquamarine',
786
- 'mediumblue',
787
- 'mediumorchid',
788
- 'mediumpurple',
789
- 'mediumseagreen',
790
- 'mediumslateblue',
791
- 'mediumspringgreen',
792
- 'mediumturquoise',
793
- 'mediumvioletred',
794
- 'midnightblue',
795
- 'mintcream',
796
- 'mistyrose',
797
- 'moccasin',
798
- 'navajowhite',
799
- 'navy',
800
- 'oldlace',
801
- 'olive',
802
- 'olivedrab',
803
- 'orange',
804
- 'orangered',
805
- 'orchid',
806
- 'palegoldenrod',
807
- 'palegreen',
808
- 'paleturquoise',
809
- 'palevioletred',
810
- 'papayawhip',
811
- 'peachpuff',
812
- 'peru',
813
- 'pink',
814
- 'plum',
815
- 'powderblue',
816
- 'purple',
817
- 'rebeccapurple',
818
- 'red',
819
- 'rosybrown',
820
- 'royalblue',
821
- 'saddlebrown',
822
- 'salmon',
823
- 'sandybrown',
824
- 'seagreen',
825
- 'seashell',
826
- 'sienna',
827
- 'silver',
828
- 'skyblue',
829
- 'slateblue',
830
- 'slategray',
831
- 'slategrey',
832
- 'snow',
833
- 'springgreen',
834
- 'steelblue',
835
- 'tan',
836
- 'teal',
837
- 'thistle',
838
- 'tomato',
839
- 'turquoise',
840
- 'violet',
841
- 'wheat',
842
- 'white',
843
- 'whitesmoke',
844
- 'yellow',
845
- 'yellowgreen',
846
- ];
1097
+ // Hue angles
1098
+ const ANGLES = 'deg|rad|grad|turn';
847
1099
 
848
1100
  // <http://www.w3.org/TR/css3-values/#integers>
849
1101
  const CSS_INTEGER = '[-\\+]?\\d+%?';
850
1102
 
1103
+ // Include CSS3 Module
851
1104
  // <http://www.w3.org/TR/css3-values/#number-value>
852
1105
  const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
853
1106
 
1107
+ // Include CSS4 Module Hue degrees unit
1108
+ // <https://www.w3.org/TR/css3-values/#angle-value>
1109
+ const CSS_ANGLE = `[-\\+]?\\d*\\.?\\d+(?:${ANGLES})?`;
1110
+
854
1111
  // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
855
1112
  const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
856
1113
 
1114
+ // Add angles to the mix
1115
+ const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
1116
+
857
1117
  // Actual matching.
858
1118
  // Parentheses and commas are optional, but not required.
859
1119
  // Whitespace can take the place of commas or opening paren
860
- const PERMISSIVE_MATCH3 = `[\\s|\\(]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})\\s*\\)?`;
861
- const PERMISSIVE_MATCH4 = `[\\s|\\(]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})\\s*\\)?`;
1120
+ const PERMISSIVE_MATCH = `[\\s|\\(]+(${CSS_UNIT2})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s|\\/\\s]*(${CSS_UNIT})?\\s*\\)?`;
862
1121
 
863
1122
  const matchers = {
864
- CSS_UNIT: new RegExp(CSS_UNIT),
865
- rgb: new RegExp(`rgb${PERMISSIVE_MATCH3}`),
866
- rgba: new RegExp(`rgba${PERMISSIVE_MATCH4}`),
867
- hsl: new RegExp(`hsl${PERMISSIVE_MATCH3}`),
868
- hsla: new RegExp(`hsla${PERMISSIVE_MATCH4}`),
869
- hsv: new RegExp(`hsv${PERMISSIVE_MATCH3}`),
870
- hsva: new RegExp(`hsva${PERMISSIVE_MATCH4}`),
1123
+ CSS_UNIT: new RegExp(CSS_UNIT2),
1124
+ hwb: new RegExp(`hwb${PERMISSIVE_MATCH}`),
1125
+ rgb: new RegExp(`rgb(?:a)?${PERMISSIVE_MATCH}`),
1126
+ hsl: new RegExp(`hsl(?:a)?${PERMISSIVE_MATCH}`),
1127
+ hsv: new RegExp(`hsv(?:a)?${PERMISSIVE_MATCH}`),
871
1128
  hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
872
1129
  hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
873
1130
  hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
@@ -877,27 +1134,46 @@
877
1134
  /**
878
1135
  * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
879
1136
  * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
880
- * @param {string} n
881
- * @returns {boolean}
1137
+ * @param {string} n testing number
1138
+ * @returns {boolean} the query result
882
1139
  */
883
1140
  function isOnePointZero(n) {
884
- return typeof n === 'string' && n.includes('.') && parseFloat(n) === 1;
1141
+ return `${n}`.includes('.') && parseFloat(n) === 1;
885
1142
  }
886
1143
 
887
1144
  /**
888
1145
  * Check to see if string passed in is a percentage
889
- * @param {string} n
890
- * @returns {boolean}
1146
+ * @param {string} n testing number
1147
+ * @returns {boolean} the query result
891
1148
  */
892
1149
  function isPercentage(n) {
893
- return typeof n === 'string' && n.includes('%');
1150
+ return `${n}`.includes('%');
1151
+ }
1152
+
1153
+ /**
1154
+ * Check to see if string passed in is an angle
1155
+ * @param {string} n testing string
1156
+ * @returns {boolean} the query result
1157
+ */
1158
+ function isAngle(n) {
1159
+ return ANGLES.split('|').some((a) => `${n}`.includes(a));
1160
+ }
1161
+
1162
+ /**
1163
+ * Check to see if string passed is a web safe colour.
1164
+ * @param {string} color a colour name, EG: *red*
1165
+ * @returns {boolean} the query result
1166
+ */
1167
+ function isColorName(color) {
1168
+ return !['#', ...COLOR_FORMAT].some((s) => color.includes(s))
1169
+ && !/[0-9]/.test(color);
894
1170
  }
895
1171
 
896
1172
  /**
897
1173
  * Check to see if it looks like a CSS unit
898
1174
  * (see `matchers` above for definition).
899
- * @param {string | number} color
900
- * @returns {boolean}
1175
+ * @param {string | number} color testing value
1176
+ * @returns {boolean} the query result
901
1177
  */
902
1178
  function isValidCSSUnit(color) {
903
1179
  return Boolean(matchers.CSS_UNIT.exec(String(color)));
@@ -905,22 +1181,24 @@
905
1181
 
906
1182
  /**
907
1183
  * Take input from [0, n] and return it as [0, 1]
908
- * @param {*} n
909
- * @param {number} max
910
- * @returns {number}
1184
+ * @param {*} N the input number
1185
+ * @param {number} max the number maximum value
1186
+ * @returns {number} the number in [0, 1] value range
911
1187
  */
912
- function bound01(n, max) {
913
- let N = n;
914
- if (isOnePointZero(n)) N = '100%';
1188
+ function bound01(N, max) {
1189
+ let n = N;
1190
+ if (isOnePointZero(n)) n = '100%';
915
1191
 
916
- N = max === 360 ? N : Math.min(max, Math.max(0, parseFloat(N)));
1192
+ n = max === 360 ? n : Math.min(max, Math.max(0, parseFloat(n)));
1193
+
1194
+ // Handle hue angles
1195
+ if (isAngle(N)) n = N.replace(new RegExp(ANGLES), '');
917
1196
 
918
1197
  // Automatically convert percentage into number
919
- if (isPercentage(N)) {
920
- N = parseInt(String(N * max), 10) / 100;
921
- }
1198
+ if (isPercentage(n)) n = parseInt(String(n * max), 10) / 100;
1199
+
922
1200
  // Handle floating point rounding errors
923
- if (Math.abs(N - max) < 0.000001) {
1201
+ if (Math.abs(n - max) < 0.000001) {
924
1202
  return 1;
925
1203
  }
926
1204
  // Convert into [0, 1] range if it isn't already
@@ -928,23 +1206,22 @@
928
1206
  // If n is a hue given in degrees,
929
1207
  // wrap around out-of-range values into [0, 360] range
930
1208
  // then convert into [0, 1].
931
- N = (N < 0 ? (N % max) + max : N % max) / parseFloat(String(max));
1209
+ n = (n < 0 ? (n % max) + max : n % max) / parseFloat(String(max));
932
1210
  } else {
933
1211
  // If n not a hue given in degrees
934
1212
  // Convert into [0, 1] range if it isn't already.
935
- N = (N % max) / parseFloat(String(max));
1213
+ n = (n % max) / parseFloat(String(max));
936
1214
  }
937
- return N;
1215
+ return n;
938
1216
  }
939
1217
 
940
1218
  /**
941
1219
  * Return a valid alpha value [0,1] with all invalid values being set to 1.
942
- * @param {string | number} a
943
- * @returns {number}
1220
+ * @param {string | number} a transparency value
1221
+ * @returns {number} a transparency value in the [0, 1] range
944
1222
  */
945
1223
  function boundAlpha(a) {
946
- // @ts-ignore
947
- let na = parseFloat(a);
1224
+ let na = parseFloat(`${a}`);
948
1225
 
949
1226
  if (Number.isNaN(na) || na < 0 || na > 1) {
950
1227
  na = 1;
@@ -954,12 +1231,12 @@
954
1231
  }
955
1232
 
956
1233
  /**
957
- * Force a number between 0 and 1
958
- * @param {number} val
959
- * @returns {number}
1234
+ * Force a number between 0 and 1.
1235
+ * @param {number} v the float number
1236
+ * @returns {number} - the resulting number
960
1237
  */
961
- function clamp01(val) {
962
- return Math.min(1, Math.max(0, val));
1238
+ function clamp01(v) {
1239
+ return Math.min(1, Math.max(0, v));
963
1240
  }
964
1241
 
965
1242
  /**
@@ -967,7 +1244,7 @@
967
1244
  * @param {string} name
968
1245
  * @returns {string}
969
1246
  */
970
- function getHexFromColorName(name) {
1247
+ function getRGBFromName(name) {
971
1248
  const documentHead = getDocumentHead();
972
1249
  setElementStyle(documentHead, { color: name });
973
1250
  const colorName = getElementStyle(documentHead, 'color');
@@ -976,59 +1253,53 @@
976
1253
  }
977
1254
 
978
1255
  /**
979
- * Replace a decimal with it's percentage value
980
- * @param {number | string} n
981
- * @return {string | number}
1256
+ * Converts a decimal value to hexadecimal.
1257
+ * @param {number} d the input number
1258
+ * @returns {string} - the hexadecimal value
982
1259
  */
983
- function convertToPercentage(n) {
984
- if (n <= 1) {
985
- return `${Number(n) * 100}%`;
986
- }
987
- return n;
1260
+ function convertDecimalToHex(d) {
1261
+ return roundPart(d * 255).toString(16);
988
1262
  }
989
1263
 
990
1264
  /**
991
- * Force a hex value to have 2 characters
992
- * @param {string} c
993
- * @returns {string}
1265
+ * Converts a hexadecimal value to decimal.
1266
+ * @param {string} h hexadecimal value
1267
+ * @returns {number} number in decimal format
994
1268
  */
995
- function pad2(c) {
996
- return c.length === 1 ? `0${c}` : String(c);
1269
+ function convertHexToDecimal(h) {
1270
+ return parseIntFromHex(h) / 255;
997
1271
  }
998
1272
 
999
- // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
1000
- // <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
1001
1273
  /**
1002
- * Handle bounds / percentage checking to conform to CSS color spec
1003
- * * *Assumes:* r, g, b in [0, 255] or [0, 1]
1004
- * * *Returns:* { r, g, b } in [0, 255]
1005
- * @see http://www.w3.org/TR/css3-color/
1006
- * @param {number | string} r
1007
- * @param {number | string} g
1008
- * @param {number | string} b
1009
- * @returns {CP.RGB}
1274
+ * Converts a base-16 hexadecimal value into a base-10 integer.
1275
+ * @param {string} val
1276
+ * @returns {number}
1010
1277
  */
1011
- function rgbToRgb(r, g, b) {
1012
- return {
1013
- r: bound01(r, 255) * 255,
1014
- g: bound01(g, 255) * 255,
1015
- b: bound01(b, 255) * 255,
1016
- };
1278
+ function parseIntFromHex(val) {
1279
+ return parseInt(val, 16);
1017
1280
  }
1018
1281
 
1019
1282
  /**
1020
- * Converts an RGB color value to HSL.
1021
- * *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
1022
- * *Returns:* { h, s, l } in [0,1]
1023
- * @param {number} R
1024
- * @param {number} G
1025
- * @param {number} B
1026
- * @returns {CP.HSL}
1283
+ * Force a hexadecimal value to have 2 characters.
1284
+ * @param {string} c string with [0-9A-F] ranged values
1285
+ * @returns {string} 0 => 00, a => 0a
1286
+ */
1287
+ function pad2(c) {
1288
+ return c.length === 1 ? `0${c}` : String(c);
1289
+ }
1290
+
1291
+ /**
1292
+ * Converts an RGB colour value to HSL.
1293
+ *
1294
+ * @param {number} R Red component [0, 255]
1295
+ * @param {number} G Green component [0, 255]
1296
+ * @param {number} B Blue component [0, 255]
1297
+ * @returns {CP.HSL} {h,s,l} object with [0, 1] ranged values
1027
1298
  */
1028
1299
  function rgbToHsl(R, G, B) {
1029
- const r = bound01(R, 255);
1030
- const g = bound01(G, 255);
1031
- const b = bound01(B, 255);
1300
+ const r = R / 255;
1301
+ const g = G / 255;
1302
+ const b = B / 255;
1032
1303
  const max = Math.max(r, g, b);
1033
1304
  const min = Math.min(r, g, b);
1034
1305
  let h = 0;
@@ -1058,50 +1329,95 @@
1058
1329
 
1059
1330
  /**
1060
1331
  * Returns a normalized RGB component value.
1061
- * @param {number} P
1062
- * @param {number} Q
1063
- * @param {number} T
1332
+ * @param {number} p
1333
+ * @param {number} q
1334
+ * @param {number} t
1064
1335
  * @returns {number}
1065
1336
  */
1066
- function hue2rgb(P, Q, T) {
1067
- const p = P;
1068
- const q = Q;
1069
- let t = T;
1070
- if (t < 0) {
1071
- t += 1;
1072
- }
1073
- if (t > 1) {
1074
- t -= 1;
1075
- }
1076
- if (t < 1 / 6) {
1077
- return p + (q - p) * (6 * t);
1078
- }
1079
- if (t < 1 / 2) {
1080
- return q;
1081
- }
1082
- if (t < 2 / 3) {
1083
- return p + (q - p) * (2 / 3 - t) * 6;
1084
- }
1337
+ function hueToRgb(p, q, t) {
1338
+ let T = t;
1339
+ if (T < 0) T += 1;
1340
+ if (T > 1) T -= 1;
1341
+ if (T < 1 / 6) return p + (q - p) * (6 * T);
1342
+ if (T < 1 / 2) return q;
1343
+ if (T < 2 / 3) return p + (q - p) * (2 / 3 - T) * 6;
1085
1344
  return p;
1086
1345
  }
1087
1346
 
1347
+ /**
1348
+ * Returns an HWB colour object from an RGB colour object.
1349
+ * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
1350
+ * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
1351
+ *
1352
+ * @param {number} R Red component [0, 255]
1353
+ * @param {number} G Green [0, 255]
1354
+ * @param {number} B Blue [0, 255]
1355
+ * @return {CP.HWB} {h,w,b} object with [0, 1] ranged values
1356
+ */
1357
+ function rgbToHwb(R, G, B) {
1358
+ const r = R / 255;
1359
+ const g = G / 255;
1360
+ const b = B / 255;
1361
+
1362
+ let f = 0;
1363
+ let i = 0;
1364
+ const whiteness = Math.min(r, g, b);
1365
+ const max = Math.max(r, g, b);
1366
+ const black = 1 - max;
1367
+
1368
+ if (max === whiteness) return { h: 0, w: whiteness, b: black };
1369
+ if (r === whiteness) {
1370
+ f = g - b;
1371
+ i = 3;
1372
+ } else {
1373
+ f = g === whiteness ? b - r : r - g;
1374
+ i = g === whiteness ? 5 : 1;
1375
+ }
1376
+
1377
+ const h = (i - f / (max - whiteness)) / 6;
1378
+ return {
1379
+ h: h === 1 ? 0 : h,
1380
+ w: whiteness,
1381
+ b: black,
1382
+ };
1383
+ }
1384
+
1385
+ /**
1386
+ * Returns an RGB colour object from an HWB colour.
1387
+ *
1388
+ * @param {number} H Hue Angle [0, 1]
1389
+ * @param {number} W Whiteness [0, 1]
1390
+ * @param {number} B Blackness [0, 1]
1391
+ * @return {CP.RGB} {r,g,b} object with [0, 255] ranged values
1392
+ *
1393
+ * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
1394
+ * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
1395
+ */
1396
+ function hwbToRgb(H, W, B) {
1397
+ if (W + B >= 1) {
1398
+ const gray = (W / (W + B)) * 255;
1399
+ return { r: gray, g: gray, b: gray };
1400
+ }
1401
+ let { r, g, b } = hslToRgb(H, 1, 0.5);
1402
+ [r, g, b] = [r, g, b]
1403
+ .map((v) => (v / 255) * (1 - W - B) + W)
1404
+ .map((v) => v * 255);
1405
+
1406
+ return { r, g, b };
1407
+ }
1408
+
1088
1409
  /**
1089
1410
  * Converts an HSL colour value to RGB.
1090
1411
  *
1091
- * * *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
1092
- * * *Returns:* { r, g, b } in the set [0, 255]
1093
- * @param {number | string} H
1094
- * @param {number | string} S
1095
- * @param {number | string} L
1096
- * @returns {CP.RGB}
1097
- */
1098
- function hslToRgb(H, S, L) {
1412
+ * @param {number} h Hue Angle [0, 1]
1413
+ * @param {number} s Saturation [0, 1]
1414
+ * @param {number} l Lightness Angle [0, 1]
1415
+ * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1416
+ */
1417
+ function hslToRgb(h, s, l) {
1099
1418
  let r = 0;
1100
1419
  let g = 0;
1101
1420
  let b = 0;
1102
- const h = bound01(H, 360);
1103
- const s = bound01(S, 100);
1104
- const l = bound01(L, 100);
1105
1421
 
1106
1422
  if (s === 0) {
1107
1423
  // achromatic
@@ -1111,27 +1427,27 @@
1111
1427
  } else {
1112
1428
  const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1113
1429
  const p = 2 * l - q;
1114
- r = hue2rgb(p, q, h + 1 / 3);
1115
- g = hue2rgb(p, q, h);
1116
- b = hue2rgb(p, q, h - 1 / 3);
1430
+ r = hueToRgb(p, q, h + 1 / 3);
1431
+ g = hueToRgb(p, q, h);
1432
+ b = hueToRgb(p, q, h - 1 / 3);
1117
1433
  }
1118
- return { r: r * 255, g: g * 255, b: b * 255 };
1434
+ [r, g, b] = [r, g, b].map((x) => x * 255);
1435
+
1436
+ return { r, g, b };
1119
1437
  }
1120
1438
 
1121
1439
  /**
1122
1440
  * Converts an RGB colour value to HSV.
1123
1441
  *
1124
- * *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
1125
- * *Returns:* { h, s, v } in [0,1]
1126
- * @param {number | string} R
1127
- * @param {number | string} G
1128
- * @param {number | string} B
1129
- * @returns {CP.HSV}
1442
+ * @param {number} R Red component [0, 255]
1443
+ * @param {number} G Green [0, 255]
1444
+ * @param {number} B Blue [0, 255]
1445
+ * @returns {CP.HSV} {h,s,v} object with [0, 1] ranged values
1130
1446
  */
1131
1447
  function rgbToHsv(R, G, B) {
1132
- const r = bound01(R, 255);
1133
- const g = bound01(G, 255);
1134
- const b = bound01(B, 255);
1448
+ const r = R / 255;
1449
+ const g = G / 255;
1450
+ const b = B / 255;
1135
1451
  const max = Math.max(r, g, b);
1136
1452
  const min = Math.min(r, g, b);
1137
1453
  let h = 0;
@@ -1158,19 +1474,17 @@
1158
1474
  }
1159
1475
 
1160
1476
  /**
1161
- * Converts an HSV color value to RGB.
1477
+ * Converts an HSV colour value to RGB.
1162
1478
  *
1163
- * *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
1164
- * *Returns:* { r, g, b } in the set [0, 255]
1165
- * @param {number | string} H
1166
- * @param {number | string} S
1167
- * @param {number | string} V
1168
- * @returns {CP.RGB}
1479
+ * @param {number} H Hue Angle [0, 1]
1480
+ * @param {number} S Saturation [0, 1]
1481
+ * @param {number} V Brightness Angle [0, 1]
1482
+ * @returns {CP.RGB} {r,g,b} object with [0, 1] ranged values
1169
1483
  */
1170
1484
  function hsvToRgb(H, S, V) {
1171
- const h = bound01(H, 360) * 6;
1172
- const s = bound01(S, 100);
1173
- const v = bound01(V, 100);
1485
+ const h = H * 6;
1486
+ const s = S;
1487
+ const v = V;
1174
1488
  const i = Math.floor(h);
1175
1489
  const f = h - i;
1176
1490
  const p = v * (1 - s);
@@ -1184,47 +1498,65 @@
1184
1498
  }
1185
1499
 
1186
1500
  /**
1187
- * Converts an RGB color to hex
1501
+ * Converts an RGB colour to hex
1188
1502
  *
1189
1503
  * Assumes r, g, and b are contained in the set [0, 255]
1190
1504
  * Returns a 3 or 6 character hex
1191
- * @param {number} r
1192
- * @param {number} g
1193
- * @param {number} b
1505
+ * @param {number} r Red component [0, 255]
1506
+ * @param {number} g Green [0, 255]
1507
+ * @param {number} b Blue [0, 255]
1508
+ * @param {boolean=} allow3Char
1194
1509
  * @returns {string}
1195
1510
  */
1196
- function rgbToHex(r, g, b) {
1511
+ function rgbToHex(r, g, b, allow3Char) {
1197
1512
  const hex = [
1198
- pad2(Math.round(r).toString(16)),
1199
- pad2(Math.round(g).toString(16)),
1200
- pad2(Math.round(b).toString(16)),
1513
+ pad2(roundPart(r).toString(16)),
1514
+ pad2(roundPart(g).toString(16)),
1515
+ pad2(roundPart(b).toString(16)),
1201
1516
  ];
1202
1517
 
1518
+ // Return a 3 character hex if possible
1519
+ if (allow3Char && hex[0].charAt(0) === hex[0].charAt(1)
1520
+ && hex[1].charAt(0) === hex[1].charAt(1)
1521
+ && hex[2].charAt(0) === hex[2].charAt(1)) {
1522
+ return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
1523
+ }
1524
+
1203
1525
  return hex.join('');
1204
1526
  }
1205
1527
 
1206
1528
  /**
1207
- * Converts a hex value to a decimal.
1208
- * @param {string} h
1209
- * @returns {number}
1210
- */
1211
- function convertHexToDecimal(h) {
1212
- return parseIntFromHex(h) / 255;
1213
- }
1529
+ * Converts an RGBA color plus alpha transparency to hex8.
1530
+ *
1531
+ * @param {number} r Red component [0, 255]
1532
+ * @param {number} g Green [0, 255]
1533
+ * @param {number} b Blue [0, 255]
1534
+ * @param {number} a Alpha transparency [0, 1]
1535
+ * @param {boolean=} allow4Char when *true* it will also find hex shorthand
1536
+ * @returns {string} a hexadecimal value with alpha transparency
1537
+ */
1538
+ function rgbaToHex(r, g, b, a, allow4Char) {
1539
+ const hex = [
1540
+ pad2(roundPart(r).toString(16)),
1541
+ pad2(roundPart(g).toString(16)),
1542
+ pad2(roundPart(b).toString(16)),
1543
+ pad2(convertDecimalToHex(a)),
1544
+ ];
1214
1545
 
1215
- /**
1216
- * Parse a base-16 hex value into a base-10 integer.
1217
- * @param {string} val
1218
- * @returns {number}
1219
- */
1220
- function parseIntFromHex(val) {
1221
- return parseInt(val, 16);
1546
+ // Return a 4 character hex if possible
1547
+ if (allow4Char && hex[0].charAt(0) === hex[0].charAt(1)
1548
+ && hex[1].charAt(0) === hex[1].charAt(1)
1549
+ && hex[2].charAt(0) === hex[2].charAt(1)
1550
+ && hex[3].charAt(0) === hex[3].charAt(1)) {
1551
+ return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
1552
+ }
1553
+ return hex.join('');
1222
1554
  }
1223
1555
 
1224
1556
  /**
1225
- * Returns an `{r,g,b}` color object corresponding to a given number.
1226
- * @param {number} color
1227
- * @returns {CP.RGB}
1557
+ * Returns a colour object corresponding to a given number.
1558
+ * @param {number} color input number
1559
+ * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1228
1560
  */
1229
1561
  function numberInputToObject(color) {
1230
1562
  /* eslint-disable no-bitwise */
@@ -1237,10 +1569,10 @@
1237
1569
  }
1238
1570
 
1239
1571
  /**
1240
- * Permissive string parsing. Take in a number of formats, and output an object
1241
- * based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
1242
- * @param {string} input
1243
- * @returns {Record<string, (number | string)> | false}
1572
+ * Permissive string parsing. Take in a number of formats, and output an object
1573
+ * based on detected format. Returns {r,g,b} or {h,s,l} or {h,s,v}
1574
+ * @param {string} input colour value in any format
1575
+ * @returns {Record<string, (number | string)> | false} an object matching the RegExp
1244
1576
  */
1245
1577
  function stringInputToObject(input) {
1246
1578
  let color = input.trim().toLowerCase();
@@ -1250,12 +1582,15 @@
1250
1582
  };
1251
1583
  }
1252
1584
  let named = false;
1253
- if (colorNames.includes(color)) {
1254
- color = getHexFromColorName(color);
1585
+ if (isColorName(color)) {
1586
+ color = getRGBFromName(color);
1255
1587
  named = true;
1256
- } else if (color === 'transparent') {
1588
+ } else if (nonColors.includes(color)) {
1589
+ const isTransparent = color === 'transparent';
1590
+ const rgb = isTransparent ? 0 : 255;
1591
+ const a = isTransparent ? 0 : 1;
1257
1592
  return {
1258
- r: 0, g: 0, b: 0, a: 0, format: 'name',
1593
+ r: rgb, g: rgb, b: rgb, a, format: 'rgb',
1259
1594
  };
1260
1595
  }
1261
1596
 
@@ -1264,72 +1599,68 @@
1264
1599
  // don't worry about [0,1] or [0,100] or [0,360]
1265
1600
  // Just return an object and let the conversion functions handle that.
1266
1601
  // This way the result will be the same whether Color is initialized with string or object.
1267
- let match = matchers.rgb.exec(color);
1268
- if (match) {
1269
- return { r: match[1], g: match[2], b: match[3] };
1270
- }
1271
- match = matchers.rgba.exec(color);
1272
- if (match) {
1602
+ let [, m1, m2, m3, m4] = matchers.rgb.exec(color) || [];
1603
+ if (m1 && m2 && m3/* && m4 */) {
1273
1604
  return {
1274
- r: match[1], g: match[2], b: match[3], a: match[4],
1605
+ r: m1, g: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'rgb',
1275
1606
  };
1276
1607
  }
1277
- match = matchers.hsl.exec(color);
1278
- if (match) {
1279
- return { h: match[1], s: match[2], l: match[3] };
1280
- }
1281
- match = matchers.hsla.exec(color);
1282
- if (match) {
1608
+ [, m1, m2, m3, m4] = matchers.hsl.exec(color) || [];
1609
+ if (m1 && m2 && m3/* && m4 */) {
1283
1610
  return {
1284
- h: match[1], s: match[2], l: match[3], a: match[4],
1611
+ h: m1, s: m2, l: m3, a: m4 !== undefined ? m4 : 1, format: 'hsl',
1285
1612
  };
1286
1613
  }
1287
- match = matchers.hsv.exec(color);
1288
- if (match) {
1289
- return { h: match[1], s: match[2], v: match[3] };
1614
+ [, m1, m2, m3, m4] = matchers.hsv.exec(color) || [];
1615
+ if (m1 && m2 && m3/* && m4 */) {
1616
+ return {
1617
+ h: m1, s: m2, v: m3, a: m4 !== undefined ? m4 : 1, format: 'hsv',
1618
+ };
1290
1619
  }
1291
- match = matchers.hsva.exec(color);
1292
- if (match) {
1620
+ [, m1, m2, m3, m4] = matchers.hwb.exec(color) || [];
1621
+ if (m1 && m2 && m3) {
1293
1622
  return {
1294
- h: match[1], s: match[2], v: match[3], a: match[4],
1623
+ h: m1, w: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'hwb',
1295
1624
  };
1296
1625
  }
1297
- match = matchers.hex8.exec(color);
1298
- if (match) {
1626
+ [, m1, m2, m3, m4] = matchers.hex8.exec(color) || [];
1627
+ if (m1 && m2 && m3 && m4) {
1299
1628
  return {
1300
- r: parseIntFromHex(match[1]),
1301
- g: parseIntFromHex(match[2]),
1302
- b: parseIntFromHex(match[3]),
1303
- a: convertHexToDecimal(match[4]),
1304
- format: named ? 'name' : 'hex8',
1629
+ r: parseIntFromHex(m1),
1630
+ g: parseIntFromHex(m2),
1631
+ b: parseIntFromHex(m3),
1632
+ a: convertHexToDecimal(m4),
1633
+ // format: named ? 'rgb' : 'hex8',
1634
+ format: named ? 'rgb' : 'hex',
1305
1635
  };
1306
1636
  }
1307
- match = matchers.hex6.exec(color);
1308
- if (match) {
1637
+ [, m1, m2, m3] = matchers.hex6.exec(color) || [];
1638
+ if (m1 && m2 && m3) {
1309
1639
  return {
1310
- r: parseIntFromHex(match[1]),
1311
- g: parseIntFromHex(match[2]),
1312
- b: parseIntFromHex(match[3]),
1313
- format: named ? 'name' : 'hex',
1640
+ r: parseIntFromHex(m1),
1641
+ g: parseIntFromHex(m2),
1642
+ b: parseIntFromHex(m3),
1643
+ format: named ? 'rgb' : 'hex',
1314
1644
  };
1315
1645
  }
1316
- match = matchers.hex4.exec(color);
1317
- if (match) {
1646
+ [, m1, m2, m3, m4] = matchers.hex4.exec(color) || [];
1647
+ if (m1 && m2 && m3 && m4) {
1318
1648
  return {
1319
- r: parseIntFromHex(match[1] + match[1]),
1320
- g: parseIntFromHex(match[2] + match[2]),
1321
- b: parseIntFromHex(match[3] + match[3]),
1322
- a: convertHexToDecimal(match[4] + match[4]),
1323
- format: named ? 'name' : 'hex8',
1649
+ r: parseIntFromHex(m1 + m1),
1650
+ g: parseIntFromHex(m2 + m2),
1651
+ b: parseIntFromHex(m3 + m3),
1652
+ a: convertHexToDecimal(m4 + m4),
1653
+ // format: named ? 'rgb' : 'hex8',
1654
+ format: named ? 'rgb' : 'hex',
1324
1655
  };
1325
1656
  }
1326
- match = matchers.hex3.exec(color);
1327
- if (match) {
1657
+ [, m1, m2, m3] = matchers.hex3.exec(color) || [];
1658
+ if (m1 && m2 && m3) {
1328
1659
  return {
1329
- r: parseIntFromHex(match[1] + match[1]),
1330
- g: parseIntFromHex(match[2] + match[2]),
1331
- b: parseIntFromHex(match[3] + match[3]),
1332
- format: named ? 'name' : 'hex',
1660
+ r: parseIntFromHex(m1 + m1),
1661
+ g: parseIntFromHex(m2 + m2),
1662
+ b: parseIntFromHex(m3 + m3),
1663
+ format: named ? 'rgb' : 'hex',
1333
1664
  };
1334
1665
  }
1335
1666
  return false;
@@ -1343,26 +1674,35 @@
1343
1674
  * "red"
1344
1675
  * "#f00" or "f00"
1345
1676
  * "#ff0000" or "ff0000"
1346
- * "#ff000000" or "ff000000"
1677
+ * "#ff000000" or "ff000000" // CSS4 Module
1347
1678
  * "rgb 255 0 0" or "rgb (255, 0, 0)"
1348
1679
  * "rgb 1.0 0 0" or "rgb (1, 0, 0)"
1349
- * "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
1350
- * "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
1680
+ * "rgba(255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
1681
+ * "rgba(1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
1682
+ * "rgb(255 0 0 / 10%)" or "rgb 255 0 0 0.1" // CSS4 Module
1351
1683
  * "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
1352
1684
  * "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
1685
+ * "hsl(0deg 100% 50% / 50%)" or "hsl 0 100 50 50" // CSS4 Module
1353
1686
  * "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
1687
+ * "hsva(0, 100%, 100%, 0.1)" or "hsva 0 100% 100% 0.1"
1688
+ * "hsv(0deg 100% 100% / 10%)" or "hsv 0 100 100 0.1" // CSS4 Module
1689
+ * "hwb(0deg, 100%, 100%, 100%)" or "hwb 0 100% 100% 0.1" // CSS4 Module
1354
1690
  * ```
1355
1691
  * @param {string | Record<string, any>} input
1356
1692
  * @returns {CP.ColorObject}
1357
1693
  */
1358
1694
  function inputToRGB(input) {
1359
- /** @type {CP.RGB} */
1360
1695
  let rgb = { r: 0, g: 0, b: 0 };
1361
1696
  let color = input;
1362
- let a;
1697
+ let a = 1;
1363
1698
  let s = null;
1364
1699
  let v = null;
1365
1700
  let l = null;
1701
+ let w = null;
1702
+ let b = null;
1703
+ let h = null;
1704
+ let r = null;
1705
+ let g = null;
1366
1706
  let ok = false;
1367
1707
  let format = 'hex';
1368
1708
 
@@ -1373,23 +1713,41 @@
1373
1713
  }
1374
1714
  if (typeof color === 'object') {
1375
1715
  if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
1376
- rgb = rgbToRgb(color.r, color.g, color.b);
1716
+ ({ r, g, b } = color);
1717
+ // RGB values now are all in [0, 255] range
1718
+ [r, g, b] = [r, g, b].map((n) => bound01(n, isPercentage(n) ? 100 : 255) * 255);
1719
+ rgb = { r, g, b };
1377
1720
  ok = true;
1378
- format = `${color.r}`.slice(-1) === '%' ? 'prgb' : 'rgb';
1721
+ format = 'rgb';
1379
1722
  } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
1380
- s = convertToPercentage(color.s);
1381
- v = convertToPercentage(color.v);
1382
- rgb = hsvToRgb(color.h, s, v);
1723
+ ({ h, s, v } = color);
1724
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1725
+ s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1726
+ v = typeof v === 'number' ? v : bound01(v, 100); // brightness can be `5%` or a [0, 1] value
1727
+ rgb = hsvToRgb(h, s, v);
1383
1728
  ok = true;
1384
1729
  format = 'hsv';
1385
1730
  } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
1386
- s = convertToPercentage(color.s);
1387
- l = convertToPercentage(color.l);
1388
- rgb = hslToRgb(color.h, s, l);
1731
+ ({ h, s, l } = color);
1732
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1733
+ s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1734
+ l = typeof l === 'number' ? l : bound01(l, 100); // lightness can be `5%` or a [0, 1] value
1735
+ rgb = hslToRgb(h, s, l);
1389
1736
  ok = true;
1390
1737
  format = 'hsl';
1738
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.w) && isValidCSSUnit(color.b)) {
1739
+ ({ h, w, b } = color);
1740
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1741
+ w = typeof w === 'number' ? w : bound01(w, 100); // whiteness can be `5%` or a [0, 1] value
1742
+ b = typeof b === 'number' ? b : bound01(b, 100); // blackness can be `5%` or a [0, 1] value
1743
+ rgb = hwbToRgb(h, w, b);
1744
+ ok = true;
1745
+ format = 'hwb';
1746
+ }
1747
+ if (isValidCSSUnit(color.a)) {
1748
+ a = color.a;
1749
+ a = isPercentage(`${a}`) ? bound01(a, 100) : a;
1391
1750
  }
1392
- if ('a' in color) a = color.a;
1393
1751
  }
1394
1752
 
1395
1753
  return {
@@ -1402,27 +1760,21 @@
1402
1760
  };
1403
1761
  }
1404
1762
 
1405
- /** @type {CP.ColorOptions} */
1406
- const colorPickerDefaults = {
1407
- format: 'hex',
1408
- };
1409
-
1410
1763
  /**
1764
+ * @class
1411
1765
  * Returns a new `Color` instance.
1412
1766
  * @see https://github.com/bgrins/TinyColor
1413
- * @class
1414
1767
  */
1415
1768
  class Color {
1416
1769
  /**
1417
1770
  * @constructor
1418
- * @param {CP.ColorInput} input
1419
- * @param {CP.ColorOptions=} config
1771
+ * @param {CP.ColorInput} input the given colour value
1772
+ * @param {CP.ColorFormats=} config the given format
1420
1773
  */
1421
1774
  constructor(input, config) {
1422
1775
  let color = input;
1423
- const opts = typeof config === 'object'
1424
- ? ObjectAssign(colorPickerDefaults, config)
1425
- : ObjectAssign({}, colorPickerDefaults);
1776
+ const configFormat = config && COLOR_FORMAT.includes(config)
1777
+ ? config : 'rgb';
1426
1778
 
1427
1779
  // If input is already a `Color`, return itself
1428
1780
  if (color instanceof Color) {
@@ -1435,36 +1787,23 @@
1435
1787
  r, g, b, a, ok, format,
1436
1788
  } = inputToRGB(color);
1437
1789
 
1790
+ // bind
1791
+ const self = this;
1792
+
1438
1793
  /** @type {CP.ColorInput} */
1439
- this.originalInput = color;
1794
+ self.originalInput = color;
1440
1795
  /** @type {number} */
1441
- this.r = r;
1796
+ self.r = r;
1442
1797
  /** @type {number} */
1443
- this.g = g;
1798
+ self.g = g;
1444
1799
  /** @type {number} */
1445
- this.b = b;
1800
+ self.b = b;
1446
1801
  /** @type {number} */
1447
- this.a = a;
1802
+ self.a = a;
1448
1803
  /** @type {boolean} */
1449
- this.ok = ok;
1450
- /** @type {number} */
1451
- this.roundA = Math.round(100 * this.a) / 100;
1804
+ self.ok = ok;
1452
1805
  /** @type {CP.ColorFormats} */
1453
- this.format = opts.format || format;
1454
-
1455
- // Don't let the range of [0,255] come back in [0,1].
1456
- // Potentially lose a little bit of precision here, but will fix issues where
1457
- // .5 gets interpreted as half of the total, instead of half of 1
1458
- // If it was supposed to be 128, this was already taken care of by `inputToRgb`
1459
- if (this.r < 1) {
1460
- this.r = Math.round(this.r);
1461
- }
1462
- if (this.g < 1) {
1463
- this.g = Math.round(this.g);
1464
- }
1465
- if (this.b < 1) {
1466
- this.b = Math.round(this.b);
1467
- }
1806
+ self.format = configFormat || format;
1468
1807
  }
1469
1808
 
1470
1809
  /**
@@ -1480,44 +1819,44 @@
1480
1819
  * @returns {boolean} the query result
1481
1820
  */
1482
1821
  get isDark() {
1483
- return this.brightness < 128;
1822
+ return this.brightness < 120;
1484
1823
  }
1485
1824
 
1486
1825
  /**
1487
- * Returns the perceived luminance of a color.
1826
+ * Returns the perceived luminance of a colour.
1488
1827
  * @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
1489
- * @returns {number} a number in [0-1] range
1828
+ * @returns {number} a number in the [0, 1] range
1490
1829
  */
1491
1830
  get luminance() {
1492
1831
  const { r, g, b } = this;
1493
1832
  let R = 0;
1494
1833
  let G = 0;
1495
1834
  let B = 0;
1496
- const RsRGB = r / 255;
1497
- const GsRGB = g / 255;
1498
- const BsRGB = b / 255;
1835
+ const rp = r / 255;
1836
+ const rg = g / 255;
1837
+ const rb = b / 255;
1499
1838
 
1500
- if (RsRGB <= 0.03928) {
1501
- R = RsRGB / 12.92;
1839
+ if (rp <= 0.03928) {
1840
+ R = rp / 12.92;
1502
1841
  } else {
1503
- R = ((RsRGB + 0.055) / 1.055) ** 2.4;
1842
+ R = ((rp + 0.055) / 1.055) ** 2.4;
1504
1843
  }
1505
- if (GsRGB <= 0.03928) {
1506
- G = GsRGB / 12.92;
1844
+ if (rg <= 0.03928) {
1845
+ G = rg / 12.92;
1507
1846
  } else {
1508
- G = ((GsRGB + 0.055) / 1.055) ** 2.4;
1847
+ G = ((rg + 0.055) / 1.055) ** 2.4;
1509
1848
  }
1510
- if (BsRGB <= 0.03928) {
1511
- B = BsRGB / 12.92;
1849
+ if (rb <= 0.03928) {
1850
+ B = rb / 12.92;
1512
1851
  } else {
1513
- B = ((BsRGB + 0.055) / 1.055) ** 2.4;
1852
+ B = ((rb + 0.055) / 1.055) ** 2.4;
1514
1853
  }
1515
1854
  return 0.2126 * R + 0.7152 * G + 0.0722 * B;
1516
1855
  }
1517
1856
 
1518
1857
  /**
1519
- * Returns the perceived brightness of the color.
1520
- * @returns {number} a number in [0-255] range
1858
+ * Returns the perceived brightness of the colour.
1859
+ * @returns {number} a number in the [0, 255] range
1521
1860
  */
1522
1861
  get brightness() {
1523
1862
  const { r, g, b } = this;
@@ -1525,123 +1864,287 @@
1525
1864
  }
1526
1865
 
1527
1866
  /**
1528
- * Returns the color as a RGBA object.
1529
- * @returns {CP.RGBA}
1867
+ * Returns the colour as an RGBA object.
1868
+ * @returns {CP.RGBA} an {r,g,b,a} object with [0, 255] ranged values
1530
1869
  */
1531
1870
  toRgb() {
1871
+ const {
1872
+ r, g, b, a,
1873
+ } = this;
1874
+
1532
1875
  return {
1533
- r: Math.round(this.r),
1534
- g: Math.round(this.g),
1535
- b: Math.round(this.b),
1536
- a: this.a,
1876
+ r, g, b, a: roundPart(a * 100) / 100,
1537
1877
  };
1538
1878
  }
1539
1879
 
1540
1880
  /**
1541
- * Returns the RGBA values concatenated into a string.
1542
- * @returns {string} the CSS valid color in RGB/RGBA format
1881
+ * Returns the RGBA values concatenated into a CSS3 Module string format.
1882
+ * * rgb(255,255,255)
1883
+ * * rgba(255,255,255,0.5)
1884
+ * @returns {string} the CSS valid colour in RGB/RGBA format
1543
1885
  */
1544
1886
  toRgbString() {
1545
- const r = Math.round(this.r);
1546
- const g = Math.round(this.g);
1547
- const b = Math.round(this.b);
1548
- return this.a === 1
1549
- ? `rgb(${r},${g},${b})`
1550
- : `rgba(${r},${g},${b},${this.roundA})`;
1887
+ const {
1888
+ r, g, b, a,
1889
+ } = this.toRgb();
1890
+ const [R, G, B] = [r, g, b].map(roundPart);
1891
+
1892
+ return a === 1
1893
+ ? `rgb(${R}, ${G}, ${B})`
1894
+ : `rgba(${R}, ${G}, ${B}, ${a})`;
1895
+ }
1896
+
1897
+ /**
1898
+ * Returns the RGBA values concatenated into a CSS4 Module string format.
1899
+ * * rgb(255 255 255)
1900
+ * * rgb(255 255 255 / 50%)
1901
+ * @returns {string} the CSS valid colour in CSS4 RGB format
1902
+ */
1903
+ toRgbCSS4String() {
1904
+ const {
1905
+ r, g, b, a,
1906
+ } = this.toRgb();
1907
+ const [R, G, B] = [r, g, b].map(roundPart);
1908
+ const A = a === 1 ? '' : ` / ${roundPart(a * 100)}%`;
1909
+
1910
+ return `rgb(${R} ${G} ${B}${A})`;
1911
+ }
1912
+
1913
+ /**
1914
+ * Returns the hexadecimal value of the colour. When the parameter is *true*
1915
+ * it will find a 3 characters shorthand of the decimal value.
1916
+ *
1917
+ * @param {boolean=} allow3Char when `true` returns shorthand HEX
1918
+ * @returns {string} the hexadecimal colour format
1919
+ */
1920
+ toHex(allow3Char) {
1921
+ const {
1922
+ r, g, b, a,
1923
+ } = this.toRgb();
1924
+
1925
+ return a === 1
1926
+ ? rgbToHex(r, g, b, allow3Char)
1927
+ : rgbaToHex(r, g, b, a, allow3Char);
1551
1928
  }
1552
1929
 
1553
1930
  /**
1554
- * Returns the HEX value of the color.
1555
- * @returns {string} the hexadecimal color format
1931
+ * Returns the CSS valid hexadecimal vaue of the colour. When the parameter is *true*
1932
+ * it will find a 3 characters shorthand of the value.
1933
+ *
1934
+ * @param {boolean=} allow3Char when `true` returns shorthand HEX
1935
+ * @returns {string} the CSS valid colour in hexadecimal format
1936
+ */
1937
+ toHexString(allow3Char) {
1938
+ return `#${this.toHex(allow3Char)}`;
1939
+ }
1940
+
1941
+ /**
1942
+ * Returns the HEX8 value of the colour.
1943
+ * @param {boolean=} allow4Char when `true` returns shorthand HEX
1944
+ * @returns {string} the CSS valid colour in hexadecimal format
1556
1945
  */
1557
- toHex() {
1558
- return rgbToHex(this.r, this.g, this.b);
1946
+ toHex8(allow4Char) {
1947
+ const {
1948
+ r, g, b, a,
1949
+ } = this.toRgb();
1950
+
1951
+ return rgbaToHex(r, g, b, a, allow4Char);
1559
1952
  }
1560
1953
 
1561
1954
  /**
1562
- * Returns the HEX value of the color.
1563
- * @returns {string} the CSS valid color in hexadecimal format
1955
+ * Returns the HEX8 value of the colour.
1956
+ * @param {boolean=} allow4Char when `true` returns shorthand HEX
1957
+ * @returns {string} the CSS valid colour in hexadecimal format
1564
1958
  */
1565
- toHexString() {
1566
- return `#${this.toHex()}`;
1959
+ toHex8String(allow4Char) {
1960
+ return `#${this.toHex8(allow4Char)}`;
1567
1961
  }
1568
1962
 
1569
1963
  /**
1570
- * Returns the color as a HSVA object.
1571
- * @returns {CP.HSVA} the `{h,s,v,a}` object
1964
+ * Returns the colour as a HSVA object.
1965
+ * @returns {CP.HSVA} the `{h,s,v,a}` object with [0, 1] ranged values
1572
1966
  */
1573
1967
  toHsv() {
1574
- const { h, s, v } = rgbToHsv(this.r, this.g, this.b);
1968
+ const {
1969
+ r, g, b, a,
1970
+ } = this.toRgb();
1971
+ const { h, s, v } = rgbToHsv(r, g, b);
1972
+
1575
1973
  return {
1576
- h: h * 360, s, v, a: this.a,
1974
+ h, s, v, a,
1577
1975
  };
1578
1976
  }
1579
1977
 
1580
1978
  /**
1581
- * Returns the color as a HSLA object.
1582
- * @returns {CP.HSLA}
1979
+ * Returns the colour as an HSLA object.
1980
+ * @returns {CP.HSLA} the `{h,s,l,a}` object with [0, 1] ranged values
1583
1981
  */
1584
1982
  toHsl() {
1585
- const { h, s, l } = rgbToHsl(this.r, this.g, this.b);
1983
+ const {
1984
+ r, g, b, a,
1985
+ } = this.toRgb();
1986
+ const { h, s, l } = rgbToHsl(r, g, b);
1987
+
1586
1988
  return {
1587
- h: h * 360, s, l, a: this.a,
1989
+ h, s, l, a,
1588
1990
  };
1589
1991
  }
1590
1992
 
1591
1993
  /**
1592
- * Returns the HSLA values concatenated into a string.
1593
- * @returns {string} the CSS valid color in HSL/HSLA format
1994
+ * Returns the HSLA values concatenated into a CSS3 Module format string.
1995
+ * * `hsl(150, 100%, 50%)`
1996
+ * * `hsla(150, 100%, 50%, 0.5)`
1997
+ * @returns {string} the CSS valid colour in HSL/HSLA format
1594
1998
  */
1595
1999
  toHslString() {
1596
- const hsl = this.toHsl();
1597
- const h = Math.round(hsl.h);
1598
- const s = Math.round(hsl.s * 100);
1599
- const l = Math.round(hsl.l * 100);
1600
- return this.a === 1
1601
- ? `hsl(${h},${s}%,${l}%)`
1602
- : `hsla(${h},${s}%,${l}%,${this.roundA})`;
2000
+ let {
2001
+ h, s, l, a,
2002
+ } = this.toHsl();
2003
+ h = roundPart(h * 360);
2004
+ s = roundPart(s * 100);
2005
+ l = roundPart(l * 100);
2006
+ a = roundPart(a * 100) / 100;
2007
+
2008
+ return a === 1
2009
+ ? `hsl(${h}, ${s}%, ${l}%)`
2010
+ : `hsla(${h}, ${s}%, ${l}%, ${a})`;
2011
+ }
2012
+
2013
+ /**
2014
+ * Returns the HSLA values concatenated into a CSS4 Module format string.
2015
+ * * `hsl(150deg 100% 50%)`
2016
+ * * `hsl(150deg 100% 50% / 50%)`
2017
+ * @returns {string} the CSS valid colour in CSS4 HSL format
2018
+ */
2019
+ toHslCSS4String() {
2020
+ let {
2021
+ h, s, l, a,
2022
+ } = this.toHsl();
2023
+ h = roundPart(h * 360);
2024
+ s = roundPart(s * 100);
2025
+ l = roundPart(l * 100);
2026
+ a = roundPart(a * 100);
2027
+ const A = a < 100 ? ` / ${roundPart(a)}%` : '';
2028
+
2029
+ return `hsl(${h}deg ${s}% ${l}%${A})`;
2030
+ }
2031
+
2032
+ /**
2033
+ * Returns the colour as an HWBA object.
2034
+ * @returns {CP.HWBA} the `{h,w,b,a}` object with [0, 1] ranged values
2035
+ */
2036
+ toHwb() {
2037
+ const {
2038
+ r, g, b, a,
2039
+ } = this;
2040
+ const { h, w, b: bl } = rgbToHwb(r, g, b);
2041
+ return {
2042
+ h, w, b: bl, a,
2043
+ };
1603
2044
  }
1604
2045
 
1605
2046
  /**
1606
- * Sets the alpha value on the current color.
1607
- * @param {number} alpha a new alpha value in [0-1] range.
1608
- * @returns {Color} a new `Color` instance
2047
+ * Returns the HWBA values concatenated into a string.
2048
+ * @returns {string} the CSS valid colour in HWB format
2049
+ */
2050
+ toHwbString() {
2051
+ let {
2052
+ h, w, b, a,
2053
+ } = this.toHwb();
2054
+ h = roundPart(h * 360);
2055
+ w = roundPart(w * 100);
2056
+ b = roundPart(b * 100);
2057
+ a = roundPart(a * 100);
2058
+ const A = a < 100 ? ` / ${roundPart(a)}%` : '';
2059
+
2060
+ return `hwb(${h}deg ${w}% ${b}%${A})`;
2061
+ }
2062
+
2063
+ /**
2064
+ * Sets the alpha value of the current colour.
2065
+ * @param {number} alpha a new alpha value in the [0, 1] range.
2066
+ * @returns {Color} the `Color` instance
1609
2067
  */
1610
2068
  setAlpha(alpha) {
1611
- this.a = boundAlpha(alpha);
1612
- this.roundA = Math.round(100 * this.a) / 100;
1613
- return this;
2069
+ const self = this;
2070
+ self.a = boundAlpha(alpha);
2071
+ return self;
1614
2072
  }
1615
2073
 
1616
2074
  /**
1617
- * Saturate the color with a given amount.
1618
- * @param {number=} amount a value in [0-100] range
1619
- * @returns {Color} a new `Color` instance
2075
+ * Saturate the colour with a given amount.
2076
+ * @param {number=} amount a value in the [0, 100] range
2077
+ * @returns {Color} the `Color` instance
1620
2078
  */
1621
2079
  saturate(amount) {
1622
- if (!amount) return this;
1623
- const hsl = this.toHsl();
1624
- hsl.s += amount / 100;
1625
- hsl.s = clamp01(hsl.s);
1626
- return new Color(hsl);
2080
+ const self = this;
2081
+ if (typeof amount !== 'number') return self;
2082
+ const { h, s, l } = self.toHsl();
2083
+ const { r, g, b } = hslToRgb(h, clamp01(s + amount / 100), l);
2084
+
2085
+ ObjectAssign(self, { r, g, b });
2086
+ return self;
1627
2087
  }
1628
2088
 
1629
2089
  /**
1630
- * Desaturate the color with a given amount.
1631
- * @param {number=} amount a value in [0-100] range
1632
- * @returns {Color} a new `Color` instance
2090
+ * Desaturate the colour with a given amount.
2091
+ * @param {number=} amount a value in the [0, 100] range
2092
+ * @returns {Color} the `Color` instance
1633
2093
  */
1634
2094
  desaturate(amount) {
1635
- return amount ? this.saturate(-amount) : this;
2095
+ return typeof amount === 'number' ? this.saturate(-amount) : this;
1636
2096
  }
1637
2097
 
1638
2098
  /**
1639
- * Completely desaturates a color into greyscale.
2099
+ * Completely desaturates a colour into greyscale.
1640
2100
  * Same as calling `desaturate(100)`
1641
- * @returns {Color} a new `Color` instance
2101
+ * @returns {Color} the `Color` instance
1642
2102
  */
1643
2103
  greyscale() {
1644
- return this.desaturate(100);
2104
+ return this.saturate(-100);
2105
+ }
2106
+
2107
+ /**
2108
+ * Increase the colour lightness with a given amount.
2109
+ * @param {number=} amount a value in the [0, 100] range
2110
+ * @returns {Color} the `Color` instance
2111
+ */
2112
+ lighten(amount) {
2113
+ const self = this;
2114
+ if (typeof amount !== 'number') return self;
2115
+
2116
+ const { h, s, l } = self.toHsl();
2117
+ const { r, g, b } = hslToRgb(h, s, clamp01(l + amount / 100));
2118
+
2119
+ ObjectAssign(self, { r, g, b });
2120
+ return self;
2121
+ }
2122
+
2123
+ /**
2124
+ * Decrease the colour lightness with a given amount.
2125
+ * @param {number=} amount a value in the [0, 100] range
2126
+ * @returns {Color} the `Color` instance
2127
+ */
2128
+ darken(amount) {
2129
+ return typeof amount === 'number' ? this.lighten(-amount) : this;
2130
+ }
2131
+
2132
+ /**
2133
+ * Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
2134
+ * Values outside of this range will be wrapped into this range.
2135
+ *
2136
+ * @param {number=} amount a value in the [0, 100] range
2137
+ * @returns {Color} the `Color` instance
2138
+ */
2139
+ spin(amount) {
2140
+ const self = this;
2141
+ if (typeof amount !== 'number') return self;
2142
+
2143
+ const { h, s, l } = self.toHsl();
2144
+ const { r, g, b } = hslToRgb(clamp01(((h * 360 + amount) % 360) / 360), s, l);
2145
+
2146
+ ObjectAssign(self, { r, g, b });
2147
+ return self;
1645
2148
  }
1646
2149
 
1647
2150
  /** Returns a clone of the current `Color` instance. */
@@ -1650,77 +2153,235 @@
1650
2153
  }
1651
2154
 
1652
2155
  /**
1653
- * Returns the color value in CSS valid string format.
1654
- * @returns {string}
2156
+ * Returns the colour value in CSS valid string format.
2157
+ * @param {boolean=} allowShort when *true*, HEX values can be shorthand
2158
+ * @returns {string} the CSS valid colour in the configured format
1655
2159
  */
1656
- toString() {
1657
- const { format } = this;
2160
+ toString(allowShort) {
2161
+ const self = this;
2162
+ const { format } = self;
1658
2163
 
1659
- if (format === 'rgb') {
1660
- return this.toRgbString();
1661
- }
1662
- if (format === 'hsl') {
1663
- return this.toHslString();
1664
- }
1665
- return this.toHexString();
2164
+ if (format === 'hex') return self.toHexString(allowShort);
2165
+ if (format === 'hsl') return self.toHslString();
2166
+ if (format === 'hwb') return self.toHwbString();
2167
+
2168
+ return self.toRgbString();
1666
2169
  }
1667
2170
  }
1668
2171
 
1669
2172
  ObjectAssign(Color, {
1670
- colorNames,
2173
+ ANGLES,
2174
+ CSS_ANGLE,
1671
2175
  CSS_INTEGER,
1672
2176
  CSS_NUMBER,
1673
2177
  CSS_UNIT,
1674
- PERMISSIVE_MATCH3,
1675
- PERMISSIVE_MATCH4,
2178
+ CSS_UNIT2,
2179
+ PERMISSIVE_MATCH,
1676
2180
  matchers,
1677
2181
  isOnePointZero,
1678
2182
  isPercentage,
1679
2183
  isValidCSSUnit,
2184
+ pad2,
2185
+ clamp01,
1680
2186
  bound01,
1681
2187
  boundAlpha,
1682
- clamp01,
1683
- getHexFromColorName,
1684
- convertToPercentage,
2188
+ getRGBFromName,
1685
2189
  convertHexToDecimal,
1686
- pad2,
1687
- rgbToRgb,
2190
+ convertDecimalToHex,
1688
2191
  rgbToHsl,
1689
2192
  rgbToHex,
1690
2193
  rgbToHsv,
2194
+ rgbToHwb,
2195
+ rgbaToHex,
1691
2196
  hslToRgb,
1692
2197
  hsvToRgb,
1693
- hue2rgb,
2198
+ hueToRgb,
2199
+ hwbToRgb,
1694
2200
  parseIntFromHex,
1695
2201
  numberInputToObject,
1696
2202
  stringInputToObject,
1697
2203
  inputToRGB,
2204
+ roundPart,
2205
+ ObjectAssign,
1698
2206
  });
1699
2207
 
2208
+ /**
2209
+ * @class
2210
+ * Returns a color palette with a given set of parameters.
2211
+ * @example
2212
+ * new ColorPalette(0, 12, 10);
2213
+ * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: array }
2214
+ */
2215
+ class ColorPalette {
2216
+ /**
2217
+ * The `hue` parameter is optional, which would be set to 0.
2218
+ * @param {number[]} args represeinting hue, hueSteps, lightSteps
2219
+ * * `args.hue` the starting Hue [0, 360]
2220
+ * * `args.hueSteps` Hue Steps Count [5, 24]
2221
+ * * `args.lightSteps` Lightness Steps Count [5, 12]
2222
+ */
2223
+ constructor(...args) {
2224
+ let hue = 0;
2225
+ let hueSteps = 12;
2226
+ let lightSteps = 10;
2227
+ let lightnessArray = [0.5];
2228
+
2229
+ if (args.length === 3) {
2230
+ [hue, hueSteps, lightSteps] = args;
2231
+ } else if (args.length === 2) {
2232
+ [hueSteps, lightSteps] = args;
2233
+ } else {
2234
+ throw TypeError('ColorPalette requires minimum 2 arguments');
2235
+ }
2236
+
2237
+ /** @type {string[]} */
2238
+ const colors = [];
2239
+
2240
+ const hueStep = 360 / hueSteps;
2241
+ const half = roundPart((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
2242
+ const estimatedStep = 100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100;
2243
+
2244
+ let lightStep = 0.25;
2245
+ lightStep = [4, 5].includes(lightSteps) ? 0.2 : lightStep;
2246
+ lightStep = [6, 7].includes(lightSteps) ? 0.15 : lightStep;
2247
+ lightStep = [8, 9].includes(lightSteps) ? 0.11 : lightStep;
2248
+ lightStep = [10, 11].includes(lightSteps) ? 0.09 : lightStep;
2249
+ lightStep = [12, 13].includes(lightSteps) ? 0.075 : lightStep;
2250
+ lightStep = lightSteps > 13 ? estimatedStep : lightStep;
2251
+
2252
+ // light tints
2253
+ for (let i = 1; i < half + 1; i += 1) {
2254
+ lightnessArray = [...lightnessArray, (0.5 + lightStep * (i))];
2255
+ }
2256
+
2257
+ // dark tints
2258
+ for (let i = 1; i < lightSteps - half; i += 1) {
2259
+ lightnessArray = [(0.5 - lightStep * (i)), ...lightnessArray];
2260
+ }
2261
+
2262
+ // feed `colors` Array
2263
+ for (let i = 0; i < hueSteps; i += 1) {
2264
+ const currentHue = ((hue + i * hueStep) % 360) / 360;
2265
+ lightnessArray.forEach((l) => {
2266
+ colors.push(new Color({ h: currentHue, s: 1, l }).toHexString());
2267
+ });
2268
+ }
2269
+
2270
+ this.hue = hue;
2271
+ this.hueSteps = hueSteps;
2272
+ this.lightSteps = lightSteps;
2273
+ this.colors = colors;
2274
+ }
2275
+ }
2276
+
2277
+ /**
2278
+ * Returns a color-defaults with given values and class.
2279
+ * @param {CP.ColorPicker} self
2280
+ * @param {CP.ColorPalette | string[]} colorsSource
2281
+ * @param {string} menuClass
2282
+ * @returns {HTMLElement | Element}
2283
+ */
2284
+ function getColorMenu(self, colorsSource, menuClass) {
2285
+ const { input, format, componentLabels } = self;
2286
+ const { defaultsLabel, presetsLabel } = componentLabels;
2287
+ const isOptionsMenu = menuClass === 'color-options';
2288
+ const isPalette = colorsSource instanceof ColorPalette;
2289
+ const menuLabel = isOptionsMenu ? presetsLabel : defaultsLabel;
2290
+ let colorsArray = isPalette ? colorsSource.colors : colorsSource;
2291
+ colorsArray = colorsArray instanceof Array ? colorsArray : [];
2292
+ const colorsCount = colorsArray.length;
2293
+ const { lightSteps } = isPalette ? colorsSource : { lightSteps: null };
2294
+ const fit = lightSteps || [9, 10].find((x) => colorsCount > x * 2 && !(colorsCount % x)) || 5;
2295
+ const isMultiLine = isOptionsMenu && colorsCount > fit;
2296
+ let rowCountHover = 2;
2297
+ rowCountHover = isMultiLine && colorsCount >= fit * 2 ? 3 : rowCountHover;
2298
+ rowCountHover = colorsCount >= fit * 3 ? 4 : rowCountHover;
2299
+ rowCountHover = colorsCount >= fit * 4 ? 5 : rowCountHover;
2300
+ const rowCount = rowCountHover - (colorsCount < fit * 3 ? 1 : 2);
2301
+ const isScrollable = isMultiLine && colorsCount > rowCount * fit;
2302
+ let finalClass = menuClass;
2303
+ finalClass += isScrollable ? ' scrollable' : '';
2304
+ finalClass += isMultiLine ? ' multiline' : '';
2305
+ const gap = isMultiLine ? '1px' : '0.25rem';
2306
+ let optionSize = isMultiLine ? 1.75 : 2;
2307
+ optionSize = fit > 5 && isMultiLine ? 1.5 : optionSize;
2308
+ const menuHeight = `${(rowCount || 1) * optionSize}rem`;
2309
+ const menuHeightHover = `calc(${rowCountHover} * ${optionSize}rem + ${rowCountHover - 1} * ${gap})`;
2310
+
2311
+ const menu = createElement({
2312
+ tagName: 'ul',
2313
+ className: finalClass,
2314
+ });
2315
+ setAttribute(menu, 'role', 'listbox');
2316
+ setAttribute(menu, ariaLabel, menuLabel);
2317
+
2318
+ if (isScrollable) { // @ts-ignore
2319
+ setCSSProperties(menu, {
2320
+ '--grid-item-size': `${optionSize}rem`,
2321
+ '--grid-fit': fit,
2322
+ '--grid-gap': gap,
2323
+ '--grid-height': menuHeight,
2324
+ '--grid-hover-height': menuHeightHover,
2325
+ });
2326
+ }
2327
+
2328
+ colorsArray.forEach((x) => {
2329
+ const [value, label] = x.trim().split(':');
2330
+ const xRealColor = new Color(value, format).toString();
2331
+ const isActive = xRealColor === getAttribute(input, 'value');
2332
+ const active = isActive ? ' active' : '';
2333
+
2334
+ const option = createElement({
2335
+ tagName: 'li',
2336
+ className: `color-option${active}`,
2337
+ innerText: `${label || x}`,
2338
+ });
2339
+
2340
+ setAttribute(option, tabIndex, '0');
2341
+ setAttribute(option, 'data-value', `${value}`);
2342
+ setAttribute(option, 'role', 'option');
2343
+ setAttribute(option, ariaSelected, isActive ? 'true' : 'false');
2344
+
2345
+ if (isOptionsMenu) {
2346
+ setElementStyle(option, { backgroundColor: x });
2347
+ }
2348
+
2349
+ menu.append(option);
2350
+ });
2351
+ return menu;
2352
+ }
2353
+
2354
+ /**
2355
+ * Check if a string is valid JSON string.
2356
+ * @param {string} str the string input
2357
+ * @returns {boolean} the query result
2358
+ */
2359
+ function isValidJSON(str) {
2360
+ try {
2361
+ JSON.parse(str);
2362
+ } catch (e) {
2363
+ return false;
2364
+ }
2365
+ return true;
2366
+ }
2367
+
2368
+ var version = "0.0.1";
2369
+
2370
+ // @ts-ignore
2371
+
2372
+ const Version = version;
2373
+
1700
2374
  // ColorPicker GC
1701
2375
  // ==============
1702
2376
  const colorPickerString = 'color-picker';
1703
2377
  const colorPickerSelector = `[data-function="${colorPickerString}"]`;
1704
- const nonColors = ['transparent', 'currentColor', 'inherit', 'initial'];
1705
- const colorNames$1 = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
1706
- const colorPickerLabels = {
1707
- pickerLabel: 'Colour Picker',
1708
- toggleLabel: 'Select colour',
1709
- menuLabel: 'Select colour preset',
1710
- requiredLabel: 'Required',
1711
- formatLabel: 'Colour Format',
1712
- formatHEX: 'Hexadecimal Format',
1713
- formatRGB: 'RGB Format',
1714
- formatHSL: 'HSL Format',
1715
- alphaLabel: 'Alpha',
1716
- appearanceLabel: 'Colour Appearance',
1717
- hexLabel: 'Hexadecimal',
1718
- hueLabel: 'Hue',
1719
- saturationLabel: 'Saturation',
1720
- lightnessLabel: 'Lightness',
1721
- redLabel: 'Red',
1722
- greenLabel: 'Green',
1723
- blueLabel: 'Blue',
2378
+ const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2379
+ const colorPickerDefaults = {
2380
+ componentLabels: colorPickerLabels,
2381
+ colorLabels: colorNames,
2382
+ format: 'rgb',
2383
+ colorPresets: false,
2384
+ colorKeywords: false,
1724
2385
  };
1725
2386
 
1726
2387
  // ColorPicker Static Methods
@@ -1735,165 +2396,94 @@
1735
2396
  // ColorPicker Private Methods
1736
2397
  // ===========================
1737
2398
 
1738
- /**
1739
- * Add / remove `ColorPicker` main event listeners.
1740
- * @param {ColorPicker} self
1741
- * @param {boolean=} action
1742
- */
1743
- function toggleEvents(self, action) {
1744
- const fn = action ? addListener : removeListener;
1745
- const { input, pickerToggle, menuToggle } = self;
1746
-
1747
- fn(input, 'focusin', self.showPicker);
1748
- fn(pickerToggle, 'click', self.togglePicker);
1749
-
1750
- fn(input, 'keydown', self.keyHandler);
1751
-
1752
- if (menuToggle) {
1753
- fn(menuToggle, 'click', self.toggleMenu);
1754
- }
1755
- }
1756
-
1757
2399
  /**
1758
2400
  * Generate HTML markup and update instance properties.
1759
2401
  * @param {ColorPicker} self
1760
2402
  */
1761
2403
  function initCallback(self) {
1762
2404
  const {
1763
- input, parent, format, id, componentLabels, keywords,
2405
+ input, parent, format, id, componentLabels, colorKeywords, colorPresets,
1764
2406
  } = self;
1765
2407
  const colorValue = getAttribute(input, 'value') || '#fff';
1766
2408
 
1767
2409
  const {
1768
- toggleLabel, menuLabel, formatLabel, pickerLabel, appearanceLabel,
2410
+ toggleLabel, pickerLabel, formatLabel, hexLabel,
1769
2411
  } = componentLabels;
1770
2412
 
1771
2413
  // update color
1772
2414
  const color = nonColors.includes(colorValue) ? '#fff' : colorValue;
1773
- self.color = new Color(color, { format });
2415
+ self.color = new Color(color, format);
1774
2416
 
1775
2417
  // set initial controls dimensions
1776
2418
  // make the controls smaller on mobile
1777
- const cv1w = isMobile ? 150 : 230;
1778
- const cvh = isMobile ? 150 : 230;
1779
- const cv2w = 21;
1780
2419
  const dropClass = isMobile ? ' mobile' : '';
1781
- const ctrl1Labelledby = format === 'hsl' ? `appearance_${id} appearance1_${id}` : `appearance1_${id}`;
1782
- const ctrl2Labelledby = format === 'hsl' ? `appearance2_${id}` : `appearance_${id} appearance2_${id}`;
2420
+ const formatString = format === 'hex' ? hexLabel : format.toUpperCase();
1783
2421
 
1784
2422
  const pickerBtn = createElement({
2423
+ id: `picker-btn-${id}`,
1785
2424
  tagName: 'button',
1786
- className: 'picker-toggle button-appearance',
1787
- ariaExpanded: 'false',
1788
- ariaHasPopup: 'true',
1789
- ariaLive: 'polite',
2425
+ className: 'picker-toggle btn-appearance',
1790
2426
  });
1791
- setAttribute(pickerBtn, 'tabindex', '-1');
2427
+ setAttribute(pickerBtn, ariaExpanded, 'false');
2428
+ setAttribute(pickerBtn, ariaHasPopup, 'true');
1792
2429
  pickerBtn.append(createElement({
1793
2430
  tagName: 'span',
1794
2431
  className: vHidden,
1795
- innerText: 'Open Color Picker',
2432
+ innerText: `${pickerLabel}. ${formatLabel}: ${formatString}`,
1796
2433
  }));
1797
2434
 
1798
- const colorPickerDropdown = createElement({
2435
+ const pickerDropdown = createElement({
1799
2436
  tagName: 'div',
1800
2437
  className: `color-dropdown picker${dropClass}`,
1801
2438
  });
1802
- setAttribute(colorPickerDropdown, ariaLabelledBy, `picker-label-${id} format-label-${id}`);
1803
- setAttribute(colorPickerDropdown, 'role', 'group');
1804
- colorPickerDropdown.append(
1805
- createElement({
1806
- tagName: 'label',
1807
- className: vHidden,
1808
- ariaHidden: 'true',
1809
- id: `picker-label-${id}`,
1810
- innerText: `${pickerLabel}`,
1811
- }),
1812
- createElement({
1813
- tagName: 'label',
1814
- className: vHidden,
1815
- ariaHidden: 'true',
1816
- id: `format-label-${id}`,
1817
- innerText: `${formatLabel}`,
1818
- }),
1819
- createElement({
1820
- tagName: 'label',
1821
- className: `color-appearance ${vHidden}`,
1822
- ariaHidden: 'true',
1823
- ariaLive: 'polite',
1824
- id: `appearance_${id}`,
1825
- innerText: `${appearanceLabel}`,
1826
- }),
1827
- );
1828
-
1829
- const colorControls = createElement({
1830
- tagName: 'div',
1831
- className: `color-controls ${format}`,
1832
- });
1833
-
1834
- colorControls.append(
1835
- getColorControl(1, id, cv1w, cvh, ctrl1Labelledby),
1836
- getColorControl(2, id, cv2w, cvh, ctrl2Labelledby),
1837
- );
2439
+ setAttribute(pickerDropdown, ariaLabelledBy, `picker-btn-${id}`);
2440
+ setAttribute(pickerDropdown, 'role', 'group');
1838
2441
 
1839
- if (format !== 'hex') {
1840
- colorControls.append(
1841
- getColorControl(3, id, cv2w, cvh),
1842
- );
1843
- }
1844
-
1845
- // @ts-ignore
2442
+ const colorControls = getColorControls(self);
1846
2443
  const colorForm = getColorForm(self);
1847
- colorPickerDropdown.append(colorControls, colorForm);
1848
- parent.append(pickerBtn, colorPickerDropdown);
1849
2444
 
1850
- // set color key menu template
1851
- if (keywords) {
1852
- const colorKeys = keywords;
2445
+ pickerDropdown.append(colorControls, colorForm);
2446
+ input.before(pickerBtn);
2447
+ parent.append(pickerDropdown);
2448
+
2449
+ // set colour key menu template
2450
+ if (colorKeywords || colorPresets) {
1853
2451
  const presetsDropdown = createElement({
1854
2452
  tagName: 'div',
1855
- className: `color-dropdown menu${dropClass}`,
1856
- });
1857
- const presetsMenu = createElement({
1858
- tagName: 'ul',
1859
- ariaLabel: `${menuLabel}`,
1860
- className: 'color-menu',
1861
- });
1862
- setAttribute(presetsMenu, 'role', 'listbox');
1863
- presetsDropdown.append(presetsMenu);
1864
-
1865
- colorKeys.forEach((x) => {
1866
- const xKey = x.trim();
1867
- const xRealColor = new Color(xKey, { format }).toString();
1868
- const isActive = xRealColor === getAttribute(input, 'value');
1869
- const active = isActive ? ' active' : '';
1870
-
1871
- const keyOption = createElement({
1872
- tagName: 'li',
1873
- className: `color-option${active}`,
1874
- ariaSelected: isActive ? 'true' : 'false',
1875
- innerText: `${x}`,
1876
- });
1877
- setAttribute(keyOption, 'role', 'option');
1878
- setAttribute(keyOption, 'tabindex', '0');
1879
- setAttribute(keyOption, 'data-value', `${xKey}`);
1880
- presetsMenu.append(keyOption);
2453
+ className: `color-dropdown scrollable menu${dropClass}`,
1881
2454
  });
2455
+
2456
+ // color presets
2457
+ if ((colorPresets instanceof Array && colorPresets.length)
2458
+ || (colorPresets instanceof ColorPalette && colorPresets.colors)) {
2459
+ const presetsMenu = getColorMenu(self, colorPresets, 'color-options');
2460
+ presetsDropdown.append(presetsMenu);
2461
+ }
2462
+
2463
+ // explicit defaults [reset, initial, inherit, transparent, currentColor]
2464
+ if (colorKeywords && colorKeywords.length) {
2465
+ const keywordsMenu = getColorMenu(self, colorKeywords, 'color-defaults');
2466
+ presetsDropdown.append(keywordsMenu);
2467
+ }
2468
+
1882
2469
  const presetsBtn = createElement({
1883
2470
  tagName: 'button',
1884
- className: 'menu-toggle button-appearance',
1885
- ariaExpanded: 'false',
1886
- ariaHasPopup: 'true',
2471
+ className: 'menu-toggle btn-appearance',
1887
2472
  });
2473
+ setAttribute(presetsBtn, tabIndex, '-1');
2474
+ setAttribute(presetsBtn, ariaExpanded, 'false');
2475
+ setAttribute(presetsBtn, ariaHasPopup, 'true');
2476
+
1888
2477
  const xmlns = encodeURI('http://www.w3.org/2000/svg');
1889
2478
  const presetsIcon = createElementNS(xmlns, { tagName: 'svg' });
1890
2479
  setAttribute(presetsIcon, 'xmlns', xmlns);
1891
- setAttribute(presetsIcon, ariaHidden, 'true');
1892
2480
  setAttribute(presetsIcon, 'viewBox', '0 0 512 512');
1893
- const piPath = createElementNS(xmlns, { tagName: 'path' });
1894
- setAttribute(piPath, 'd', 'M98,158l157,156L411,158l27,27L255,368L71,185L98,158z');
1895
- setAttribute(piPath, 'fill', '#fff');
1896
- presetsIcon.append(piPath);
2481
+ setAttribute(presetsIcon, ariaHidden, 'true');
2482
+
2483
+ const path = createElementNS(xmlns, { tagName: 'path' });
2484
+ setAttribute(path, 'd', 'M98,158l157,156L411,158l27,27L255,368L71,185L98,158z');
2485
+ setAttribute(path, 'fill', '#fff');
2486
+ presetsIcon.append(path);
1897
2487
  presetsBtn.append(createElement({
1898
2488
  tagName: 'span',
1899
2489
  className: vHidden,
@@ -1904,9 +2494,29 @@
1904
2494
  }
1905
2495
 
1906
2496
  // solve non-colors after settings save
1907
- if (keywords && nonColors.includes(colorValue)) {
2497
+ if (colorKeywords && nonColors.includes(colorValue)) {
1908
2498
  self.value = colorValue;
1909
2499
  }
2500
+ setAttribute(input, tabIndex, '-1');
2501
+ }
2502
+
2503
+ /**
2504
+ * Add / remove `ColorPicker` main event listeners.
2505
+ * @param {ColorPicker} self
2506
+ * @param {boolean=} action
2507
+ */
2508
+ function toggleEvents(self, action) {
2509
+ const fn = action ? addListener : removeListener;
2510
+ const { input, pickerToggle, menuToggle } = self;
2511
+
2512
+ fn(input, focusinEvent, self.showPicker);
2513
+ fn(pickerToggle, mouseclickEvent, self.togglePicker);
2514
+
2515
+ fn(input, keydownEvent, self.keyToggle);
2516
+
2517
+ if (menuToggle) {
2518
+ fn(menuToggle, mouseclickEvent, self.toggleMenu);
2519
+ }
1910
2520
  }
1911
2521
 
1912
2522
  /**
@@ -1916,26 +2526,33 @@
1916
2526
  */
1917
2527
  function toggleEventsOnShown(self, action) {
1918
2528
  const fn = action ? addListener : removeListener;
1919
- const pointerEvents = 'ontouchstart' in document
1920
- ? { down: 'touchstart', move: 'touchmove', up: 'touchend' }
1921
- : { down: 'mousedown', move: 'mousemove', up: 'mouseup' };
2529
+ const { input, colorMenu, parent } = self;
2530
+ const doc = getDocument(input);
2531
+ const win = getWindow(input);
2532
+ const pointerEvents = `on${touchstartEvent}` in doc
2533
+ ? { down: touchstartEvent, move: touchmoveEvent, up: touchendEvent }
2534
+ : { down: mousedownEvent, move: mousemoveEvent, up: mouseupEvent };
1922
2535
 
1923
2536
  fn(self.controls, pointerEvents.down, self.pointerDown);
1924
- self.controlKnobs.forEach((x) => fn(x, 'keydown', self.handleKnobs));
2537
+ self.controlKnobs.forEach((x) => fn(x, keydownEvent, self.handleKnobs));
1925
2538
 
1926
- fn(window, 'scroll', self.handleScroll);
2539
+ // @ts-ignore -- this is `Window`
2540
+ fn(win, scrollEvent, self.handleScroll);
2541
+ // @ts-ignore -- this is `Window`
2542
+ fn(win, resizeEvent, self.update);
1927
2543
 
1928
- [self.input, ...self.inputs].forEach((x) => fn(x, 'change', self.changeHandler));
2544
+ [input, ...self.inputs].forEach((x) => fn(x, changeEvent, self.changeHandler));
1929
2545
 
1930
- if (self.colorMenu) {
1931
- fn(self.colorMenu, 'click', self.menuClickHandler);
1932
- fn(self.colorMenu, 'keydown', self.menuKeyHandler);
2546
+ if (colorMenu) {
2547
+ fn(colorMenu, mouseclickEvent, self.menuClickHandler);
2548
+ fn(colorMenu, keydownEvent, self.menuKeyHandler);
1933
2549
  }
1934
2550
 
1935
- fn(document, pointerEvents.move, self.pointerMove);
1936
- fn(document, pointerEvents.up, self.pointerUp);
1937
- fn(window, 'keyup', self.handleDismiss);
1938
- fn(self.parent, 'focusout', self.handleFocusOut);
2551
+ fn(doc, pointerEvents.move, self.pointerMove);
2552
+ fn(doc, pointerEvents.up, self.pointerUp);
2553
+ fn(parent, focusoutEvent, self.handleFocusOut);
2554
+ // @ts-ignore -- this is `Window`
2555
+ fn(win, keyupEvent, self.handleDismiss);
1939
2556
  }
1940
2557
 
1941
2558
  /**
@@ -1947,61 +2564,93 @@
1947
2564
  }
1948
2565
 
1949
2566
  /**
1950
- * Toggles the visibility of a dropdown or returns false if none is visible.
2567
+ * Hides a visible dropdown.
1951
2568
  * @param {HTMLElement} element
1952
- * @param {boolean=} check
1953
- * @returns {void | boolean}
2569
+ * @returns {void}
1954
2570
  */
1955
- function classToggle(element, check) {
1956
- const fn1 = !check ? 'forEach' : 'some';
1957
- const fn2 = !check ? removeClass : hasClass;
1958
-
2571
+ function removePosition(element) {
1959
2572
  if (element) {
1960
- return ['show', 'show-top'][fn1]((x) => fn2(element, x));
2573
+ ['bottom', 'top'].forEach((x) => removeClass(element, x));
1961
2574
  }
1962
-
1963
- return false;
1964
2575
  }
1965
2576
 
1966
2577
  /**
1967
- * Shows the `ColorPicker` presets menu.
2578
+ * Shows a `ColorPicker` dropdown and close the curent open dropdown.
1968
2579
  * @param {ColorPicker} self
2580
+ * @param {HTMLElement | Element} dropdown
1969
2581
  */
1970
- function showMenu(self) {
1971
- classToggle(self.colorPicker);
1972
- addClass(self.colorMenu, 'show');
1973
- self.show();
1974
- setAttribute(self.menuToggle, ariaExpanded, 'true');
2582
+ function showDropdown(self, dropdown) {
2583
+ const {
2584
+ colorPicker, colorMenu, menuToggle, pickerToggle, parent,
2585
+ } = self;
2586
+ const isPicker = dropdown === colorPicker;
2587
+ const openDropdown = isPicker ? colorMenu : colorPicker;
2588
+ const activeBtn = isPicker ? menuToggle : pickerToggle;
2589
+ const nextBtn = !isPicker ? menuToggle : pickerToggle;
2590
+
2591
+ if (!hasClass(parent, 'open')) {
2592
+ addClass(parent, 'open');
2593
+ }
2594
+ if (openDropdown) {
2595
+ removeClass(openDropdown, 'show');
2596
+ removePosition(openDropdown);
2597
+ }
2598
+ addClass(dropdown, 'bottom');
2599
+ reflow(dropdown);
2600
+ addClass(dropdown, 'show');
2601
+
2602
+ if (isPicker) self.update();
2603
+
2604
+ if (!self.isOpen) {
2605
+ toggleEventsOnShown(self, true);
2606
+ self.updateDropdownPosition();
2607
+ self.isOpen = true;
2608
+ setAttribute(self.input, tabIndex, '0');
2609
+ if (menuToggle) {
2610
+ setAttribute(menuToggle, tabIndex, '0');
2611
+ }
2612
+ }
2613
+
2614
+ setAttribute(nextBtn, ariaExpanded, 'true');
2615
+ if (activeBtn) {
2616
+ setAttribute(activeBtn, ariaExpanded, 'false');
2617
+ }
1975
2618
  }
1976
2619
 
1977
2620
  /**
1978
- * Color Picker
2621
+ * Color Picker Web Component
1979
2622
  * @see http://thednp.github.io/color-picker
1980
2623
  */
1981
2624
  class ColorPicker {
1982
2625
  /**
1983
- * Returns a new ColorPicker instance.
2626
+ * Returns a new `ColorPicker` instance. The target of this constructor
2627
+ * must be an `HTMLInputElement`.
2628
+ *
1984
2629
  * @param {HTMLInputElement | string} target the target `<input>` element
2630
+ * @param {CP.ColorPickerOptions=} config instance options
1985
2631
  */
1986
- constructor(target) {
2632
+ constructor(target, config) {
1987
2633
  const self = this;
1988
2634
  /** @type {HTMLInputElement} */
1989
2635
  // @ts-ignore
1990
- self.input = querySelector(target);
2636
+ const input = querySelector(target);
2637
+
1991
2638
  // invalidate
1992
- if (!self.input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
1993
- const { input } = self;
2639
+ if (!input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
2640
+ self.input = input;
2641
+
2642
+ const parent = closest(input, colorPickerParentSelector);
2643
+ if (!parent) throw new TypeError('ColorPicker requires a specific markup to work.');
1994
2644
 
1995
2645
  /** @type {HTMLElement} */
1996
2646
  // @ts-ignore
1997
- self.parent = closest(input, `.${colorPickerString},${colorPickerString}`);
1998
- if (!self.parent) throw new TypeError('ColorPicker requires a specific markup to work.');
2647
+ self.parent = parent;
1999
2648
 
2000
2649
  /** @type {number} */
2001
2650
  self.id = getUID(input, colorPickerString);
2002
2651
 
2003
2652
  // set initial state
2004
- /** @type {HTMLCanvasElement?} */
2653
+ /** @type {HTMLElement?} */
2005
2654
  self.dragElement = null;
2006
2655
  /** @type {boolean} */
2007
2656
  self.isOpen = false;
@@ -2011,26 +2660,59 @@
2011
2660
  };
2012
2661
  /** @type {Record<string, string>} */
2013
2662
  self.colorLabels = {};
2014
- /** @type {Array<string> | false} */
2015
- self.keywords = false;
2016
- /** @type {Color} */
2017
- self.color = new Color('white', { format: self.format });
2663
+ /** @type {string[]=} */
2664
+ self.colorKeywords = undefined;
2665
+ /** @type {(ColorPalette | string[])=} */
2666
+ self.colorPresets = undefined;
2667
+
2668
+ // process options
2669
+ const {
2670
+ format, componentLabels, colorLabels, colorKeywords, colorPresets,
2671
+ } = normalizeOptions(this.isCE ? parent : input, colorPickerDefaults, config || {});
2672
+
2673
+ let translatedColorLabels = colorNames;
2674
+ if (colorLabels instanceof Array && colorLabels.length === 17) {
2675
+ translatedColorLabels = colorLabels;
2676
+ } else if (colorLabels && colorLabels.split(',').length === 17) {
2677
+ translatedColorLabels = colorLabels.split(',');
2678
+ }
2679
+
2680
+ // expose colour labels to all methods
2681
+ colorNames.forEach((c, i) => {
2682
+ self.colorLabels[c] = translatedColorLabels[i].trim();
2683
+ });
2684
+
2685
+ // update and expose component labels
2686
+ const tempLabels = ObjectAssign({}, colorPickerLabels);
2687
+ const jsonLabels = componentLabels && isValidJSON(componentLabels)
2688
+ ? JSON.parse(componentLabels) : componentLabels || {};
2689
+
2018
2690
  /** @type {Record<string, string>} */
2019
- self.componentLabels = ObjectAssign({}, colorPickerLabels);
2691
+ self.componentLabels = ObjectAssign(tempLabels, jsonLabels);
2020
2692
 
2021
- const { componentLabels, colorLabels, keywords } = input.dataset;
2022
- const temp = componentLabels ? JSON.parse(componentLabels) : {};
2023
- self.componentLabels = ObjectAssign(self.componentLabels, temp);
2693
+ /** @type {Color} */
2694
+ self.color = new Color('white', format);
2024
2695
 
2025
- const translatedColorLabels = colorLabels && colorLabels.split(',').length === 17
2026
- ? colorLabels.split(',') : colorNames$1;
2696
+ /** @type {CP.ColorFormats} */
2697
+ self.format = format;
2027
2698
 
2028
- // expose color labels to all methods
2029
- colorNames$1.forEach((c, i) => { self.colorLabels[c] = translatedColorLabels[i]; });
2699
+ // set colour defaults
2700
+ if (colorKeywords instanceof Array) {
2701
+ self.colorKeywords = colorKeywords;
2702
+ } else if (typeof colorKeywords === 'string' && colorKeywords.length) {
2703
+ self.colorKeywords = colorKeywords.split(',');
2704
+ }
2030
2705
 
2031
2706
  // set colour presets
2032
- if (keywords !== 'false') {
2033
- self.keywords = keywords ? keywords.split(',') : nonColors;
2707
+ if (colorPresets instanceof Array) {
2708
+ self.colorPresets = colorPresets;
2709
+ } else if (typeof colorPresets === 'string' && colorPresets.length) {
2710
+ if (isValidJSON(colorPresets)) {
2711
+ const { hue, hueSteps, lightSteps } = JSON.parse(colorPresets);
2712
+ self.colorPresets = new ColorPalette(hue, hueSteps, lightSteps);
2713
+ } else {
2714
+ self.colorPresets = colorPresets.split(',').map((x) => x.trim());
2715
+ }
2034
2716
  }
2035
2717
 
2036
2718
  // bind events
@@ -2042,17 +2724,18 @@
2042
2724
  self.pointerDown = self.pointerDown.bind(self);
2043
2725
  self.pointerMove = self.pointerMove.bind(self);
2044
2726
  self.pointerUp = self.pointerUp.bind(self);
2727
+ self.update = self.update.bind(self);
2045
2728
  self.handleScroll = self.handleScroll.bind(self);
2046
2729
  self.handleFocusOut = self.handleFocusOut.bind(self);
2047
2730
  self.changeHandler = self.changeHandler.bind(self);
2048
2731
  self.handleDismiss = self.handleDismiss.bind(self);
2049
- self.keyHandler = self.keyHandler.bind(self);
2732
+ self.keyToggle = self.keyToggle.bind(self);
2050
2733
  self.handleKnobs = self.handleKnobs.bind(self);
2051
2734
 
2052
2735
  // generate markup
2053
2736
  initCallback(self);
2054
2737
 
2055
- const { parent } = self;
2738
+ const [colorPicker, colorMenu] = getElementsByClassName('color-dropdown', parent);
2056
2739
  // set main elements
2057
2740
  /** @type {HTMLElement} */
2058
2741
  // @ts-ignore
@@ -2062,68 +2745,24 @@
2062
2745
  self.menuToggle = querySelector('.menu-toggle', parent);
2063
2746
  /** @type {HTMLElement} */
2064
2747
  // @ts-ignore
2065
- self.colorMenu = querySelector('.color-dropdown.menu', parent);
2066
- /** @type {HTMLElement} */
2067
- // @ts-ignore
2068
- self.colorPicker = querySelector('.color-dropdown.picker', parent);
2748
+ self.colorPicker = colorPicker;
2069
2749
  /** @type {HTMLElement} */
2070
2750
  // @ts-ignore
2071
- self.controls = querySelector('.color-controls', parent);
2751
+ self.colorMenu = colorMenu;
2072
2752
  /** @type {HTMLInputElement[]} */
2073
2753
  // @ts-ignore
2074
- self.inputs = [...querySelectorAll('.color-input', parent)];
2754
+ self.inputs = [...getElementsByClassName('color-input', parent)];
2755
+ const [controls] = getElementsByClassName('color-controls', parent);
2756
+ self.controls = controls;
2757
+ /** @type {(HTMLElement | Element)[]} */
2758
+ self.controlKnobs = [...getElementsByClassName('knob', controls)];
2075
2759
  /** @type {(HTMLElement)[]} */
2076
2760
  // @ts-ignore
2077
- self.controlKnobs = [...querySelectorAll('.knob', parent)];
2078
- /** @type {HTMLCanvasElement[]} */
2079
- // @ts-ignore
2080
- self.visuals = [...querySelectorAll('canvas', self.controls)];
2081
- /** @type {HTMLLabelElement[]} */
2082
- // @ts-ignore
2083
- self.knobLabels = [...querySelectorAll('.color-label', parent)];
2084
- /** @type {HTMLLabelElement} */
2085
- // @ts-ignore
2086
- self.appearance = querySelector('.color-appearance', parent);
2087
-
2088
- const [v1, v2, v3] = self.visuals;
2089
- // set dimensions
2090
- /** @type {number} */
2091
- self.width1 = v1.width;
2092
- /** @type {number} */
2093
- self.height1 = v1.height;
2094
- /** @type {number} */
2095
- self.width2 = v2.width;
2096
- /** @type {number} */
2097
- self.height2 = v2.height;
2098
- // set main controls
2099
- /** @type {*} */
2100
- self.ctx1 = v1.getContext('2d');
2101
- /** @type {*} */
2102
- self.ctx2 = v2.getContext('2d');
2103
- self.ctx1.rect(0, 0, self.width1, self.height1);
2104
- self.ctx2.rect(0, 0, self.width2, self.height2);
2761
+ self.visuals = [...getElementsByClassName('visual-control', controls)];
2105
2762
 
2106
- /** @type {number} */
2107
- self.width3 = 0;
2108
- /** @type {number} */
2109
- self.height3 = 0;
2110
-
2111
- // set alpha control except hex
2112
- if (self.format !== 'hex') {
2113
- self.width3 = v3.width;
2114
- self.height3 = v3.height;
2115
- /** @type {*} */
2116
- this.ctx3 = v3.getContext('2d');
2117
- self.ctx3.rect(0, 0, self.width3, self.height3);
2118
- }
2763
+ // update colour picker controls, inputs and visuals
2764
+ self.update();
2119
2765
 
2120
- // update color picker controls, inputs and visuals
2121
- this.setControlPositions();
2122
- this.setColorAppearence();
2123
- // don't trigger change at initialization
2124
- this.updateInputs(true);
2125
- this.updateControls();
2126
- this.updateVisuals();
2127
2766
  // add main events listeners
2128
2767
  toggleEvents(self, true);
2129
2768
 
@@ -2131,65 +2770,52 @@
2131
2770
  Data.set(input, colorPickerString, self);
2132
2771
  }
2133
2772
 
2134
- /** Returns the current color value */
2773
+ /** Returns the current colour value */
2135
2774
  get value() { return this.input.value; }
2136
2775
 
2137
2776
  /**
2138
- * Sets a new color value.
2139
- * @param {string} v new color value
2777
+ * Sets a new colour value.
2778
+ * @param {string} v new colour value
2140
2779
  */
2141
2780
  set value(v) { this.input.value = v; }
2142
2781
 
2143
- /** Check if the input is required to have a valid value. */
2144
- get required() { return hasAttribute(this.input, 'required'); }
2145
-
2146
- /**
2147
- * Returns the colour format.
2148
- * @returns {CP.ColorFormats | string}
2149
- */
2150
- get format() { return getAttribute(this.input, 'format') || 'hex'; }
2151
-
2152
- /** Returns the input name. */
2153
- get name() { return getAttribute(this.input, 'name'); }
2154
-
2155
- /**
2156
- * Returns the label associated to the input.
2157
- * @returns {HTMLLabelElement?}
2158
- */
2159
- // @ts-ignore
2160
- get label() { return querySelector(`[for="${this.input.id}"]`); }
2161
-
2162
- /** Check if the color presets include any non-color. */
2163
- get includeNonColor() {
2164
- return this.keywords instanceof Array
2165
- && this.keywords.some((x) => nonColors.includes(x));
2782
+ /** Check if the colour presets include any non-colour. */
2783
+ get hasNonColor() {
2784
+ return this.colorKeywords instanceof Array
2785
+ && this.colorKeywords.some((x) => nonColors.includes(x));
2166
2786
  }
2167
2787
 
2168
- /** Returns hexadecimal value of the current color. */
2169
- get hex() { return this.color.toHex(); }
2788
+ /** Check if the parent of the target is a `ColorPickerElement` instance. */
2789
+ get isCE() { return this.parent.localName === colorPickerString; }
2790
+
2791
+ /** Returns hexadecimal value of the current colour. */
2792
+ get hex() { return this.color.toHex(true); }
2170
2793
 
2171
- /** Returns the current color value in {h,s,v,a} object format. */
2794
+ /** Returns the current colour value in {h,s,v,a} object format. */
2172
2795
  get hsv() { return this.color.toHsv(); }
2173
2796
 
2174
- /** Returns the current color value in {h,s,l,a} object format. */
2797
+ /** Returns the current colour value in {h,s,l,a} object format. */
2175
2798
  get hsl() { return this.color.toHsl(); }
2176
2799
 
2177
- /** Returns the current color value in {r,g,b,a} object format. */
2800
+ /** Returns the current colour value in {h,w,b,a} object format. */
2801
+ get hwb() { return this.color.toHwb(); }
2802
+
2803
+ /** Returns the current colour value in {r,g,b,a} object format. */
2178
2804
  get rgb() { return this.color.toRgb(); }
2179
2805
 
2180
- /** Returns the current color brightness. */
2806
+ /** Returns the current colour brightness. */
2181
2807
  get brightness() { return this.color.brightness; }
2182
2808
 
2183
- /** Returns the current color luminance. */
2809
+ /** Returns the current colour luminance. */
2184
2810
  get luminance() { return this.color.luminance; }
2185
2811
 
2186
- /** Checks if the current colour requires a light text color. */
2812
+ /** Checks if the current colour requires a light text colour. */
2187
2813
  get isDark() {
2188
- const { rgb, brightness } = this;
2189
- return brightness < 120 && rgb.a > 0.33;
2814
+ const { color, brightness } = this;
2815
+ return brightness < 120 && color.a > 0.33;
2190
2816
  }
2191
2817
 
2192
- /** Checks if the current input value is a valid color. */
2818
+ /** Checks if the current input value is a valid colour. */
2193
2819
  get isValid() {
2194
2820
  const inputValue = this.input.value;
2195
2821
  return inputValue !== '' && new Color(inputValue).isValid;
@@ -2199,89 +2825,79 @@
2199
2825
  updateVisuals() {
2200
2826
  const self = this;
2201
2827
  const {
2202
- color, format, controlPositions,
2203
- width1, width2, width3,
2204
- height1, height2, height3,
2205
- ctx1, ctx2, ctx3,
2828
+ format, controlPositions, visuals,
2206
2829
  } = self;
2207
- const { r, g, b } = color;
2830
+ const [v1, v2, v3] = visuals;
2831
+ const { offsetWidth, offsetHeight } = v1;
2832
+ const hue = format === 'hsl'
2833
+ ? controlPositions.c1x / offsetWidth
2834
+ : controlPositions.c2y / offsetHeight;
2835
+ // @ts-ignore - `hslToRgb` is assigned to `Color` as static method
2836
+ const { r, g, b } = Color.hslToRgb(hue, 1, 0.5);
2837
+ const whiteGrad = 'linear-gradient(rgb(255,255,255) 0%, rgb(255,255,255) 100%)';
2838
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
2839
+ const roundA = roundPart((alpha * 100)) / 100;
2208
2840
 
2209
2841
  if (format !== 'hsl') {
2210
- const hue = Math.round((controlPositions.c2y / height2) * 360);
2211
- ctx1.fillStyle = new Color(`hsl(${hue},100%,50%})`).toRgbString();
2212
- ctx1.fillRect(0, 0, width1, height1);
2213
-
2214
- const whiteGrad = ctx2.createLinearGradient(0, 0, width1, 0);
2215
- whiteGrad.addColorStop(0, 'rgba(255,255,255,1)');
2216
- whiteGrad.addColorStop(1, 'rgba(255,255,255,0)');
2217
- ctx1.fillStyle = whiteGrad;
2218
- ctx1.fillRect(0, 0, width1, height1);
2219
-
2220
- const blackGrad = ctx2.createLinearGradient(0, 0, 0, height1);
2221
- blackGrad.addColorStop(0, 'rgba(0,0,0,0)');
2222
- blackGrad.addColorStop(1, 'rgba(0,0,0,1)');
2223
- ctx1.fillStyle = blackGrad;
2224
- ctx1.fillRect(0, 0, width1, height1);
2225
-
2226
- const hueGrad = ctx2.createLinearGradient(0, 0, 0, height1);
2227
- hueGrad.addColorStop(0, 'rgba(255,0,0,1)');
2228
- hueGrad.addColorStop(0.17, 'rgba(255,255,0,1)');
2229
- hueGrad.addColorStop(0.34, 'rgba(0,255,0,1)');
2230
- hueGrad.addColorStop(0.51, 'rgba(0,255,255,1)');
2231
- hueGrad.addColorStop(0.68, 'rgba(0,0,255,1)');
2232
- hueGrad.addColorStop(0.85, 'rgba(255,0,255,1)');
2233
- hueGrad.addColorStop(1, 'rgba(255,0,0,1)');
2234
- ctx2.fillStyle = hueGrad;
2235
- ctx2.fillRect(0, 0, width2, height2);
2842
+ const fill = new Color({
2843
+ h: hue, s: 1, l: 0.5, a: alpha,
2844
+ }).toRgbString();
2845
+ const hueGradient = `linear-gradient(
2846
+ rgb(255,0,0) 0%, rgb(255,255,0) 16.67%,
2847
+ rgb(0,255,0) 33.33%, rgb(0,255,255) 50%,
2848
+ rgb(0,0,255) 66.67%, rgb(255,0,255) 83.33%,
2849
+ rgb(255,0,0) 100%)`;
2850
+ setElementStyle(v1, {
2851
+ background: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,${roundA}) 100%),
2852
+ linear-gradient(to right, rgba(255,255,255,${roundA}) 0%, ${fill} 100%),
2853
+ ${whiteGrad}`,
2854
+ });
2855
+ setElementStyle(v2, { background: hueGradient });
2236
2856
  } else {
2237
- const hueGrad = ctx1.createLinearGradient(0, 0, width1, 0);
2238
- const saturation = Math.round((1 - controlPositions.c2y / height2) * 100);
2239
-
2240
- hueGrad.addColorStop(0, new Color('rgba(255,0,0,1)').desaturate(100 - saturation).toRgbString());
2241
- hueGrad.addColorStop(0.17, new Color('rgba(255,255,0,1)').desaturate(100 - saturation).toRgbString());
2242
- hueGrad.addColorStop(0.34, new Color('rgba(0,255,0,1)').desaturate(100 - saturation).toRgbString());
2243
- hueGrad.addColorStop(0.51, new Color('rgba(0,255,255,1)').desaturate(100 - saturation).toRgbString());
2244
- hueGrad.addColorStop(0.68, new Color('rgba(0,0,255,1)').desaturate(100 - saturation).toRgbString());
2245
- hueGrad.addColorStop(0.85, new Color('rgba(255,0,255,1)').desaturate(100 - saturation).toRgbString());
2246
- hueGrad.addColorStop(1, new Color('rgba(255,0,0,1)').desaturate(100 - saturation).toRgbString());
2247
-
2248
- ctx1.fillStyle = hueGrad;
2249
- ctx1.fillRect(0, 0, width1, height1);
2250
-
2251
- const whiteGrad = ctx1.createLinearGradient(0, 0, 0, height1);
2252
- whiteGrad.addColorStop(0, 'rgba(255,255,255,1)');
2253
- whiteGrad.addColorStop(0.5, 'rgba(255,255,255,0)');
2254
- ctx1.fillStyle = whiteGrad;
2255
- ctx1.fillRect(0, 0, width1, height1);
2256
-
2257
- const blackGrad = ctx1.createLinearGradient(0, 0, 0, height1);
2258
- blackGrad.addColorStop(0.5, 'rgba(0,0,0,0)');
2259
- blackGrad.addColorStop(1, 'rgba(0,0,0,1)');
2260
- ctx1.fillStyle = blackGrad;
2261
- ctx1.fillRect(0, 0, width1, height1);
2262
-
2263
- const saturationGrad = ctx2.createLinearGradient(0, 0, 0, height2);
2264
- const incolor = color.clone().greyscale().toRgb();
2265
-
2266
- saturationGrad.addColorStop(0, `rgba(${r},${g},${b},1)`);
2267
- saturationGrad.addColorStop(1, `rgba(${incolor.r},${incolor.g},${incolor.b},1)`);
2268
-
2269
- ctx2.fillStyle = saturationGrad;
2270
- ctx2.fillRect(0, 0, width3, height3);
2271
- }
2272
-
2273
- if (format !== 'hex') {
2274
- ctx3.clearRect(0, 0, width3, height3);
2275
- const alphaGrad = ctx3.createLinearGradient(0, 0, 0, height3);
2276
- alphaGrad.addColorStop(0, `rgba(${r},${g},${b},1)`);
2277
- alphaGrad.addColorStop(1, `rgba(${r},${g},${b},0)`);
2278
- ctx3.fillStyle = alphaGrad;
2279
- ctx3.fillRect(0, 0, width3, height3);
2857
+ const saturation = roundPart((controlPositions.c2y / offsetHeight) * 100);
2858
+ const fill0 = new Color({
2859
+ r: 255, g: 0, b: 0, a: alpha,
2860
+ }).saturate(-saturation).toRgbString();
2861
+ const fill1 = new Color({
2862
+ r: 255, g: 255, b: 0, a: alpha,
2863
+ }).saturate(-saturation).toRgbString();
2864
+ const fill2 = new Color({
2865
+ r: 0, g: 255, b: 0, a: alpha,
2866
+ }).saturate(-saturation).toRgbString();
2867
+ const fill3 = new Color({
2868
+ r: 0, g: 255, b: 255, a: alpha,
2869
+ }).saturate(-saturation).toRgbString();
2870
+ const fill4 = new Color({
2871
+ r: 0, g: 0, b: 255, a: alpha,
2872
+ }).saturate(-saturation).toRgbString();
2873
+ const fill5 = new Color({
2874
+ r: 255, g: 0, b: 255, a: alpha,
2875
+ }).saturate(-saturation).toRgbString();
2876
+ const fill6 = new Color({
2877
+ r: 255, g: 0, b: 0, a: alpha,
2878
+ }).saturate(-saturation).toRgbString();
2879
+ const fillGradient = `linear-gradient(to right,
2880
+ ${fill0} 0%, ${fill1} 16.67%, ${fill2} 33.33%, ${fill3} 50%,
2881
+ ${fill4} 66.67%, ${fill5} 83.33%, ${fill6} 100%)`;
2882
+ const lightGrad = `linear-gradient(rgba(255,255,255,${roundA}) 0%, rgba(255,255,255,0) 50%),
2883
+ linear-gradient(rgba(0,0,0,0) 50%, rgba(0,0,0,${roundA}) 100%)`;
2884
+
2885
+ setElementStyle(v1, { background: `${lightGrad},${fillGradient},${whiteGrad}` });
2886
+ const {
2887
+ r: gr, g: gg, b: gb,
2888
+ } = new Color({ r, g, b }).greyscale().toRgb();
2889
+
2890
+ setElementStyle(v2, {
2891
+ background: `linear-gradient(rgb(${r},${g},${b}) 0%, rgb(${gr},${gg},${gb}) 100%)`,
2892
+ });
2280
2893
  }
2894
+ setElementStyle(v3, {
2895
+ background: `linear-gradient(rgba(${r},${g},${b},1) 0%,rgba(${r},${g},${b},0) 100%)`,
2896
+ });
2281
2897
  }
2282
2898
 
2283
2899
  /**
2284
- * Handles the `focusout` listener of the `ColorPicker`.
2900
+ * The `ColorPicker` *focusout* event listener when open.
2285
2901
  * @param {FocusEvent} e
2286
2902
  * @this {ColorPicker}
2287
2903
  */
@@ -2293,7 +2909,7 @@
2293
2909
  }
2294
2910
 
2295
2911
  /**
2296
- * Handles the `focusout` listener of the `ColorPicker`.
2912
+ * The `ColorPicker` *keyup* event listener when open.
2297
2913
  * @param {KeyboardEvent} e
2298
2914
  * @this {ColorPicker}
2299
2915
  */
@@ -2305,14 +2921,13 @@
2305
2921
  }
2306
2922
 
2307
2923
  /**
2308
- * Handles the `ColorPicker` scroll listener when open.
2924
+ * The `ColorPicker` *scroll* event listener when open.
2309
2925
  * @param {Event} e
2310
2926
  * @this {ColorPicker}
2311
2927
  */
2312
2928
  handleScroll(e) {
2313
2929
  const self = this;
2314
- /** @type {*} */
2315
- const { activeElement } = document;
2930
+ const { activeElement } = getDocument(self.input);
2316
2931
 
2317
2932
  if ((isMobile && self.dragElement)
2318
2933
  || (activeElement && self.controlKnobs.includes(activeElement))) {
@@ -2324,22 +2939,51 @@
2324
2939
  }
2325
2940
 
2326
2941
  /**
2327
- * Handles all `ColorPicker` click listeners.
2942
+ * The `ColorPicker` keyboard event listener for menu navigation.
2328
2943
  * @param {KeyboardEvent} e
2329
2944
  * @this {ColorPicker}
2330
2945
  */
2331
2946
  menuKeyHandler(e) {
2332
2947
  const { target, code } = e;
2333
-
2334
- if ([keyArrowDown, keyArrowUp].includes(code)) {
2948
+ // @ts-ignore
2949
+ const { previousElementSibling, nextElementSibling, parentElement } = target;
2950
+ const isColorOptionsMenu = parentElement && hasClass(parentElement, 'color-options');
2951
+ const allSiblings = [...parentElement.children];
2952
+ const columnsCount = isColorOptionsMenu
2953
+ && getElementStyle(parentElement, 'grid-template-columns').split(' ').length;
2954
+ const currentIndex = allSiblings.indexOf(target);
2955
+ const previousElement = currentIndex > -1
2956
+ && columnsCount && allSiblings[currentIndex - columnsCount];
2957
+ const nextElement = currentIndex > -1
2958
+ && columnsCount && allSiblings[currentIndex + columnsCount];
2959
+
2960
+ if ([keyArrowDown, keyArrowUp, keySpace].includes(code)) {
2961
+ // prevent scroll when navigating the menu via arrow keys / Space
2335
2962
  e.preventDefault();
2336
- } else if ([keyEnter, keySpace].includes(code)) {
2963
+ }
2964
+ if (isColorOptionsMenu) {
2965
+ if (previousElement && code === keyArrowUp) {
2966
+ focus(previousElement);
2967
+ } else if (nextElement && code === keyArrowDown) {
2968
+ focus(nextElement);
2969
+ } else if (previousElementSibling && code === keyArrowLeft) {
2970
+ focus(previousElementSibling);
2971
+ } else if (nextElementSibling && code === keyArrowRight) {
2972
+ focus(nextElementSibling);
2973
+ }
2974
+ } else if (previousElementSibling && [keyArrowLeft, keyArrowUp].includes(code)) {
2975
+ focus(previousElementSibling);
2976
+ } else if (nextElementSibling && [keyArrowRight, keyArrowDown].includes(code)) {
2977
+ focus(nextElementSibling);
2978
+ }
2979
+
2980
+ if ([keyEnter, keySpace].includes(code)) {
2337
2981
  this.menuClickHandler({ target });
2338
2982
  }
2339
2983
  }
2340
2984
 
2341
2985
  /**
2342
- * Handles all `ColorPicker` click listeners.
2986
+ * The `ColorPicker` click event listener for the colour menu presets / defaults.
2343
2987
  * @param {Partial<Event>} e
2344
2988
  * @this {ColorPicker}
2345
2989
  */
@@ -2347,51 +2991,57 @@
2347
2991
  const self = this;
2348
2992
  /** @type {*} */
2349
2993
  const { target } = e;
2350
- const { format } = self;
2994
+ const { colorMenu } = self;
2351
2995
  const newOption = (getAttribute(target, 'data-value') || '').trim();
2352
- const currentActive = self.colorMenu.querySelector('li.active');
2353
- const newColor = nonColors.includes(newOption) ? 'white' : newOption;
2354
- self.color = new Color(newColor, { format });
2355
- self.setControlPositions();
2356
- self.setColorAppearence();
2357
- self.updateInputs(true);
2358
- self.updateControls();
2359
- self.updateVisuals();
2996
+ // invalidate for targets other than color options
2997
+ if (!newOption.length) return;
2998
+ const currentActive = querySelector('li.active', colorMenu);
2999
+ let newColor = nonColors.includes(newOption) ? 'white' : newOption;
3000
+ newColor = newOption === 'transparent' ? 'rgba(0,0,0,0)' : newOption;
2360
3001
 
2361
- if (currentActive) {
2362
- removeClass(currentActive, 'active');
2363
- removeAttribute(currentActive, ariaSelected);
2364
- }
3002
+ const {
3003
+ r, g, b, a,
3004
+ } = new Color(newColor);
3005
+
3006
+ ObjectAssign(self.color, {
3007
+ r, g, b, a,
3008
+ });
3009
+
3010
+ self.update();
2365
3011
 
2366
3012
  if (currentActive !== target) {
3013
+ if (currentActive) {
3014
+ removeClass(currentActive, 'active');
3015
+ removeAttribute(currentActive, ariaSelected);
3016
+ }
3017
+
2367
3018
  addClass(target, 'active');
2368
3019
  setAttribute(target, ariaSelected, 'true');
2369
3020
 
2370
3021
  if (nonColors.includes(newOption)) {
2371
3022
  self.value = newOption;
2372
- firePickerChange(self);
2373
3023
  }
3024
+ firePickerChange(self);
2374
3025
  }
2375
3026
  }
2376
3027
 
2377
3028
  /**
2378
- * Handles the `ColorPicker` touchstart / mousedown events listeners.
3029
+ * The `ColorPicker` *touchstart* / *mousedown* events listener for control knobs.
2379
3030
  * @param {TouchEvent} e
2380
3031
  * @this {ColorPicker}
2381
3032
  */
2382
3033
  pointerDown(e) {
2383
3034
  const self = this;
3035
+ /** @type {*} */
2384
3036
  const {
2385
- // @ts-ignore
2386
3037
  type, target, touches, pageX, pageY,
2387
3038
  } = e;
2388
- const { visuals, controlKnobs, format } = self;
3039
+ const { colorMenu, visuals, controlKnobs } = self;
2389
3040
  const [v1, v2, v3] = visuals;
2390
3041
  const [c1, c2, c3] = controlKnobs;
2391
- /** @type {HTMLCanvasElement} */
2392
- // @ts-ignore
2393
- const visual = target.tagName === 'canvas' // @ts-ignore
2394
- ? target : querySelector('canvas', target.parentElement);
3042
+ /** @type {HTMLElement} */
3043
+ const visual = hasClass(target, 'visual-control')
3044
+ ? target : querySelector('.visual-control', target.parentElement);
2395
3045
  const visualRect = getBoundingClientRect(visual);
2396
3046
  const X = type === 'touchstart' ? touches[0].pageX : pageX;
2397
3047
  const Y = type === 'touchstart' ? touches[0].pageY : pageY;
@@ -2400,42 +3050,53 @@
2400
3050
 
2401
3051
  if (target === v1 || target === c1) {
2402
3052
  self.dragElement = visual;
2403
- self.changeControl1({ offsetX, offsetY });
3053
+ self.changeControl1(offsetX, offsetY);
2404
3054
  } else if (target === v2 || target === c2) {
2405
3055
  self.dragElement = visual;
2406
- self.changeControl2({ offsetY });
2407
- } else if (format !== 'hex' && (target === v3 || target === c3)) {
3056
+ self.changeControl2(offsetY);
3057
+ } else if (target === v3 || target === c3) {
2408
3058
  self.dragElement = visual;
2409
- self.changeAlpha({ offsetY });
3059
+ self.changeAlpha(offsetY);
3060
+ }
3061
+
3062
+ if (colorMenu) {
3063
+ const currentActive = querySelector('li.active', colorMenu);
3064
+ if (currentActive) {
3065
+ removeClass(currentActive, 'active');
3066
+ removeAttribute(currentActive, ariaSelected);
3067
+ }
2410
3068
  }
2411
3069
  e.preventDefault();
2412
3070
  }
2413
3071
 
2414
3072
  /**
2415
- * Handles the `ColorPicker` touchend / mouseup events listeners.
3073
+ * The `ColorPicker` *touchend* / *mouseup* events listener for control knobs.
2416
3074
  * @param {TouchEvent} e
2417
3075
  * @this {ColorPicker}
2418
3076
  */
2419
3077
  pointerUp({ target }) {
2420
3078
  const self = this;
2421
- const selection = document.getSelection();
3079
+ const { parent } = self;
3080
+ const doc = getDocument(parent);
3081
+ const currentOpen = querySelector(`${colorPickerParentSelector}.open`, doc) !== null;
3082
+ const selection = doc.getSelection();
2422
3083
  // @ts-ignore
2423
3084
  if (!self.dragElement && !selection.toString().length
2424
3085
  // @ts-ignore
2425
- && !self.parent.contains(target)) {
2426
- self.hide();
3086
+ && !parent.contains(target)) {
3087
+ self.hide(currentOpen);
2427
3088
  }
2428
3089
 
2429
3090
  self.dragElement = null;
2430
3091
  }
2431
3092
 
2432
3093
  /**
2433
- * Handles the `ColorPicker` touchmove / mousemove events listeners.
3094
+ * The `ColorPicker` *touchmove* / *mousemove* events listener for control knobs.
2434
3095
  * @param {TouchEvent} e
2435
3096
  */
2436
3097
  pointerMove(e) {
2437
3098
  const self = this;
2438
- const { dragElement, visuals, format } = self;
3099
+ const { dragElement, visuals } = self;
2439
3100
  const [v1, v2, v3] = visuals;
2440
3101
  const {
2441
3102
  // @ts-ignore
@@ -2451,20 +3112,20 @@
2451
3112
  const offsetY = Y - window.pageYOffset - controlRect.top;
2452
3113
 
2453
3114
  if (dragElement === v1) {
2454
- self.changeControl1({ offsetX, offsetY });
3115
+ self.changeControl1(offsetX, offsetY);
2455
3116
  }
2456
3117
 
2457
3118
  if (dragElement === v2) {
2458
- self.changeControl2({ offsetY });
3119
+ self.changeControl2(offsetY);
2459
3120
  }
2460
3121
 
2461
- if (dragElement === v3 && format !== 'hex') {
2462
- self.changeAlpha({ offsetY });
3122
+ if (dragElement === v3) {
3123
+ self.changeAlpha(offsetY);
2463
3124
  }
2464
3125
  }
2465
3126
 
2466
3127
  /**
2467
- * Handles the `ColorPicker` events listeners associated with the color knobs.
3128
+ * The `ColorPicker` *keydown* event listener for control knobs.
2468
3129
  * @param {KeyboardEvent} e
2469
3130
  */
2470
3131
  handleKnobs(e) {
@@ -2475,54 +3136,64 @@
2475
3136
  if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
2476
3137
  e.preventDefault();
2477
3138
 
2478
- const { activeElement } = document;
2479
- const { controlKnobs } = self;
2480
- const currentKnob = controlKnobs.find((x) => x === activeElement);
3139
+ const { format, controlKnobs, visuals } = self;
3140
+ const { offsetWidth, offsetHeight } = visuals[0];
2481
3141
  const [c1, c2, c3] = controlKnobs;
3142
+ const { activeElement } = getDocument(c1);
3143
+ const currentKnob = controlKnobs.find((x) => x === activeElement);
3144
+ const yRatio = offsetHeight / (format === 'hsl' ? 100 : 360);
2482
3145
 
2483
3146
  if (currentKnob) {
2484
3147
  let offsetX = 0;
2485
3148
  let offsetY = 0;
3149
+
2486
3150
  if (target === c1) {
3151
+ const xRatio = offsetWidth / (format === 'hsl' ? 360 : 100);
3152
+
2487
3153
  if ([keyArrowLeft, keyArrowRight].includes(code)) {
2488
- self.controlPositions.c1x += code === keyArrowRight ? +1 : -1;
3154
+ self.controlPositions.c1x += code === keyArrowRight ? xRatio : -xRatio;
2489
3155
  } else if ([keyArrowUp, keyArrowDown].includes(code)) {
2490
- self.controlPositions.c1y += code === keyArrowDown ? +1 : -1;
3156
+ self.controlPositions.c1y += code === keyArrowDown ? yRatio : -yRatio;
2491
3157
  }
2492
3158
 
2493
3159
  offsetX = self.controlPositions.c1x;
2494
3160
  offsetY = self.controlPositions.c1y;
2495
- self.changeControl1({ offsetX, offsetY });
3161
+ self.changeControl1(offsetX, offsetY);
2496
3162
  } else if (target === c2) {
2497
- self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3163
+ self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code)
3164
+ ? yRatio
3165
+ : -yRatio;
3166
+
2498
3167
  offsetY = self.controlPositions.c2y;
2499
- self.changeControl2({ offsetY });
3168
+ self.changeControl2(offsetY);
2500
3169
  } else if (target === c3) {
2501
- self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3170
+ self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code)
3171
+ ? yRatio
3172
+ : -yRatio;
3173
+
2502
3174
  offsetY = self.controlPositions.c3y;
2503
- self.changeAlpha({ offsetY });
3175
+ self.changeAlpha(offsetY);
2504
3176
  }
2505
-
2506
- self.setColorAppearence();
2507
- self.updateInputs();
2508
- self.updateControls();
2509
- self.updateVisuals();
2510
3177
  self.handleScroll(e);
2511
3178
  }
2512
3179
  }
2513
3180
 
2514
- /** Handles the event listeners of the color form. */
3181
+ /** The event listener of the colour form inputs. */
2515
3182
  changeHandler() {
2516
3183
  const self = this;
2517
3184
  let colorSource;
2518
- /** @type {HTMLInputElement} */
2519
- // @ts-ignore
2520
- const { activeElement } = document;
2521
3185
  const {
2522
- inputs, format, value: currentValue, input,
3186
+ inputs, format, value: currentValue, input, controlPositions, visuals,
2523
3187
  } = self;
2524
- const [i1, i2, i3, i4] = inputs;
2525
- const isNonColorValue = self.includeNonColor && nonColors.includes(currentValue);
3188
+ /** @type {*} */
3189
+ const { activeElement } = getDocument(input);
3190
+ const { offsetHeight } = visuals[0];
3191
+ const [i1,,, i4] = inputs;
3192
+ const [v1, v2, v3, v4] = format === 'rgb'
3193
+ ? inputs.map((i) => parseFloat(i.value) / (i === i4 ? 100 : 1))
3194
+ : inputs.map((i) => parseFloat(i.value) / (i !== i1 ? 100 : 360));
3195
+ const isNonColorValue = self.hasNonColor && nonColors.includes(currentValue);
3196
+ const alpha = i4 ? v4 : (1 - controlPositions.c3y / offsetHeight);
2526
3197
 
2527
3198
  if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
2528
3199
  if (activeElement === input) {
@@ -2534,14 +3205,28 @@
2534
3205
  } else if (format === 'hex') {
2535
3206
  colorSource = i1.value;
2536
3207
  } else if (format === 'hsl') {
2537
- colorSource = `hsla(${i1.value},${i2.value}%,${i3.value}%,${i4.value})`;
3208
+ colorSource = {
3209
+ h: v1, s: v2, l: v3, a: alpha,
3210
+ };
3211
+ } else if (format === 'hwb') {
3212
+ colorSource = {
3213
+ h: v1, w: v2, b: v3, a: alpha,
3214
+ };
2538
3215
  } else {
2539
- colorSource = `rgba(${inputs.map((x) => x.value).join(',')})`;
3216
+ colorSource = {
3217
+ r: v1, g: v2, b: v3, a: alpha,
3218
+ };
2540
3219
  }
2541
3220
 
2542
- self.color = new Color(colorSource, { format });
3221
+ const {
3222
+ r, g, b, a,
3223
+ } = new Color(colorSource);
3224
+
3225
+ ObjectAssign(self.color, {
3226
+ r, g, b, a,
3227
+ });
2543
3228
  self.setControlPositions();
2544
- self.setColorAppearence();
3229
+ self.updateAppearance();
2545
3230
  self.updateInputs();
2546
3231
  self.updateControls();
2547
3232
  self.updateVisuals();
@@ -2558,49 +3243,57 @@
2558
3243
  * * `lightness` and `saturation` for HEX/RGB;
2559
3244
  * * `lightness` and `hue` for HSL.
2560
3245
  *
2561
- * @param {Record<string, number>} offsets
3246
+ * @param {number} X the X component of the offset
3247
+ * @param {number} Y the Y component of the offset
2562
3248
  */
2563
- changeControl1(offsets) {
3249
+ changeControl1(X, Y) {
2564
3250
  const self = this;
2565
3251
  let [offsetX, offsetY] = [0, 0];
2566
- const { offsetX: X, offsetY: Y } = offsets;
2567
3252
  const {
2568
- format, controlPositions,
2569
- height1, height2, height3, width1,
3253
+ format, controlPositions, visuals,
2570
3254
  } = self;
3255
+ const { offsetHeight, offsetWidth } = visuals[0];
2571
3256
 
2572
- if (X > width1) {
2573
- offsetX = width1;
2574
- } else if (X >= 0) {
2575
- offsetX = X;
2576
- }
3257
+ if (X > offsetWidth) offsetX = offsetWidth;
3258
+ else if (X >= 0) offsetX = X;
2577
3259
 
2578
- if (Y > height1) {
2579
- offsetY = height1;
2580
- } else if (Y >= 0) {
2581
- offsetY = Y;
2582
- }
3260
+ if (Y > offsetHeight) offsetY = offsetHeight;
3261
+ else if (Y >= 0) offsetY = Y;
3262
+
3263
+ const hue = format === 'hsl'
3264
+ ? offsetX / offsetWidth
3265
+ : controlPositions.c2y / offsetHeight;
2583
3266
 
2584
- const hue = format !== 'hsl'
2585
- ? Math.round((controlPositions.c2y / height2) * 360)
2586
- : Math.round((offsetX / width1) * 360);
3267
+ const saturation = format === 'hsl'
3268
+ ? 1 - controlPositions.c2y / offsetHeight
3269
+ : offsetX / offsetWidth;
2587
3270
 
2588
- const saturation = format !== 'hsl'
2589
- ? Math.round((offsetX / width1) * 100)
2590
- : Math.round((1 - controlPositions.c2y / height2) * 100);
3271
+ const lightness = 1 - offsetY / offsetHeight;
3272
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
2591
3273
 
2592
- const lightness = Math.round((1 - offsetY / height1) * 100);
2593
- const alpha = format !== 'hex' ? Math.round((1 - controlPositions.c3y / height3) * 100) / 100 : 1;
2594
- const tempFormat = format !== 'hsl' ? 'hsva' : 'hsla';
3274
+ const colorObject = format === 'hsl'
3275
+ ? {
3276
+ h: hue, s: saturation, l: lightness, a: alpha,
3277
+ }
3278
+ : {
3279
+ h: hue, s: saturation, v: lightness, a: alpha,
3280
+ };
2595
3281
 
2596
3282
  // new color
2597
- self.color = new Color(`${tempFormat}(${hue},${saturation}%,${lightness}%,${alpha})`, { format });
3283
+ const {
3284
+ r, g, b, a,
3285
+ } = new Color(colorObject);
3286
+
3287
+ ObjectAssign(self.color, {
3288
+ r, g, b, a,
3289
+ });
3290
+
2598
3291
  // new positions
2599
3292
  self.controlPositions.c1x = offsetX;
2600
3293
  self.controlPositions.c1y = offsetY;
2601
3294
 
2602
3295
  // update color picker
2603
- self.setColorAppearence();
3296
+ self.updateAppearance();
2604
3297
  self.updateInputs();
2605
3298
  self.updateControls();
2606
3299
  self.updateVisuals();
@@ -2608,37 +3301,52 @@
2608
3301
 
2609
3302
  /**
2610
3303
  * Updates `ColorPicker` second control:
2611
- * * `hue` for HEX/RGB;
3304
+ * * `hue` for HEX/RGB/HWB;
2612
3305
  * * `saturation` for HSL.
2613
3306
  *
2614
- * @param {Record<string, number>} offset
3307
+ * @param {number} Y the Y offset
2615
3308
  */
2616
- changeControl2(offset) {
3309
+ changeControl2(Y) {
2617
3310
  const self = this;
2618
- const { offsetY: Y } = offset;
2619
3311
  const {
2620
- format, width1, height1, height2, height3, controlPositions,
3312
+ format, controlPositions, visuals,
2621
3313
  } = self;
2622
- let offsetY = 0;
3314
+ const { offsetHeight, offsetWidth } = visuals[0];
2623
3315
 
2624
- if (Y > height2) {
2625
- offsetY = height2;
2626
- } else if (Y >= 0) {
2627
- offsetY = Y;
2628
- }
3316
+ let offsetY = 0;
2629
3317
 
2630
- const hue = format !== 'hsl' ? Math.round((offsetY / height2) * 360) : Math.round((controlPositions.c1x / width1) * 360);
2631
- const saturation = format !== 'hsl' ? Math.round((controlPositions.c1x / width1) * 100) : Math.round((1 - offsetY / height2) * 100);
2632
- const lightness = Math.round((1 - controlPositions.c1y / height1) * 100);
2633
- const alpha = format !== 'hex' ? Math.round((1 - controlPositions.c3y / height3) * 100) / 100 : 1;
2634
- const colorFormat = format !== 'hsl' ? 'hsva' : 'hsla';
3318
+ if (Y > offsetHeight) offsetY = offsetHeight;
3319
+ else if (Y >= 0) offsetY = Y;
3320
+
3321
+ const hue = format === 'hsl'
3322
+ ? controlPositions.c1x / offsetWidth
3323
+ : offsetY / offsetHeight;
3324
+ const saturation = format === 'hsl'
3325
+ ? 1 - offsetY / offsetHeight
3326
+ : controlPositions.c1x / offsetWidth;
3327
+ const lightness = 1 - controlPositions.c1y / offsetHeight;
3328
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
3329
+ const colorObject = format === 'hsl'
3330
+ ? {
3331
+ h: hue, s: saturation, l: lightness, a: alpha,
3332
+ }
3333
+ : {
3334
+ h: hue, s: saturation, v: lightness, a: alpha,
3335
+ };
2635
3336
 
2636
3337
  // new color
2637
- self.color = new Color(`${colorFormat}(${hue},${saturation}%,${lightness}%,${alpha})`, { format });
3338
+ const {
3339
+ r, g, b, a,
3340
+ } = new Color(colorObject);
3341
+
3342
+ ObjectAssign(self.color, {
3343
+ r, g, b, a,
3344
+ });
3345
+
2638
3346
  // new position
2639
3347
  self.controlPositions.c2y = offsetY;
2640
3348
  // update color picker
2641
- self.setColorAppearence();
3349
+ self.updateAppearance();
2642
3350
  self.updateInputs();
2643
3351
  self.updateControls();
2644
3352
  self.updateVisuals();
@@ -2646,95 +3354,108 @@
2646
3354
 
2647
3355
  /**
2648
3356
  * Updates `ColorPicker` last control,
2649
- * the `alpha` channel for RGB/HSL.
3357
+ * the `alpha` channel.
2650
3358
  *
2651
- * @param {Record<string, number>} offset
3359
+ * @param {number} Y
2652
3360
  */
2653
- changeAlpha(offset) {
3361
+ changeAlpha(Y) {
2654
3362
  const self = this;
2655
- const { height3 } = self;
2656
- const { offsetY: Y } = offset;
3363
+ const { visuals } = self;
3364
+ const { offsetHeight } = visuals[0];
2657
3365
  let offsetY = 0;
2658
3366
 
2659
- if (Y > height3) {
2660
- offsetY = height3;
2661
- } else if (Y >= 0) {
2662
- offsetY = Y;
2663
- }
3367
+ if (Y > offsetHeight) offsetY = offsetHeight;
3368
+ else if (Y >= 0) offsetY = Y;
2664
3369
 
2665
3370
  // update color alpha
2666
- const alpha = Math.round((1 - offsetY / height3) * 100);
2667
- self.color.setAlpha(alpha / 100);
3371
+ const alpha = 1 - offsetY / offsetHeight;
3372
+ self.color.setAlpha(alpha);
2668
3373
  // update position
2669
3374
  self.controlPositions.c3y = offsetY;
2670
3375
  // update color picker
3376
+ self.updateAppearance();
2671
3377
  self.updateInputs();
2672
3378
  self.updateControls();
2673
- // alpha?
2674
3379
  self.updateVisuals();
2675
3380
  }
2676
3381
 
2677
- /** Update opened dropdown position on scroll. */
3382
+ /**
3383
+ * Updates `ColorPicker` control positions on:
3384
+ * * initialization
3385
+ * * window resize
3386
+ */
3387
+ update() {
3388
+ const self = this;
3389
+ self.updateDropdownPosition();
3390
+ self.updateAppearance();
3391
+ self.setControlPositions();
3392
+ self.updateInputs(true);
3393
+ self.updateControls();
3394
+ self.updateVisuals();
3395
+ }
3396
+
3397
+ /** Updates the open dropdown position on *scroll* event. */
2678
3398
  updateDropdownPosition() {
2679
3399
  const self = this;
2680
3400
  const { input, colorPicker, colorMenu } = self;
2681
3401
  const elRect = getBoundingClientRect(input);
3402
+ const { top, bottom } = elRect;
2682
3403
  const { offsetHeight: elHeight } = input;
2683
- const windowHeight = document.documentElement.clientHeight;
2684
- const isPicker = classToggle(colorPicker, true);
3404
+ const windowHeight = getDocumentElement(input).clientHeight;
3405
+ const isPicker = hasClass(colorPicker, 'show');
2685
3406
  const dropdown = isPicker ? colorPicker : colorMenu;
3407
+ if (!dropdown) return;
2686
3408
  const { offsetHeight: dropHeight } = dropdown;
2687
- const distanceBottom = windowHeight - elRect.bottom;
2688
- const distanceTop = elRect.top;
2689
- const bottomExceed = elRect.top + dropHeight + elHeight > windowHeight; // show
2690
- const topExceed = elRect.top - dropHeight < 0; // show-top
2691
-
2692
- if (hasClass(dropdown, 'show') && distanceBottom < distanceTop && bottomExceed) {
2693
- removeClass(dropdown, 'show');
2694
- addClass(dropdown, 'show-top');
2695
- }
2696
- if (hasClass(dropdown, 'show-top') && distanceBottom > distanceTop && topExceed) {
2697
- removeClass(dropdown, 'show-top');
2698
- addClass(dropdown, 'show');
3409
+ const distanceBottom = windowHeight - bottom;
3410
+ const distanceTop = top;
3411
+ const bottomExceed = top + dropHeight + elHeight > windowHeight; // show
3412
+ const topExceed = top - dropHeight < 0; // show-top
3413
+
3414
+ if ((hasClass(dropdown, 'bottom') || !topExceed) && distanceBottom < distanceTop && bottomExceed) {
3415
+ removeClass(dropdown, 'bottom');
3416
+ addClass(dropdown, 'top');
3417
+ } else {
3418
+ removeClass(dropdown, 'top');
3419
+ addClass(dropdown, 'bottom');
2699
3420
  }
2700
3421
  }
2701
3422
 
2702
- /** Update control knobs' positions. */
3423
+ /** Updates control knobs' positions. */
2703
3424
  setControlPositions() {
2704
3425
  const self = this;
2705
3426
  const {
2706
- hsv, hsl, format, height1, height2, height3, width1,
3427
+ format, visuals, color, hsl, hsv,
2707
3428
  } = self;
3429
+ const { offsetHeight, offsetWidth } = visuals[0];
3430
+ const alpha = color.a;
2708
3431
  const hue = hsl.h;
3432
+
2709
3433
  const saturation = format !== 'hsl' ? hsv.s : hsl.s;
2710
3434
  const lightness = format !== 'hsl' ? hsv.v : hsl.l;
2711
- const alpha = hsv.a;
2712
-
2713
- self.controlPositions.c1x = format !== 'hsl' ? saturation * width1 : (hue / 360) * width1;
2714
- self.controlPositions.c1y = (1 - lightness) * height1;
2715
- self.controlPositions.c2y = format !== 'hsl' ? (hue / 360) * height2 : (1 - saturation) * height2;
2716
3435
 
2717
- if (format !== 'hex') {
2718
- self.controlPositions.c3y = (1 - alpha) * height3;
2719
- }
3436
+ self.controlPositions.c1x = format !== 'hsl' ? saturation * offsetWidth : hue * offsetWidth;
3437
+ self.controlPositions.c1y = (1 - lightness) * offsetHeight;
3438
+ self.controlPositions.c2y = format !== 'hsl' ? hue * offsetHeight : (1 - saturation) * offsetHeight;
3439
+ self.controlPositions.c3y = (1 - alpha) * offsetHeight;
2720
3440
  }
2721
3441
 
2722
- /** Update the visual appearance label. */
2723
- setColorAppearence() {
3442
+ /** Update the visual appearance label and control knob labels. */
3443
+ updateAppearance() {
2724
3444
  const self = this;
2725
3445
  const {
2726
- componentLabels, colorLabels, hsl, hsv, hex, format, knobLabels,
3446
+ componentLabels, colorLabels, color, parent,
3447
+ hsl, hsv, hex, format, controlKnobs,
2727
3448
  } = self;
2728
3449
  const {
2729
- lightnessLabel, saturationLabel, hueLabel, alphaLabel, appearanceLabel, hexLabel,
3450
+ appearanceLabel, hexLabel, valueLabel,
2730
3451
  } = componentLabels;
2731
- let { requiredLabel } = componentLabels;
2732
- const [knob1Lbl, knob2Lbl, knob3Lbl] = knobLabels;
2733
- const hue = Math.round(hsl.h);
2734
- const alpha = hsv.a;
3452
+ const { r, g, b } = color.toRgb();
3453
+ const [knob1, knob2, knob3] = controlKnobs;
3454
+ const hue = roundPart(hsl.h * 360);
3455
+ const alpha = color.a;
2735
3456
  const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
2736
- const saturation = Math.round(saturationSource * 100);
2737
- const lightness = Math.round(hsl.l * 100);
3457
+ const saturation = roundPart(saturationSource * 100);
3458
+ const lightness = roundPart(hsl.l * 100);
2738
3459
  const hsvl = hsv.v * 100;
2739
3460
  let colorName;
2740
3461
 
@@ -2770,99 +3491,118 @@
2770
3491
  colorName = colorLabels.pink;
2771
3492
  }
2772
3493
 
3494
+ let colorLabel = `${hexLabel} ${hex.split('').join(' ')}`;
3495
+
2773
3496
  if (format === 'hsl') {
2774
- knob1Lbl.innerText = `${hueLabel}: ${hue}°. ${lightnessLabel}: ${lightness}%`;
2775
- knob2Lbl.innerText = `${saturationLabel}: ${saturation}%`;
3497
+ colorLabel = `HSL: ${hue}°, ${saturation}%, ${lightness}%`;
3498
+ setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3499
+ setAttribute(knob1, ariaValueText, `${hue}° & ${lightness}%`);
3500
+ setAttribute(knob1, ariaValueNow, `${hue}`);
3501
+ setAttribute(knob2, ariaValueText, `${saturation}%`);
3502
+ setAttribute(knob2, ariaValueNow, `${saturation}`);
3503
+ } else if (format === 'hwb') {
3504
+ const { hwb } = self;
3505
+ const whiteness = roundPart(hwb.w * 100);
3506
+ const blackness = roundPart(hwb.b * 100);
3507
+ colorLabel = `HWB: ${hue}°, ${whiteness}%, ${blackness}%`;
3508
+ setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3509
+ setAttribute(knob1, ariaValueText, `${whiteness}% & ${blackness}%`);
3510
+ setAttribute(knob1, ariaValueNow, `${whiteness}`);
3511
+ setAttribute(knob2, ariaValueText, `${hue}%`);
3512
+ setAttribute(knob2, ariaValueNow, `${hue}`);
2776
3513
  } else {
2777
- knob1Lbl.innerText = `${lightnessLabel}: ${lightness}%. ${saturationLabel}: ${saturation}%`;
2778
- knob2Lbl.innerText = `${hueLabel}: ${hue}°`;
3514
+ colorLabel = format === 'rgb' ? `RGB: ${r}, ${g}, ${b}` : colorLabel;
3515
+ setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3516
+ setAttribute(knob1, ariaValueText, `${lightness}% & ${saturation}%`);
3517
+ setAttribute(knob1, ariaValueNow, `${lightness}`);
3518
+ setAttribute(knob2, ariaValueText, `${hue}°`);
3519
+ setAttribute(knob2, ariaValueNow, `${hue}`);
2779
3520
  }
2780
3521
 
2781
- if (format !== 'hex') {
2782
- const alphaValue = Math.round(alpha * 100);
2783
- knob3Lbl.innerText = `${alphaLabel}: ${alphaValue}%`;
2784
- }
3522
+ const alphaValue = roundPart(alpha * 100);
3523
+ setAttribute(knob3, ariaValueText, `${alphaValue}%`);
3524
+ setAttribute(knob3, ariaValueNow, `${alphaValue}`);
2785
3525
 
2786
- // update color labels
2787
- self.appearance.innerText = `${appearanceLabel}: ${colorName}.`;
2788
- const colorLabel = format === 'hex'
2789
- ? `${hexLabel} ${hex.split('').join(' ')}.`
2790
- : self.value.toUpperCase();
3526
+ // update the input backgroundColor
3527
+ const newColor = color.toString();
3528
+ setElementStyle(self.input, { backgroundColor: newColor });
2791
3529
 
2792
- if (self.label) {
2793
- const fieldLabel = self.label.innerText.replace('*', '').trim();
2794
- /** @type {HTMLSpanElement} */
2795
- // @ts-ignore
2796
- const [pickerBtnSpan] = self.pickerToggle.children;
2797
- requiredLabel = self.required ? ` ${requiredLabel}` : '';
2798
- pickerBtnSpan.innerText = `${fieldLabel}: ${colorLabel}${requiredLabel}`;
3530
+ // toggle dark/light classes will also style the placeholder
3531
+ // dark sets color white, light sets color black
3532
+ // isDark ? '#000' : '#fff'
3533
+ if (!self.isDark) {
3534
+ if (hasClass(parent, 'txt-dark')) removeClass(parent, 'txt-dark');
3535
+ if (!hasClass(parent, 'txt-light')) addClass(parent, 'txt-light');
3536
+ } else {
3537
+ if (hasClass(parent, 'txt-light')) removeClass(parent, 'txt-light');
3538
+ if (!hasClass(parent, 'txt-dark')) addClass(parent, 'txt-dark');
2799
3539
  }
2800
3540
  }
2801
3541
 
2802
- /** Updates the control knobs positions. */
3542
+ /** Updates the control knobs actual positions. */
2803
3543
  updateControls() {
2804
- const { format, controlKnobs, controlPositions } = this;
3544
+ const { controlKnobs, controlPositions } = this;
3545
+ let {
3546
+ c1x, c1y, c2y, c3y,
3547
+ } = controlPositions;
2805
3548
  const [control1, control2, control3] = controlKnobs;
2806
- control1.style.transform = `translate3d(${controlPositions.c1x - 3}px,${controlPositions.c1y - 3}px,0)`;
2807
- control2.style.transform = `translate3d(0,${controlPositions.c2y - 3}px,0)`;
3549
+ // round control positions
3550
+ [c1x, c1y, c2y, c3y] = [c1x, c1y, c2y, c3y].map(roundPart);
2808
3551
 
2809
- if (format !== 'hex') {
2810
- control3.style.transform = `translate3d(0,${controlPositions.c3y - 3}px,0)`;
2811
- }
3552
+ setElementStyle(control1, { transform: `translate3d(${c1x - 4}px,${c1y - 4}px,0)` });
3553
+ setElementStyle(control2, { transform: `translate3d(0,${c2y - 4}px,0)` });
3554
+ setElementStyle(control3, { transform: `translate3d(0,${c3y - 4}px,0)` });
2812
3555
  }
2813
3556
 
2814
3557
  /**
2815
- * Update all color form inputs.
3558
+ * Updates all color form inputs.
2816
3559
  * @param {boolean=} isPrevented when `true`, the component original event is prevented
2817
3560
  */
2818
3561
  updateInputs(isPrevented) {
2819
3562
  const self = this;
2820
3563
  const {
2821
- value: oldColor, rgb, hsl, hsv, format, parent, input, inputs,
3564
+ value: oldColor, format, inputs, color, hsl,
2822
3565
  } = self;
2823
3566
  const [i1, i2, i3, i4] = inputs;
2824
-
2825
- const alpha = hsl.a;
2826
- const hue = Math.round(hsl.h);
2827
- const saturation = Math.round(hsl.s * 100);
2828
- const lightSource = format === 'hsl' ? hsl.l : hsv.v;
2829
- const lightness = Math.round(lightSource * 100);
3567
+ const alpha = roundPart(color.a * 100);
3568
+ const hue = roundPart(hsl.h * 360);
2830
3569
  let newColor;
2831
3570
 
2832
3571
  if (format === 'hex') {
2833
- newColor = self.color.toHexString();
3572
+ newColor = self.color.toHexString(true);
2834
3573
  i1.value = self.hex;
2835
3574
  } else if (format === 'hsl') {
3575
+ const lightness = roundPart(hsl.l * 100);
3576
+ const saturation = roundPart(hsl.s * 100);
2836
3577
  newColor = self.color.toHslString();
2837
3578
  i1.value = `${hue}`;
2838
3579
  i2.value = `${saturation}`;
2839
3580
  i3.value = `${lightness}`;
2840
3581
  i4.value = `${alpha}`;
3582
+ } else if (format === 'hwb') {
3583
+ const { w, b } = self.hwb;
3584
+ const whiteness = roundPart(w * 100);
3585
+ const blackness = roundPart(b * 100);
3586
+
3587
+ newColor = self.color.toHwbString();
3588
+ i1.value = `${hue}`;
3589
+ i2.value = `${whiteness}`;
3590
+ i3.value = `${blackness}`;
3591
+ i4.value = `${alpha}`;
2841
3592
  } else if (format === 'rgb') {
3593
+ let { r, g, b } = self.rgb;
3594
+ [r, g, b] = [r, g, b].map(roundPart);
3595
+
2842
3596
  newColor = self.color.toRgbString();
2843
- i1.value = `${rgb.r}`;
2844
- i2.value = `${rgb.g}`;
2845
- i3.value = `${rgb.b}`;
3597
+ i1.value = `${r}`;
3598
+ i2.value = `${g}`;
3599
+ i3.value = `${b}`;
2846
3600
  i4.value = `${alpha}`;
2847
3601
  }
2848
3602
 
2849
3603
  // update the color value
2850
3604
  self.value = `${newColor}`;
2851
3605
 
2852
- // update the input backgroundColor
2853
- ObjectAssign(input.style, { backgroundColor: newColor });
2854
-
2855
- // toggle dark/light classes will also style the placeholder
2856
- // dark sets color white, light sets color black
2857
- // isDark ? '#000' : '#fff'
2858
- if (!self.isDark) {
2859
- if (hasClass(parent, 'dark')) removeClass(parent, 'dark');
2860
- if (!hasClass(parent, 'light')) addClass(parent, 'light');
2861
- } else {
2862
- if (hasClass(parent, 'light')) removeClass(parent, 'light');
2863
- if (!hasClass(parent, 'dark')) addClass(parent, 'dark');
2864
- }
2865
-
2866
3606
  // don't trigger the custom event unless it's really changed
2867
3607
  if (!isPrevented && newColor !== oldColor) {
2868
3608
  firePickerChange(self);
@@ -2870,14 +3610,15 @@
2870
3610
  }
2871
3611
 
2872
3612
  /**
2873
- * Handles the `Space` and `Enter` keys inputs.
3613
+ * The `Space` & `Enter` keys specific event listener.
3614
+ * Toggle visibility of the `ColorPicker` / the presets menu, showing one will hide the other.
2874
3615
  * @param {KeyboardEvent} e
2875
3616
  * @this {ColorPicker}
2876
3617
  */
2877
- keyHandler(e) {
3618
+ keyToggle(e) {
2878
3619
  const self = this;
2879
3620
  const { menuToggle } = self;
2880
- const { activeElement } = document;
3621
+ const { activeElement } = getDocument(menuToggle);
2881
3622
  const { code } = e;
2882
3623
 
2883
3624
  if ([keyEnter, keySpace].includes(code)) {
@@ -2900,80 +3641,79 @@
2900
3641
  togglePicker(e) {
2901
3642
  e.preventDefault();
2902
3643
  const self = this;
2903
- const pickerIsOpen = classToggle(self.colorPicker, true);
3644
+ const { colorPicker } = self;
2904
3645
 
2905
- if (self.isOpen && pickerIsOpen) {
3646
+ if (self.isOpen && hasClass(colorPicker, 'show')) {
2906
3647
  self.hide(true);
2907
3648
  } else {
2908
- self.showPicker();
3649
+ showDropdown(self, colorPicker);
2909
3650
  }
2910
3651
  }
2911
3652
 
2912
3653
  /** Shows the `ColorPicker` dropdown. */
2913
3654
  showPicker() {
2914
3655
  const self = this;
2915
- classToggle(self.colorMenu);
2916
- addClass(self.colorPicker, 'show');
2917
- self.input.focus();
2918
- self.show();
2919
- setAttribute(self.pickerToggle, ariaExpanded, 'true');
3656
+ const { colorPicker } = self;
3657
+
3658
+ if (!['top', 'bottom'].some((c) => hasClass(colorPicker, c))) {
3659
+ showDropdown(self, colorPicker);
3660
+ }
2920
3661
  }
2921
3662
 
2922
3663
  /** Toggles the visibility of the `ColorPicker` presets menu. */
2923
3664
  toggleMenu() {
2924
3665
  const self = this;
2925
- const menuIsOpen = classToggle(self.colorMenu, true);
3666
+ const { colorMenu } = self;
2926
3667
 
2927
- if (self.isOpen && menuIsOpen) {
3668
+ if (self.isOpen && hasClass(colorMenu, 'show')) {
2928
3669
  self.hide(true);
2929
3670
  } else {
2930
- showMenu(self);
2931
- }
2932
- }
2933
-
2934
- /** Show the dropdown. */
2935
- show() {
2936
- const self = this;
2937
- if (!self.isOpen) {
2938
- addClass(self.parent, 'open');
2939
- toggleEventsOnShown(self, true);
2940
- self.updateDropdownPosition();
2941
- self.isOpen = true;
3671
+ showDropdown(self, colorMenu);
2942
3672
  }
2943
3673
  }
2944
3674
 
2945
3675
  /**
2946
- * Hides the currently opened dropdown.
3676
+ * Hides the currently open `ColorPicker` dropdown.
2947
3677
  * @param {boolean=} focusPrevented
2948
3678
  */
2949
3679
  hide(focusPrevented) {
2950
3680
  const self = this;
2951
3681
  if (self.isOpen) {
2952
- const { pickerToggle, colorMenu } = self;
2953
- toggleEventsOnShown(self);
2954
-
2955
- removeClass(self.parent, 'open');
2956
-
2957
- classToggle(self.colorPicker);
2958
- setAttribute(pickerToggle, ariaExpanded, 'false');
2959
-
2960
- if (colorMenu) {
2961
- classToggle(colorMenu);
2962
- setAttribute(self.menuToggle, ariaExpanded, 'false');
3682
+ const {
3683
+ pickerToggle, menuToggle, colorPicker, colorMenu, parent, input,
3684
+ } = self;
3685
+ const openPicker = hasClass(colorPicker, 'show');
3686
+ const openDropdown = openPicker ? colorPicker : colorMenu;
3687
+ const relatedBtn = openPicker ? pickerToggle : menuToggle;
3688
+ const animationDuration = openDropdown && getElementTransitionDuration(openDropdown);
3689
+
3690
+ if (openDropdown) {
3691
+ removeClass(openDropdown, 'show');
3692
+ setAttribute(relatedBtn, ariaExpanded, 'false');
3693
+ setTimeout(() => {
3694
+ removePosition(openDropdown);
3695
+ if (!querySelector('.show', parent)) {
3696
+ removeClass(parent, 'open');
3697
+ toggleEventsOnShown(self);
3698
+ self.isOpen = false;
3699
+ }
3700
+ }, animationDuration);
2963
3701
  }
2964
3702
 
2965
3703
  if (!self.isValid) {
2966
3704
  self.value = self.color.toString();
2967
3705
  }
2968
-
2969
- self.isOpen = false;
2970
-
2971
3706
  if (!focusPrevented) {
2972
- pickerToggle.focus();
3707
+ focus(pickerToggle);
3708
+ }
3709
+ setAttribute(input, tabIndex, '-1');
3710
+ if (menuToggle) {
3711
+ setAttribute(menuToggle, tabIndex, '-1');
2973
3712
  }
2974
3713
  }
2975
3714
  }
2976
3715
 
3716
+ /** Removes `ColorPicker` from target `<input>`. */
2977
3717
  dispose() {
2978
3718
  const self = this;
2979
3719
  const { input, parent } = self;
@@ -2982,25 +3722,29 @@
2982
3722
  [...parent.children].forEach((el) => {
2983
3723
  if (el !== input) el.remove();
2984
3724
  });
3725
+
3726
+ removeAttribute(input, tabIndex);
3727
+ setElementStyle(input, { backgroundColor: '' });
3728
+
3729
+ ['txt-light', 'txt-dark'].forEach((c) => removeClass(parent, c));
2985
3730
  Data.remove(input, colorPickerString);
2986
3731
  }
2987
3732
  }
2988
3733
 
2989
3734
  ObjectAssign(ColorPicker, {
2990
3735
  Color,
3736
+ ColorPalette,
3737
+ Version,
2991
3738
  getInstance: getColorPickerInstance,
2992
3739
  init: initColorPicker,
2993
3740
  selector: colorPickerSelector,
3741
+ // utils important for render
3742
+ roundPart,
3743
+ setElementStyle,
3744
+ setAttribute,
3745
+ getBoundingClientRect,
2994
3746
  });
2995
3747
 
2996
- function initCallBack() {
2997
- const { init, selector } = ColorPicker;
2998
- [...querySelectorAll(selector)].forEach(init);
2999
- }
3000
-
3001
- if (document.body) initCallBack();
3002
- else document.addEventListener('DOMContentLoaded', initCallBack, { once: true });
3003
-
3004
3748
  return ColorPicker;
3005
3749
 
3006
3750
  })));