@thednp/color-picker 0.0.1-alpha1 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
  })));