@thednp/color-picker 0.0.1-alpha1 → 0.0.1-alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +40 -19
  3. package/dist/css/color-picker.css +481 -337
  4. package/dist/css/color-picker.min.css +2 -0
  5. package/dist/css/color-picker.rtl.css +506 -0
  6. package/dist/css/color-picker.rtl.min.css +2 -0
  7. package/dist/js/color-picker-element-esm.js +3810 -2
  8. package/dist/js/color-picker-element-esm.min.js +2 -0
  9. package/dist/js/color-picker-element.js +2009 -1242
  10. package/dist/js/color-picker-element.min.js +2 -2
  11. package/dist/js/color-picker-esm.js +3704 -0
  12. package/dist/js/color-picker-esm.min.js +2 -0
  13. package/dist/js/color-picker.js +1962 -1256
  14. package/dist/js/color-picker.min.js +2 -2
  15. package/package.json +18 -9
  16. package/src/js/color-palette.js +62 -0
  17. package/src/js/color-picker-element.js +55 -13
  18. package/src/js/color-picker.js +686 -595
  19. package/src/js/color.js +615 -349
  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 +27 -19
  25. package/src/js/util/getColorMenu.js +95 -0
  26. package/src/js/util/isValidJSON.js +13 -0
  27. package/src/js/util/nonColors.js +5 -0
  28. package/src/js/util/templates.js +1 -0
  29. package/src/scss/color-picker.rtl.scss +23 -0
  30. package/src/scss/color-picker.scss +430 -0
  31. package/types/cp.d.ts +263 -160
  32. package/types/index.d.ts +9 -2
  33. package/types/source/source.ts +2 -1
  34. package/types/source/types.d.ts +28 -5
  35. package/dist/js/color-picker.esm.js +0 -2998
  36. package/dist/js/color-picker.esm.min.js +0 -2
  37. package/src/js/util/getColorControl.js +0 -49
  38. 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.1alpha2 (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 multiple uses of `HTMLElement.style.propertyName` method.
672
+ * @param {HTMLElement | Element} element target element
673
+ * @param {Partial<CSSStyleDeclaration>} styles attribute value
674
+ */
675
+ // @ts-ignore
676
+ const setElementStyle = (element, styles) => { ObjectAssign(element.style, styles); };
677
+
678
+ /**
679
+ * Shortcut for `HTMLElement.getAttribute()` method.
680
+ * @param {HTMLElement | Element} element target element
681
+ * @param {string} attribute attribute name
682
+ * @returns {string?} attribute value
683
+ */
684
+ const getAttribute = (element, attribute) => element.getAttribute(attribute);
685
+
686
+ /**
687
+ * The raw value or a given component option.
688
+ *
689
+ * @typedef {string | HTMLElement | Function | number | boolean | null} niceValue
690
+ */
691
+
692
+ /**
693
+ * Utility to normalize component options
694
+ *
695
+ * @param {any} value the input value
696
+ * @return {niceValue} the normalized value
697
+ */
698
+ function normalizeValue(value) {
699
+ if (value === 'true') { // boolean
700
+ return true;
701
+ }
702
+
703
+ if (value === 'false') { // boolean
704
+ return false;
705
+ }
706
+
707
+ if (!Number.isNaN(+value)) { // number
708
+ return +value;
709
+ }
710
+
711
+ if (value === '' || value === 'null') { // null
712
+ return null;
713
+ }
714
+
715
+ // string / function / HTMLElement / object
716
+ return value;
717
+ }
718
+
719
+ /**
720
+ * Shortcut for `Object.keys()` static method.
721
+ * @param {Record<string, any>} obj a target object
722
+ * @returns {string[]}
723
+ */
724
+ const ObjectKeys = (obj) => Object.keys(obj);
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,164 @@
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
+ });
942
+ // }
606
943
  colorForm.append(cInputLabel, cInput);
607
944
  });
608
945
  return colorForm;
609
946
  }
610
947
 
611
- /**
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}
619
- */
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({
948
+ /**
949
+ * A global namespace for aria-label.
950
+ * @type {string}
951
+ */
952
+ const ariaLabel = 'aria-label';
953
+
954
+ /**
955
+ * A global namespace for aria-valuemin.
956
+ * @type {string}
957
+ */
958
+ const ariaValueMin = 'aria-valuemin';
959
+
960
+ /**
961
+ * A global namespace for aria-valuemax.
962
+ * @type {string}
963
+ */
964
+ const ariaValueMax = 'aria-valuemax';
965
+
966
+ /**
967
+ * Returns all color controls for `ColorPicker`.
968
+ *
969
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
970
+ * @returns {HTMLElement | Element} color controls
971
+ */
972
+ function getColorControls(self) {
973
+ const { format, componentLabels } = self;
974
+ const {
975
+ hueLabel, alphaLabel, lightnessLabel, saturationLabel,
976
+ whitenessLabel, blacknessLabel,
977
+ } = componentLabels;
978
+
979
+ const max1 = format === 'hsl' ? 360 : 100;
980
+ const max2 = format === 'hsl' ? 100 : 360;
981
+ const max3 = 100;
982
+
983
+ let ctrl1Label = format === 'hsl'
984
+ ? `${hueLabel} & ${lightnessLabel}`
985
+ : `${lightnessLabel} & ${saturationLabel}`;
986
+
987
+ ctrl1Label = format === 'hwb'
988
+ ? `${whitenessLabel} & ${blacknessLabel}`
989
+ : ctrl1Label;
990
+
991
+ const ctrl2Label = format === 'hsl'
992
+ ? `${saturationLabel}`
993
+ : `${hueLabel}`;
994
+
995
+ const colorControls = createElement({
624
996
  tagName: 'div',
625
- className: 'color-control',
997
+ className: `color-controls ${format}`,
626
998
  });
627
- setAttribute(control, 'role', 'presentation');
628
999
 
629
- control.append(
630
- createElement({
631
- id: labelID,
632
- tagName: 'label',
633
- className: `color-label ${vHidden}`,
1000
+ const colorPointer = 'color-pointer';
1001
+ const colorSlider = 'color-slider';
1002
+
1003
+ const controls = [
1004
+ {
1005
+ i: 1,
1006
+ c: colorPointer,
1007
+ l: ctrl1Label,
1008
+ min: 0,
1009
+ max: max1,
1010
+ },
1011
+ {
1012
+ i: 2,
1013
+ c: colorSlider,
1014
+ l: ctrl2Label,
1015
+ min: 0,
1016
+ max: max2,
1017
+ },
1018
+ {
1019
+ i: 3,
1020
+ c: colorSlider,
1021
+ l: alphaLabel,
1022
+ min: 0,
1023
+ max: max3,
1024
+ },
1025
+ ];
1026
+
1027
+ controls.forEach((template) => {
1028
+ const {
1029
+ i, c, l, min, max,
1030
+ } = template;
1031
+ // const hidden = i === 2 && format === 'hwb' ? ' v-hidden' : '';
1032
+ const control = createElement({
1033
+ tagName: 'div',
1034
+ // className: `color-control${hidden}`,
1035
+ className: 'color-control',
1036
+ });
1037
+ setAttribute(control, 'role', 'presentation');
1038
+
1039
+ control.append(
1040
+ createElement({
1041
+ tagName: 'div',
1042
+ className: `visual-control visual-control${i}`,
1043
+ }),
1044
+ );
1045
+
1046
+ const knob = createElement({
1047
+ tagName: 'div',
1048
+ className: `${c} knob`,
634
1049
  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`,
1050
+ });
1051
+
1052
+ setAttribute(knob, ariaLabel, l);
1053
+ setAttribute(knob, 'role', 'slider');
1054
+ setAttribute(knob, 'tabindex', '0');
1055
+ setAttribute(knob, ariaValueMin, `${min}`);
1056
+ setAttribute(knob, ariaValueMax, `${max}`);
1057
+ control.append(knob);
1058
+ colorControls.append(control);
648
1059
  });
649
- setAttribute(knob, ariaLabelledBy, labelledby || labelID);
650
- setAttribute(knob, 'tabindex', '0');
651
- control.append(knob);
652
- return control;
1060
+
1061
+ return colorControls;
653
1062
  }
654
1063
 
655
1064
  /**
@@ -662,212 +1071,40 @@
662
1071
  return getDocument(node).head;
663
1072
  }
664
1073
 
665
- /**
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
675
- */
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] : '';
682
- }
683
-
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); };
1074
+ // Color supported formats
1075
+ const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsb', 'hwb'];
691
1076
 
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
- ];
1077
+ // Hue angles
1078
+ const ANGLES = 'deg|rad|grad|turn';
847
1079
 
848
1080
  // <http://www.w3.org/TR/css3-values/#integers>
849
1081
  const CSS_INTEGER = '[-\\+]?\\d+%?';
850
1082
 
1083
+ // Include CSS3 Module
851
1084
  // <http://www.w3.org/TR/css3-values/#number-value>
852
1085
  const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
853
1086
 
1087
+ // Include CSS4 Module Hue degrees unit
1088
+ // <https://www.w3.org/TR/css3-values/#angle-value>
1089
+ const CSS_ANGLE = `[-\\+]?\\d*\\.?\\d+(?:${ANGLES})?`;
1090
+
854
1091
  // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
855
1092
  const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
856
1093
 
1094
+ // Add angles to the mix
1095
+ const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
1096
+
857
1097
  // Actual matching.
858
1098
  // Parentheses and commas are optional, but not required.
859
1099
  // 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*\\)?`;
1100
+ const PERMISSIVE_MATCH = `[\\s|\\(]+(${CSS_UNIT2})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s|\\/\\s]*(${CSS_UNIT})?\\s*\\)?`;
862
1101
 
863
1102
  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}`),
1103
+ CSS_UNIT: new RegExp(CSS_UNIT2),
1104
+ hwb: new RegExp(`hwb${PERMISSIVE_MATCH}`),
1105
+ rgb: new RegExp(`rgb(?:a)?${PERMISSIVE_MATCH}`),
1106
+ hsl: new RegExp(`hsl(?:a)?${PERMISSIVE_MATCH}`),
1107
+ hsv: new RegExp(`hsv(?:a)?${PERMISSIVE_MATCH}`),
871
1108
  hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
872
1109
  hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
873
1110
  hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
@@ -877,27 +1114,46 @@
877
1114
  /**
878
1115
  * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
879
1116
  * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
880
- * @param {string} n
881
- * @returns {boolean}
1117
+ * @param {string} n testing number
1118
+ * @returns {boolean} the query result
882
1119
  */
883
1120
  function isOnePointZero(n) {
884
- return typeof n === 'string' && n.includes('.') && parseFloat(n) === 1;
1121
+ return `${n}`.includes('.') && parseFloat(n) === 1;
885
1122
  }
886
1123
 
887
1124
  /**
888
1125
  * Check to see if string passed in is a percentage
889
- * @param {string} n
890
- * @returns {boolean}
1126
+ * @param {string} n testing number
1127
+ * @returns {boolean} the query result
891
1128
  */
892
1129
  function isPercentage(n) {
893
- return typeof n === 'string' && n.includes('%');
1130
+ return `${n}`.includes('%');
1131
+ }
1132
+
1133
+ /**
1134
+ * Check to see if string passed in is an angle
1135
+ * @param {string} n testing string
1136
+ * @returns {boolean} the query result
1137
+ */
1138
+ function isAngle(n) {
1139
+ return ANGLES.split('|').some((a) => `${n}`.includes(a));
1140
+ }
1141
+
1142
+ /**
1143
+ * Check to see if string passed is a web safe colour.
1144
+ * @param {string} color a colour name, EG: *red*
1145
+ * @returns {boolean} the query result
1146
+ */
1147
+ function isColorName(color) {
1148
+ return !['#', ...COLOR_FORMAT].some((s) => color.includes(s))
1149
+ && !/[0-9]/.test(color);
894
1150
  }
895
1151
 
896
1152
  /**
897
1153
  * Check to see if it looks like a CSS unit
898
1154
  * (see `matchers` above for definition).
899
- * @param {string | number} color
900
- * @returns {boolean}
1155
+ * @param {string | number} color testing value
1156
+ * @returns {boolean} the query result
901
1157
  */
902
1158
  function isValidCSSUnit(color) {
903
1159
  return Boolean(matchers.CSS_UNIT.exec(String(color)));
@@ -905,22 +1161,24 @@
905
1161
 
906
1162
  /**
907
1163
  * Take input from [0, n] and return it as [0, 1]
908
- * @param {*} n
909
- * @param {number} max
910
- * @returns {number}
1164
+ * @param {*} N the input number
1165
+ * @param {number} max the number maximum value
1166
+ * @returns {number} the number in [0, 1] value range
911
1167
  */
912
- function bound01(n, max) {
913
- let N = n;
914
- if (isOnePointZero(n)) N = '100%';
1168
+ function bound01(N, max) {
1169
+ let n = N;
1170
+ if (isOnePointZero(n)) n = '100%';
915
1171
 
916
- N = max === 360 ? N : Math.min(max, Math.max(0, parseFloat(N)));
1172
+ n = max === 360 ? n : Math.min(max, Math.max(0, parseFloat(n)));
1173
+
1174
+ // Handle hue angles
1175
+ if (isAngle(N)) n = N.replace(new RegExp(ANGLES), '');
917
1176
 
918
1177
  // Automatically convert percentage into number
919
- if (isPercentage(N)) {
920
- N = parseInt(String(N * max), 10) / 100;
921
- }
1178
+ if (isPercentage(n)) n = parseInt(String(n * max), 10) / 100;
1179
+
922
1180
  // Handle floating point rounding errors
923
- if (Math.abs(N - max) < 0.000001) {
1181
+ if (Math.abs(n - max) < 0.000001) {
924
1182
  return 1;
925
1183
  }
926
1184
  // Convert into [0, 1] range if it isn't already
@@ -928,23 +1186,22 @@
928
1186
  // If n is a hue given in degrees,
929
1187
  // wrap around out-of-range values into [0, 360] range
930
1188
  // then convert into [0, 1].
931
- N = (N < 0 ? (N % max) + max : N % max) / parseFloat(String(max));
1189
+ n = (n < 0 ? (n % max) + max : n % max) / parseFloat(String(max));
932
1190
  } else {
933
1191
  // If n not a hue given in degrees
934
1192
  // Convert into [0, 1] range if it isn't already.
935
- N = (N % max) / parseFloat(String(max));
1193
+ n = (n % max) / parseFloat(String(max));
936
1194
  }
937
- return N;
1195
+ return n;
938
1196
  }
939
1197
 
940
1198
  /**
941
1199
  * Return a valid alpha value [0,1] with all invalid values being set to 1.
942
- * @param {string | number} a
943
- * @returns {number}
1200
+ * @param {string | number} a transparency value
1201
+ * @returns {number} a transparency value in the [0, 1] range
944
1202
  */
945
1203
  function boundAlpha(a) {
946
- // @ts-ignore
947
- let na = parseFloat(a);
1204
+ let na = parseFloat(`${a}`);
948
1205
 
949
1206
  if (Number.isNaN(na) || na < 0 || na > 1) {
950
1207
  na = 1;
@@ -954,12 +1211,12 @@
954
1211
  }
955
1212
 
956
1213
  /**
957
- * Force a number between 0 and 1
958
- * @param {number} val
959
- * @returns {number}
1214
+ * Force a number between 0 and 1.
1215
+ * @param {number} v the float number
1216
+ * @returns {number} - the resulting number
960
1217
  */
961
- function clamp01(val) {
962
- return Math.min(1, Math.max(0, val));
1218
+ function clamp01(v) {
1219
+ return Math.min(1, Math.max(0, v));
963
1220
  }
964
1221
 
965
1222
  /**
@@ -967,7 +1224,7 @@
967
1224
  * @param {string} name
968
1225
  * @returns {string}
969
1226
  */
970
- function getHexFromColorName(name) {
1227
+ function getRGBFromName(name) {
971
1228
  const documentHead = getDocumentHead();
972
1229
  setElementStyle(documentHead, { color: name });
973
1230
  const colorName = getElementStyle(documentHead, 'color');
@@ -976,59 +1233,53 @@
976
1233
  }
977
1234
 
978
1235
  /**
979
- * Replace a decimal with it's percentage value
980
- * @param {number | string} n
981
- * @return {string | number}
1236
+ * Converts a decimal value to hexadecimal.
1237
+ * @param {number} d the input number
1238
+ * @returns {string} - the hexadecimal value
982
1239
  */
983
- function convertToPercentage(n) {
984
- if (n <= 1) {
985
- return `${Number(n) * 100}%`;
986
- }
987
- return n;
1240
+ function convertDecimalToHex(d) {
1241
+ return Math.round(d * 255).toString(16);
988
1242
  }
989
1243
 
990
1244
  /**
991
- * Force a hex value to have 2 characters
992
- * @param {string} c
993
- * @returns {string}
1245
+ * Converts a hexadecimal value to decimal.
1246
+ * @param {string} h hexadecimal value
1247
+ * @returns {number} number in decimal format
994
1248
  */
995
- function pad2(c) {
996
- return c.length === 1 ? `0${c}` : String(c);
1249
+ function convertHexToDecimal(h) {
1250
+ return parseIntFromHex(h) / 255;
997
1251
  }
998
1252
 
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
1253
  /**
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}
1254
+ * Converts a base-16 hexadecimal value into a base-10 integer.
1255
+ * @param {string} val
1256
+ * @returns {number}
1010
1257
  */
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
- };
1258
+ function parseIntFromHex(val) {
1259
+ return parseInt(val, 16);
1260
+ }
1261
+
1262
+ /**
1263
+ * Force a hexadecimal value to have 2 characters.
1264
+ * @param {string} c string with [0-9A-F] ranged values
1265
+ * @returns {string} 0 => 00, a => 0a
1266
+ */
1267
+ function pad2(c) {
1268
+ return c.length === 1 ? `0${c}` : String(c);
1017
1269
  }
1018
1270
 
1019
1271
  /**
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}
1272
+ * Converts an RGB colour value to HSL.
1273
+ *
1274
+ * @param {number} R Red component [0, 255]
1275
+ * @param {number} G Green component [0, 255]
1276
+ * @param {number} B Blue component [0, 255]
1277
+ * @returns {CP.HSL} {h,s,l} object with [0, 1] ranged values
1027
1278
  */
1028
1279
  function rgbToHsl(R, G, B) {
1029
- const r = bound01(R, 255);
1030
- const g = bound01(G, 255);
1031
- const b = bound01(B, 255);
1280
+ const r = R / 255;
1281
+ const g = G / 255;
1282
+ const b = B / 255;
1032
1283
  const max = Math.max(r, g, b);
1033
1284
  const min = Math.min(r, g, b);
1034
1285
  let h = 0;
@@ -1058,50 +1309,95 @@
1058
1309
 
1059
1310
  /**
1060
1311
  * Returns a normalized RGB component value.
1061
- * @param {number} P
1062
- * @param {number} Q
1063
- * @param {number} T
1312
+ * @param {number} p
1313
+ * @param {number} q
1314
+ * @param {number} t
1064
1315
  * @returns {number}
1065
1316
  */
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
- }
1317
+ function hueToRgb(p, q, t) {
1318
+ let T = t;
1319
+ if (T < 0) T += 1;
1320
+ if (T > 1) T -= 1;
1321
+ if (T < 1 / 6) return p + (q - p) * (6 * T);
1322
+ if (T < 1 / 2) return q;
1323
+ if (T < 2 / 3) return p + (q - p) * (2 / 3 - T) * 6;
1085
1324
  return p;
1086
1325
  }
1087
1326
 
1327
+ /**
1328
+ * Returns an HWB colour object from an RGB colour object.
1329
+ * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
1330
+ * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
1331
+ *
1332
+ * @param {number} R Red component [0, 255]
1333
+ * @param {number} G Green [0, 255]
1334
+ * @param {number} B Blue [0, 255]
1335
+ * @return {CP.HWB} {h,w,b} object with [0, 1] ranged values
1336
+ */
1337
+ function rgbToHwb(R, G, B) {
1338
+ const r = R / 255;
1339
+ const g = G / 255;
1340
+ const b = B / 255;
1341
+
1342
+ let f = 0;
1343
+ let i = 0;
1344
+ const whiteness = Math.min(r, g, b);
1345
+ const max = Math.max(r, g, b);
1346
+ const black = 1 - max;
1347
+
1348
+ if (max === whiteness) return { h: 0, w: whiteness, b: black };
1349
+ if (r === whiteness) {
1350
+ f = g - b;
1351
+ i = 3;
1352
+ } else {
1353
+ f = g === whiteness ? b - r : r - g;
1354
+ i = g === whiteness ? 5 : 1;
1355
+ }
1356
+
1357
+ const h = (i - f / (max - whiteness)) / 6;
1358
+ return {
1359
+ h: h === 1 ? 0 : h,
1360
+ w: whiteness,
1361
+ b: black,
1362
+ };
1363
+ }
1364
+
1365
+ /**
1366
+ * Returns an RGB colour object from an HWB colour.
1367
+ *
1368
+ * @param {number} H Hue Angle [0, 1]
1369
+ * @param {number} W Whiteness [0, 1]
1370
+ * @param {number} B Blackness [0, 1]
1371
+ * @return {CP.RGB} {r,g,b} object with [0, 255] ranged values
1372
+ *
1373
+ * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
1374
+ * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
1375
+ */
1376
+ function hwbToRgb(H, W, B) {
1377
+ if (W + B >= 1) {
1378
+ const gray = (W / (W + B)) * 255;
1379
+ return { r: gray, g: gray, b: gray };
1380
+ }
1381
+ let { r, g, b } = hslToRgb(H, 1, 0.5);
1382
+ [r, g, b] = [r, g, b]
1383
+ .map((v) => (v / 255) * (1 - W - B) + W)
1384
+ .map((v) => v * 255);
1385
+
1386
+ return { r, g, b };
1387
+ }
1388
+
1088
1389
  /**
1089
1390
  * Converts an HSL colour value to RGB.
1090
1391
  *
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) {
1392
+ * @param {number} h Hue Angle [0, 1]
1393
+ * @param {number} s Saturation [0, 1]
1394
+ * @param {number} l Lightness Angle [0, 1]
1395
+ * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1396
+ */
1397
+ function hslToRgb(h, s, l) {
1099
1398
  let r = 0;
1100
1399
  let g = 0;
1101
1400
  let b = 0;
1102
- const h = bound01(H, 360);
1103
- const s = bound01(S, 100);
1104
- const l = bound01(L, 100);
1105
1401
 
1106
1402
  if (s === 0) {
1107
1403
  // achromatic
@@ -1111,27 +1407,27 @@
1111
1407
  } else {
1112
1408
  const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1113
1409
  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);
1410
+ r = hueToRgb(p, q, h + 1 / 3);
1411
+ g = hueToRgb(p, q, h);
1412
+ b = hueToRgb(p, q, h - 1 / 3);
1117
1413
  }
1118
- return { r: r * 255, g: g * 255, b: b * 255 };
1414
+ [r, g, b] = [r, g, b].map((x) => x * 255);
1415
+
1416
+ return { r, g, b };
1119
1417
  }
1120
1418
 
1121
1419
  /**
1122
1420
  * Converts an RGB colour value to HSV.
1123
1421
  *
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}
1422
+ * @param {number} R Red component [0, 255]
1423
+ * @param {number} G Green [0, 255]
1424
+ * @param {number} B Blue [0, 255]
1425
+ * @returns {CP.HSV} {h,s,v} object with [0, 1] ranged values
1130
1426
  */
1131
1427
  function rgbToHsv(R, G, B) {
1132
- const r = bound01(R, 255);
1133
- const g = bound01(G, 255);
1134
- const b = bound01(B, 255);
1428
+ const r = R / 255;
1429
+ const g = G / 255;
1430
+ const b = B / 255;
1135
1431
  const max = Math.max(r, g, b);
1136
1432
  const min = Math.min(r, g, b);
1137
1433
  let h = 0;
@@ -1158,19 +1454,17 @@
1158
1454
  }
1159
1455
 
1160
1456
  /**
1161
- * Converts an HSV color value to RGB.
1457
+ * Converts an HSV colour value to RGB.
1162
1458
  *
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}
1459
+ * @param {number} H Hue Angle [0, 1]
1460
+ * @param {number} S Saturation [0, 1]
1461
+ * @param {number} V Brightness Angle [0, 1]
1462
+ * @returns {CP.RGB} {r,g,b} object with [0, 1] ranged values
1169
1463
  */
1170
1464
  function hsvToRgb(H, S, V) {
1171
- const h = bound01(H, 360) * 6;
1172
- const s = bound01(S, 100);
1173
- const v = bound01(V, 100);
1465
+ const h = H * 6;
1466
+ const s = S;
1467
+ const v = V;
1174
1468
  const i = Math.floor(h);
1175
1469
  const f = h - i;
1176
1470
  const p = v * (1 - s);
@@ -1184,47 +1478,65 @@
1184
1478
  }
1185
1479
 
1186
1480
  /**
1187
- * Converts an RGB color to hex
1481
+ * Converts an RGB colour to hex
1188
1482
  *
1189
1483
  * Assumes r, g, and b are contained in the set [0, 255]
1190
1484
  * Returns a 3 or 6 character hex
1191
- * @param {number} r
1192
- * @param {number} g
1193
- * @param {number} b
1485
+ * @param {number} r Red component [0, 255]
1486
+ * @param {number} g Green [0, 255]
1487
+ * @param {number} b Blue [0, 255]
1488
+ * @param {boolean=} allow3Char
1194
1489
  * @returns {string}
1195
1490
  */
1196
- function rgbToHex(r, g, b) {
1491
+ function rgbToHex(r, g, b, allow3Char) {
1197
1492
  const hex = [
1198
1493
  pad2(Math.round(r).toString(16)),
1199
1494
  pad2(Math.round(g).toString(16)),
1200
1495
  pad2(Math.round(b).toString(16)),
1201
1496
  ];
1202
1497
 
1498
+ // Return a 3 character hex if possible
1499
+ if (allow3Char && hex[0].charAt(0) === hex[0].charAt(1)
1500
+ && hex[1].charAt(0) === hex[1].charAt(1)
1501
+ && hex[2].charAt(0) === hex[2].charAt(1)) {
1502
+ return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
1503
+ }
1504
+
1203
1505
  return hex.join('');
1204
1506
  }
1205
1507
 
1206
1508
  /**
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
- }
1509
+ * Converts an RGBA color plus alpha transparency to hex8.
1510
+ *
1511
+ * @param {number} r Red component [0, 255]
1512
+ * @param {number} g Green [0, 255]
1513
+ * @param {number} b Blue [0, 255]
1514
+ * @param {number} a Alpha transparency [0, 1]
1515
+ * @param {boolean=} allow4Char when *true* it will also find hex shorthand
1516
+ * @returns {string} a hexadecimal value with alpha transparency
1517
+ */
1518
+ function rgbaToHex(r, g, b, a, allow4Char) {
1519
+ const hex = [
1520
+ pad2(Math.round(r).toString(16)),
1521
+ pad2(Math.round(g).toString(16)),
1522
+ pad2(Math.round(b).toString(16)),
1523
+ pad2(convertDecimalToHex(a)),
1524
+ ];
1214
1525
 
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);
1526
+ // Return a 4 character hex if possible
1527
+ if (allow4Char && hex[0].charAt(0) === hex[0].charAt(1)
1528
+ && hex[1].charAt(0) === hex[1].charAt(1)
1529
+ && hex[2].charAt(0) === hex[2].charAt(1)
1530
+ && hex[3].charAt(0) === hex[3].charAt(1)) {
1531
+ return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
1532
+ }
1533
+ return hex.join('');
1222
1534
  }
1223
1535
 
1224
1536
  /**
1225
- * Returns an `{r,g,b}` color object corresponding to a given number.
1226
- * @param {number} color
1227
- * @returns {CP.RGB}
1537
+ * Returns a colour object corresponding to a given number.
1538
+ * @param {number} color input number
1539
+ * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1228
1540
  */
1229
1541
  function numberInputToObject(color) {
1230
1542
  /* eslint-disable no-bitwise */
@@ -1237,10 +1549,10 @@
1237
1549
  }
1238
1550
 
1239
1551
  /**
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}
1552
+ * Permissive string parsing. Take in a number of formats, and output an object
1553
+ * based on detected format. Returns {r,g,b} or {h,s,l} or {h,s,v}
1554
+ * @param {string} input colour value in any format
1555
+ * @returns {Record<string, (number | string)> | false} an object matching the RegExp
1244
1556
  */
1245
1557
  function stringInputToObject(input) {
1246
1558
  let color = input.trim().toLowerCase();
@@ -1250,12 +1562,15 @@
1250
1562
  };
1251
1563
  }
1252
1564
  let named = false;
1253
- if (colorNames.includes(color)) {
1254
- color = getHexFromColorName(color);
1565
+ if (isColorName(color)) {
1566
+ color = getRGBFromName(color);
1255
1567
  named = true;
1256
- } else if (color === 'transparent') {
1568
+ } else if (nonColors.includes(color)) {
1569
+ const isTransparent = color === 'transparent';
1570
+ const rgb = isTransparent ? 0 : 255;
1571
+ const a = isTransparent ? 0 : 1;
1257
1572
  return {
1258
- r: 0, g: 0, b: 0, a: 0, format: 'name',
1573
+ r: rgb, g: rgb, b: rgb, a, format: 'rgb',
1259
1574
  };
1260
1575
  }
1261
1576
 
@@ -1264,72 +1579,68 @@
1264
1579
  // don't worry about [0,1] or [0,100] or [0,360]
1265
1580
  // Just return an object and let the conversion functions handle that.
1266
1581
  // 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) {
1582
+ let [, m1, m2, m3, m4] = matchers.rgb.exec(color) || [];
1583
+ if (m1 && m2 && m3/* && m4 */) {
1273
1584
  return {
1274
- r: match[1], g: match[2], b: match[3], a: match[4],
1585
+ r: m1, g: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'rgb',
1275
1586
  };
1276
1587
  }
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) {
1588
+ [, m1, m2, m3, m4] = matchers.hsl.exec(color) || [];
1589
+ if (m1 && m2 && m3/* && m4 */) {
1283
1590
  return {
1284
- h: match[1], s: match[2], l: match[3], a: match[4],
1591
+ h: m1, s: m2, l: m3, a: m4 !== undefined ? m4 : 1, format: 'hsl',
1285
1592
  };
1286
1593
  }
1287
- match = matchers.hsv.exec(color);
1288
- if (match) {
1289
- return { h: match[1], s: match[2], v: match[3] };
1594
+ [, m1, m2, m3, m4] = matchers.hsv.exec(color) || [];
1595
+ if (m1 && m2 && m3/* && m4 */) {
1596
+ return {
1597
+ h: m1, s: m2, v: m3, a: m4 !== undefined ? m4 : 1, format: 'hsv',
1598
+ };
1290
1599
  }
1291
- match = matchers.hsva.exec(color);
1292
- if (match) {
1600
+ [, m1, m2, m3, m4] = matchers.hwb.exec(color) || [];
1601
+ if (m1 && m2 && m3) {
1293
1602
  return {
1294
- h: match[1], s: match[2], v: match[3], a: match[4],
1603
+ h: m1, w: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'hwb',
1295
1604
  };
1296
1605
  }
1297
- match = matchers.hex8.exec(color);
1298
- if (match) {
1606
+ [, m1, m2, m3, m4] = matchers.hex8.exec(color) || [];
1607
+ if (m1 && m2 && m3 && m4) {
1299
1608
  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',
1609
+ r: parseIntFromHex(m1),
1610
+ g: parseIntFromHex(m2),
1611
+ b: parseIntFromHex(m3),
1612
+ a: convertHexToDecimal(m4),
1613
+ // format: named ? 'rgb' : 'hex8',
1614
+ format: named ? 'rgb' : 'hex',
1305
1615
  };
1306
1616
  }
1307
- match = matchers.hex6.exec(color);
1308
- if (match) {
1617
+ [, m1, m2, m3] = matchers.hex6.exec(color) || [];
1618
+ if (m1 && m2 && m3) {
1309
1619
  return {
1310
- r: parseIntFromHex(match[1]),
1311
- g: parseIntFromHex(match[2]),
1312
- b: parseIntFromHex(match[3]),
1313
- format: named ? 'name' : 'hex',
1620
+ r: parseIntFromHex(m1),
1621
+ g: parseIntFromHex(m2),
1622
+ b: parseIntFromHex(m3),
1623
+ format: named ? 'rgb' : 'hex',
1314
1624
  };
1315
1625
  }
1316
- match = matchers.hex4.exec(color);
1317
- if (match) {
1626
+ [, m1, m2, m3, m4] = matchers.hex4.exec(color) || [];
1627
+ if (m1 && m2 && m3 && m4) {
1318
1628
  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',
1629
+ r: parseIntFromHex(m1 + m1),
1630
+ g: parseIntFromHex(m2 + m2),
1631
+ b: parseIntFromHex(m3 + m3),
1632
+ a: convertHexToDecimal(m4 + m4),
1633
+ // format: named ? 'rgb' : 'hex8',
1634
+ format: named ? 'rgb' : 'hex',
1324
1635
  };
1325
1636
  }
1326
- match = matchers.hex3.exec(color);
1327
- if (match) {
1637
+ [, m1, m2, m3] = matchers.hex3.exec(color) || [];
1638
+ if (m1 && m2 && m3) {
1328
1639
  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',
1640
+ r: parseIntFromHex(m1 + m1),
1641
+ g: parseIntFromHex(m2 + m2),
1642
+ b: parseIntFromHex(m3 + m3),
1643
+ format: named ? 'rgb' : 'hex',
1333
1644
  };
1334
1645
  }
1335
1646
  return false;
@@ -1343,26 +1654,33 @@
1343
1654
  * "red"
1344
1655
  * "#f00" or "f00"
1345
1656
  * "#ff0000" or "ff0000"
1346
- * "#ff000000" or "ff000000"
1657
+ * "#ff000000" or "ff000000" // CSS4 Module
1347
1658
  * "rgb 255 0 0" or "rgb (255, 0, 0)"
1348
1659
  * "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"
1660
+ * "rgba(255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
1661
+ * "rgba(1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
1662
+ * "rgb(255 0 0 / 10%)" or "rgb 255 0 0 0.1" // CSS4 Module
1351
1663
  * "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
1352
1664
  * "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
1665
+ * "hsl(0deg 100% 50% / 50%)" or "hsl 0 100 50 50" // CSS4 Module
1353
1666
  * "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
1667
+ * "hsva(0, 100%, 100%, 0.1)" or "hsva 0 100% 100% 0.1"
1668
+ * "hsv(0deg 100% 100% / 10%)" or "hsv 0 100 100 0.1" // CSS4 Module
1669
+ * "hwb(0deg, 100%, 100%, 100%)" or "hwb 0 100% 100% 0.1" // CSS4 Module
1354
1670
  * ```
1355
1671
  * @param {string | Record<string, any>} input
1356
1672
  * @returns {CP.ColorObject}
1357
1673
  */
1358
1674
  function inputToRGB(input) {
1359
- /** @type {CP.RGB} */
1360
1675
  let rgb = { r: 0, g: 0, b: 0 };
1361
1676
  let color = input;
1362
- let a;
1677
+ let a = 1;
1363
1678
  let s = null;
1364
1679
  let v = null;
1365
1680
  let l = null;
1681
+ let w = null;
1682
+ let b = null;
1683
+ let h = null;
1366
1684
  let ok = false;
1367
1685
  let format = 'hex';
1368
1686
 
@@ -1373,23 +1691,38 @@
1373
1691
  }
1374
1692
  if (typeof color === 'object') {
1375
1693
  if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
1376
- rgb = rgbToRgb(color.r, color.g, color.b);
1694
+ rgb = { r: color.r, g: color.g, b: color.b }; // RGB values in [0, 255] range
1377
1695
  ok = true;
1378
- format = `${color.r}`.slice(-1) === '%' ? 'prgb' : 'rgb';
1696
+ format = 'rgb';
1379
1697
  } 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);
1698
+ ({ h, s, v } = color);
1699
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1700
+ s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1701
+ v = typeof v === 'number' ? v : bound01(v, 100); // brightness can be `5%` or a [0, 1] value
1702
+ rgb = hsvToRgb(h, s, v);
1383
1703
  ok = true;
1384
1704
  format = 'hsv';
1385
1705
  } 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);
1706
+ ({ h, s, l } = color);
1707
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1708
+ s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1709
+ l = typeof l === 'number' ? l : bound01(l, 100); // lightness can be `5%` or a [0, 1] value
1710
+ rgb = hslToRgb(h, s, l);
1389
1711
  ok = true;
1390
1712
  format = 'hsl';
1713
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.w) && isValidCSSUnit(color.b)) {
1714
+ ({ h, w, b } = color);
1715
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1716
+ w = typeof w === 'number' ? w : bound01(w, 100); // whiteness can be `5%` or a [0, 1] value
1717
+ b = typeof b === 'number' ? b : bound01(b, 100); // blackness can be `5%` or a [0, 1] value
1718
+ rgb = hwbToRgb(h, w, b);
1719
+ ok = true;
1720
+ format = 'hwb';
1721
+ }
1722
+ if (isValidCSSUnit(color.a)) {
1723
+ a = color.a;
1724
+ a = isPercentage(`${a}`) ? bound01(a, 100) : a;
1391
1725
  }
1392
- if ('a' in color) a = color.a;
1393
1726
  }
1394
1727
 
1395
1728
  return {
@@ -1402,27 +1735,21 @@
1402
1735
  };
1403
1736
  }
1404
1737
 
1405
- /** @type {CP.ColorOptions} */
1406
- const colorPickerDefaults = {
1407
- format: 'hex',
1408
- };
1409
-
1410
1738
  /**
1739
+ * @class
1411
1740
  * Returns a new `Color` instance.
1412
1741
  * @see https://github.com/bgrins/TinyColor
1413
- * @class
1414
1742
  */
1415
1743
  class Color {
1416
1744
  /**
1417
1745
  * @constructor
1418
- * @param {CP.ColorInput} input
1419
- * @param {CP.ColorOptions=} config
1746
+ * @param {CP.ColorInput} input the given colour value
1747
+ * @param {CP.ColorFormats=} config the given format
1420
1748
  */
1421
1749
  constructor(input, config) {
1422
1750
  let color = input;
1423
- const opts = typeof config === 'object'
1424
- ? ObjectAssign(colorPickerDefaults, config)
1425
- : ObjectAssign({}, colorPickerDefaults);
1751
+ const configFormat = config && COLOR_FORMAT.includes(config)
1752
+ ? config : 'rgb';
1426
1753
 
1427
1754
  // If input is already a `Color`, return itself
1428
1755
  if (color instanceof Color) {
@@ -1435,36 +1762,31 @@
1435
1762
  r, g, b, a, ok, format,
1436
1763
  } = inputToRGB(color);
1437
1764
 
1765
+ // bind
1766
+ const self = this;
1767
+
1438
1768
  /** @type {CP.ColorInput} */
1439
- this.originalInput = color;
1769
+ self.originalInput = color;
1440
1770
  /** @type {number} */
1441
- this.r = r;
1771
+ self.r = r;
1442
1772
  /** @type {number} */
1443
- this.g = g;
1773
+ self.g = g;
1444
1774
  /** @type {number} */
1445
- this.b = b;
1775
+ self.b = b;
1446
1776
  /** @type {number} */
1447
- this.a = a;
1777
+ self.a = a;
1448
1778
  /** @type {boolean} */
1449
- this.ok = ok;
1450
- /** @type {number} */
1451
- this.roundA = Math.round(100 * this.a) / 100;
1779
+ self.ok = ok;
1452
1780
  /** @type {CP.ColorFormats} */
1453
- this.format = opts.format || format;
1781
+ self.format = configFormat || format;
1454
1782
 
1455
1783
  // Don't let the range of [0,255] come back in [0,1].
1456
1784
  // Potentially lose a little bit of precision here, but will fix issues where
1457
1785
  // .5 gets interpreted as half of the total, instead of half of 1
1458
1786
  // 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
- }
1787
+ if (r < 1) self.r = Math.round(r);
1788
+ if (g < 1) self.g = Math.round(g);
1789
+ if (b < 1) self.b = Math.round(b);
1468
1790
  }
1469
1791
 
1470
1792
  /**
@@ -1484,40 +1806,40 @@
1484
1806
  }
1485
1807
 
1486
1808
  /**
1487
- * Returns the perceived luminance of a color.
1809
+ * Returns the perceived luminance of a colour.
1488
1810
  * @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
1489
- * @returns {number} a number in [0-1] range
1811
+ * @returns {number} a number in the [0, 1] range
1490
1812
  */
1491
1813
  get luminance() {
1492
1814
  const { r, g, b } = this;
1493
1815
  let R = 0;
1494
1816
  let G = 0;
1495
1817
  let B = 0;
1496
- const RsRGB = r / 255;
1497
- const GsRGB = g / 255;
1498
- const BsRGB = b / 255;
1818
+ const rp = r / 255;
1819
+ const rg = g / 255;
1820
+ const rb = b / 255;
1499
1821
 
1500
- if (RsRGB <= 0.03928) {
1501
- R = RsRGB / 12.92;
1822
+ if (rp <= 0.03928) {
1823
+ R = rp / 12.92;
1502
1824
  } else {
1503
- R = ((RsRGB + 0.055) / 1.055) ** 2.4;
1825
+ R = ((rp + 0.055) / 1.055) ** 2.4;
1504
1826
  }
1505
- if (GsRGB <= 0.03928) {
1506
- G = GsRGB / 12.92;
1827
+ if (rg <= 0.03928) {
1828
+ G = rg / 12.92;
1507
1829
  } else {
1508
- G = ((GsRGB + 0.055) / 1.055) ** 2.4;
1830
+ G = ((rg + 0.055) / 1.055) ** 2.4;
1509
1831
  }
1510
- if (BsRGB <= 0.03928) {
1511
- B = BsRGB / 12.92;
1832
+ if (rb <= 0.03928) {
1833
+ B = rb / 12.92;
1512
1834
  } else {
1513
- B = ((BsRGB + 0.055) / 1.055) ** 2.4;
1835
+ B = ((rb + 0.055) / 1.055) ** 2.4;
1514
1836
  }
1515
1837
  return 0.2126 * R + 0.7152 * G + 0.0722 * B;
1516
1838
  }
1517
1839
 
1518
1840
  /**
1519
- * Returns the perceived brightness of the color.
1520
- * @returns {number} a number in [0-255] range
1841
+ * Returns the perceived brightness of the colour.
1842
+ * @returns {number} a number in the [0, 255] range
1521
1843
  */
1522
1844
  get brightness() {
1523
1845
  const { r, g, b } = this;
@@ -1525,123 +1847,289 @@
1525
1847
  }
1526
1848
 
1527
1849
  /**
1528
- * Returns the color as a RGBA object.
1529
- * @returns {CP.RGBA}
1850
+ * Returns the colour as an RGBA object.
1851
+ * @returns {CP.RGBA} an {r,g,b,a} object with [0, 255] ranged values
1530
1852
  */
1531
1853
  toRgb() {
1854
+ const {
1855
+ r, g, b, a,
1856
+ } = this;
1857
+ const [R, G, B] = [r, g, b].map((x) => Math.round(x));
1858
+
1532
1859
  return {
1533
- r: Math.round(this.r),
1534
- g: Math.round(this.g),
1535
- b: Math.round(this.b),
1536
- a: this.a,
1860
+ r: R,
1861
+ g: G,
1862
+ b: B,
1863
+ a: Math.round(a * 100) / 100,
1537
1864
  };
1538
1865
  }
1539
1866
 
1540
1867
  /**
1541
- * Returns the RGBA values concatenated into a string.
1542
- * @returns {string} the CSS valid color in RGB/RGBA format
1868
+ * Returns the RGBA values concatenated into a CSS3 Module string format.
1869
+ * * rgb(255,255,255)
1870
+ * * rgba(255,255,255,0.5)
1871
+ * @returns {string} the CSS valid colour in RGB/RGBA format
1543
1872
  */
1544
1873
  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})`;
1874
+ const {
1875
+ r, g, b, a,
1876
+ } = this.toRgb();
1877
+
1878
+ return a === 1
1879
+ ? `rgb(${r}, ${g}, ${b})`
1880
+ : `rgba(${r}, ${g}, ${b}, ${a})`;
1881
+ }
1882
+
1883
+ /**
1884
+ * Returns the RGBA values concatenated into a CSS4 Module string format.
1885
+ * * rgb(255 255 255)
1886
+ * * rgb(255 255 255 / 50%)
1887
+ * @returns {string} the CSS valid colour in CSS4 RGB format
1888
+ */
1889
+ toRgbCSS4String() {
1890
+ const {
1891
+ r, g, b, a,
1892
+ } = this.toRgb();
1893
+ const A = a === 1 ? '' : ` / ${Math.round(a * 100)}%`;
1894
+
1895
+ return `rgb(${r} ${g} ${b}${A})`;
1896
+ }
1897
+
1898
+ /**
1899
+ * Returns the hexadecimal value of the colour. When the parameter is *true*
1900
+ * it will find a 3 characters shorthand of the decimal value.
1901
+ *
1902
+ * @param {boolean=} allow3Char when `true` returns shorthand HEX
1903
+ * @returns {string} the hexadecimal colour format
1904
+ */
1905
+ toHex(allow3Char) {
1906
+ const {
1907
+ r, g, b, a,
1908
+ } = this.toRgb();
1909
+
1910
+ return a === 1
1911
+ ? rgbToHex(r, g, b, allow3Char)
1912
+ : rgbaToHex(r, g, b, a, allow3Char);
1913
+ }
1914
+
1915
+ /**
1916
+ * Returns the CSS valid hexadecimal vaue of the colour. When the parameter is *true*
1917
+ * it will find a 3 characters shorthand of the value.
1918
+ *
1919
+ * @param {boolean=} allow3Char when `true` returns shorthand HEX
1920
+ * @returns {string} the CSS valid colour in hexadecimal format
1921
+ */
1922
+ toHexString(allow3Char) {
1923
+ return `#${this.toHex(allow3Char)}`;
1551
1924
  }
1552
1925
 
1553
1926
  /**
1554
- * Returns the HEX value of the color.
1555
- * @returns {string} the hexadecimal color format
1927
+ * Returns the HEX8 value of the colour.
1928
+ * @param {boolean=} allow4Char when `true` returns shorthand HEX
1929
+ * @returns {string} the CSS valid colour in hexadecimal format
1556
1930
  */
1557
- toHex() {
1558
- return rgbToHex(this.r, this.g, this.b);
1931
+ toHex8(allow4Char) {
1932
+ const {
1933
+ r, g, b, a,
1934
+ } = this.toRgb();
1935
+
1936
+ return rgbaToHex(r, g, b, a, allow4Char);
1559
1937
  }
1560
1938
 
1561
1939
  /**
1562
- * Returns the HEX value of the color.
1563
- * @returns {string} the CSS valid color in hexadecimal format
1940
+ * Returns the HEX8 value of the colour.
1941
+ * @param {boolean=} allow4Char when `true` returns shorthand HEX
1942
+ * @returns {string} the CSS valid colour in hexadecimal format
1564
1943
  */
1565
- toHexString() {
1566
- return `#${this.toHex()}`;
1944
+ toHex8String(allow4Char) {
1945
+ return `#${this.toHex8(allow4Char)}`;
1567
1946
  }
1568
1947
 
1569
1948
  /**
1570
- * Returns the color as a HSVA object.
1571
- * @returns {CP.HSVA} the `{h,s,v,a}` object
1949
+ * Returns the colour as a HSVA object.
1950
+ * @returns {CP.HSVA} the `{h,s,v,a}` object with [0, 1] ranged values
1572
1951
  */
1573
1952
  toHsv() {
1574
- const { h, s, v } = rgbToHsv(this.r, this.g, this.b);
1953
+ const {
1954
+ r, g, b, a,
1955
+ } = this.toRgb();
1956
+ const { h, s, v } = rgbToHsv(r, g, b);
1957
+
1575
1958
  return {
1576
- h: h * 360, s, v, a: this.a,
1959
+ h, s, v, a,
1577
1960
  };
1578
1961
  }
1579
1962
 
1580
1963
  /**
1581
- * Returns the color as a HSLA object.
1582
- * @returns {CP.HSLA}
1964
+ * Returns the colour as an HSLA object.
1965
+ * @returns {CP.HSLA} the `{h,s,l,a}` object with [0, 1] ranged values
1583
1966
  */
1584
1967
  toHsl() {
1585
- const { h, s, l } = rgbToHsl(this.r, this.g, this.b);
1968
+ const {
1969
+ r, g, b, a,
1970
+ } = this.toRgb();
1971
+ const { h, s, l } = rgbToHsl(r, g, b);
1972
+
1586
1973
  return {
1587
- h: h * 360, s, l, a: this.a,
1974
+ h, s, l, a,
1588
1975
  };
1589
1976
  }
1590
1977
 
1591
1978
  /**
1592
- * Returns the HSLA values concatenated into a string.
1593
- * @returns {string} the CSS valid color in HSL/HSLA format
1979
+ * Returns the HSLA values concatenated into a CSS3 Module format string.
1980
+ * * `hsl(150, 100%, 50%)`
1981
+ * * `hsla(150, 100%, 50%, 0.5)`
1982
+ * @returns {string} the CSS valid colour in HSL/HSLA format
1594
1983
  */
1595
1984
  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})`;
1985
+ let {
1986
+ h, s, l, a,
1987
+ } = this.toHsl();
1988
+ h = Math.round(h * 360);
1989
+ s = Math.round(s * 100);
1990
+ l = Math.round(l * 100);
1991
+ a = Math.round(a * 100) / 100;
1992
+
1993
+ return a === 1
1994
+ ? `hsl(${h}, ${s}%, ${l}%)`
1995
+ : `hsla(${h}, ${s}%, ${l}%, ${a})`;
1996
+ }
1997
+
1998
+ /**
1999
+ * Returns the HSLA values concatenated into a CSS4 Module format string.
2000
+ * * `hsl(150deg 100% 50%)`
2001
+ * * `hsl(150deg 100% 50% / 50%)`
2002
+ * @returns {string} the CSS valid colour in CSS4 HSL format
2003
+ */
2004
+ toHslCSS4String() {
2005
+ let {
2006
+ h, s, l, a,
2007
+ } = this.toHsl();
2008
+ h = Math.round(h * 360);
2009
+ s = Math.round(s * 100);
2010
+ l = Math.round(l * 100);
2011
+ a = Math.round(a * 100);
2012
+ const A = a < 100 ? ` / ${Math.round(a)}%` : '';
2013
+
2014
+ return `hsl(${h}deg ${s}% ${l}%${A})`;
2015
+ }
2016
+
2017
+ /**
2018
+ * Returns the colour as an HWBA object.
2019
+ * @returns {CP.HWBA} the `{h,w,b,a}` object with [0, 1] ranged values
2020
+ */
2021
+ toHwb() {
2022
+ const {
2023
+ r, g, b, a,
2024
+ } = this;
2025
+ const { h, w, b: bl } = rgbToHwb(r, g, b);
2026
+ return {
2027
+ h, w, b: bl, a,
2028
+ };
2029
+ }
2030
+
2031
+ /**
2032
+ * Returns the HWBA values concatenated into a string.
2033
+ * @returns {string} the CSS valid colour in HWB format
2034
+ */
2035
+ toHwbString() {
2036
+ let {
2037
+ h, w, b, a,
2038
+ } = this.toHwb();
2039
+ h = Math.round(h * 360);
2040
+ w = Math.round(w * 100);
2041
+ b = Math.round(b * 100);
2042
+ a = Math.round(a * 100);
2043
+ const A = a < 100 ? ` / ${Math.round(a)}%` : '';
2044
+
2045
+ return `hwb(${h}deg ${w}% ${b}%${A})`;
1603
2046
  }
1604
2047
 
1605
2048
  /**
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
2049
+ * Sets the alpha value of the current colour.
2050
+ * @param {number} alpha a new alpha value in the [0, 1] range.
2051
+ * @returns {Color} the `Color` instance
1609
2052
  */
1610
2053
  setAlpha(alpha) {
1611
- this.a = boundAlpha(alpha);
1612
- this.roundA = Math.round(100 * this.a) / 100;
1613
- return this;
2054
+ const self = this;
2055
+ self.a = boundAlpha(alpha);
2056
+ return self;
1614
2057
  }
1615
2058
 
1616
2059
  /**
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
2060
+ * Saturate the colour with a given amount.
2061
+ * @param {number=} amount a value in the [0, 100] range
2062
+ * @returns {Color} the `Color` instance
1620
2063
  */
1621
2064
  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);
2065
+ const self = this;
2066
+ if (typeof amount !== 'number') return self;
2067
+ const { h, s, l } = self.toHsl();
2068
+ const { r, g, b } = hslToRgb(h, clamp01(s + amount / 100), l);
2069
+
2070
+ ObjectAssign(self, { r, g, b });
2071
+ return self;
1627
2072
  }
1628
2073
 
1629
2074
  /**
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
2075
+ * Desaturate the colour with a given amount.
2076
+ * @param {number=} amount a value in the [0, 100] range
2077
+ * @returns {Color} the `Color` instance
1633
2078
  */
1634
2079
  desaturate(amount) {
1635
- return amount ? this.saturate(-amount) : this;
2080
+ return typeof amount === 'number' ? this.saturate(-amount) : this;
1636
2081
  }
1637
2082
 
1638
2083
  /**
1639
- * Completely desaturates a color into greyscale.
2084
+ * Completely desaturates a colour into greyscale.
1640
2085
  * Same as calling `desaturate(100)`
1641
- * @returns {Color} a new `Color` instance
2086
+ * @returns {Color} the `Color` instance
1642
2087
  */
1643
2088
  greyscale() {
1644
- return this.desaturate(100);
2089
+ return this.saturate(-100);
2090
+ }
2091
+
2092
+ /**
2093
+ * Increase the colour lightness with a given amount.
2094
+ * @param {number=} amount a value in the [0, 100] range
2095
+ * @returns {Color} the `Color` instance
2096
+ */
2097
+ lighten(amount) {
2098
+ const self = this;
2099
+ if (typeof amount !== 'number') return self;
2100
+
2101
+ const { h, s, l } = self.toHsl();
2102
+ const { r, g, b } = hslToRgb(h, s, clamp01(l + amount / 100));
2103
+
2104
+ ObjectAssign(self, { r, g, b });
2105
+ return self;
2106
+ }
2107
+
2108
+ /**
2109
+ * Decrease the colour lightness with a given amount.
2110
+ * @param {number=} amount a value in the [0, 100] range
2111
+ * @returns {Color} the `Color` instance
2112
+ */
2113
+ darken(amount) {
2114
+ return typeof amount === 'number' ? this.lighten(-amount) : this;
2115
+ }
2116
+
2117
+ /**
2118
+ * Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
2119
+ * Values outside of this range will be wrapped into this range.
2120
+ *
2121
+ * @param {number=} amount a value in the [0, 100] range
2122
+ * @returns {Color} the `Color` instance
2123
+ */
2124
+ spin(amount) {
2125
+ const self = this;
2126
+ if (typeof amount !== 'number') return self;
2127
+
2128
+ const { h, s, l } = self.toHsl();
2129
+ const { r, g, b } = hslToRgb(clamp01(((h * 360 + amount) % 360) / 360), s, l);
2130
+
2131
+ ObjectAssign(self, { r, g, b });
2132
+ return self;
1645
2133
  }
1646
2134
 
1647
2135
  /** Returns a clone of the current `Color` instance. */
@@ -1650,77 +2138,235 @@
1650
2138
  }
1651
2139
 
1652
2140
  /**
1653
- * Returns the color value in CSS valid string format.
1654
- * @returns {string}
2141
+ * Returns the colour value in CSS valid string format.
2142
+ * @param {boolean=} allowShort when *true*, HEX values can be shorthand
2143
+ * @returns {string} the CSS valid colour in the configured format
1655
2144
  */
1656
- toString() {
1657
- const { format } = this;
2145
+ toString(allowShort) {
2146
+ const self = this;
2147
+ const { format } = self;
1658
2148
 
1659
- if (format === 'rgb') {
1660
- return this.toRgbString();
1661
- }
1662
- if (format === 'hsl') {
1663
- return this.toHslString();
1664
- }
1665
- return this.toHexString();
2149
+ if (format === 'hex') return self.toHexString(allowShort);
2150
+ if (format === 'hsl') return self.toHslString();
2151
+ if (format === 'hwb') return self.toHwbString();
2152
+
2153
+ return self.toRgbString();
1666
2154
  }
1667
2155
  }
1668
2156
 
1669
2157
  ObjectAssign(Color, {
1670
- colorNames,
2158
+ ANGLES,
2159
+ CSS_ANGLE,
1671
2160
  CSS_INTEGER,
1672
2161
  CSS_NUMBER,
1673
2162
  CSS_UNIT,
1674
- PERMISSIVE_MATCH3,
1675
- PERMISSIVE_MATCH4,
2163
+ CSS_UNIT2,
2164
+ PERMISSIVE_MATCH,
1676
2165
  matchers,
1677
2166
  isOnePointZero,
1678
2167
  isPercentage,
1679
2168
  isValidCSSUnit,
2169
+ pad2,
2170
+ clamp01,
1680
2171
  bound01,
1681
2172
  boundAlpha,
1682
- clamp01,
1683
- getHexFromColorName,
1684
- convertToPercentage,
2173
+ getRGBFromName,
1685
2174
  convertHexToDecimal,
1686
- pad2,
1687
- rgbToRgb,
2175
+ convertDecimalToHex,
1688
2176
  rgbToHsl,
1689
2177
  rgbToHex,
1690
2178
  rgbToHsv,
2179
+ rgbToHwb,
2180
+ rgbaToHex,
1691
2181
  hslToRgb,
1692
2182
  hsvToRgb,
1693
- hue2rgb,
2183
+ hueToRgb,
2184
+ hwbToRgb,
1694
2185
  parseIntFromHex,
1695
2186
  numberInputToObject,
1696
2187
  stringInputToObject,
1697
2188
  inputToRGB,
2189
+ ObjectAssign,
1698
2190
  });
1699
2191
 
2192
+ /**
2193
+ * @class
2194
+ * Returns a color palette with a given set of parameters.
2195
+ * @example
2196
+ * new ColorPalette(0, 12, 10);
2197
+ * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: array }
2198
+ */
2199
+ class ColorPalette {
2200
+ /**
2201
+ * The `hue` parameter is optional, which would be set to 0.
2202
+ * @param {number[]} args represeinting hue, hueSteps, lightSteps
2203
+ * * `args.hue` the starting Hue [0, 360]
2204
+ * * `args.hueSteps` Hue Steps Count [5, 13]
2205
+ * * `args.lightSteps` Lightness Steps Count [8, 10]
2206
+ */
2207
+ constructor(...args) {
2208
+ let hue = 0;
2209
+ let hueSteps = 12;
2210
+ let lightSteps = 10;
2211
+ let lightnessArray = [0.5];
2212
+
2213
+ if (args.length === 3) {
2214
+ [hue, hueSteps, lightSteps] = args;
2215
+ } else if (args.length === 2) {
2216
+ [hueSteps, lightSteps] = args;
2217
+ } else {
2218
+ throw TypeError('The ColorPalette requires minimum 2 arguments');
2219
+ }
2220
+
2221
+ /** @type {string[]} */
2222
+ const colors = [];
2223
+
2224
+ const hueStep = 360 / hueSteps;
2225
+ const lightStep = 100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100;
2226
+ const half = Math.round((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
2227
+
2228
+ // light tints
2229
+ for (let i = 0; i < half; i += 1) {
2230
+ lightnessArray = [...lightnessArray, (0.5 + lightStep * (i + 1))];
2231
+ }
2232
+
2233
+ // dark tints
2234
+ for (let i = 0; i < lightSteps - half - 1; i += 1) {
2235
+ lightnessArray = [(0.5 - lightStep * (i + 1)), ...lightnessArray];
2236
+ }
2237
+
2238
+ // feed `colors` Array
2239
+ for (let i = 0; i < hueSteps; i += 1) {
2240
+ const currentHue = ((hue + i * hueStep) % 360) / 360;
2241
+ lightnessArray.forEach((l) => {
2242
+ colors.push(new Color({ h: currentHue, s: 1, l }).toHexString());
2243
+ });
2244
+ }
2245
+
2246
+ this.hue = hue;
2247
+ this.hueSteps = hueSteps;
2248
+ this.lightSteps = lightSteps;
2249
+ this.colors = colors;
2250
+ }
2251
+ }
2252
+
2253
+ /**
2254
+ * Returns a color-defaults with given values and class.
2255
+ * @param {CP.ColorPicker} self
2256
+ * @param {CP.ColorPalette | string[]} colorsSource
2257
+ * @param {string} menuClass
2258
+ * @returns {HTMLElement | Element}
2259
+ */
2260
+ function getColorMenu(self, colorsSource, menuClass) {
2261
+ const { input, format, componentLabels } = self;
2262
+ const { defaultsLabel, presetsLabel } = componentLabels;
2263
+ const isOptionsMenu = menuClass === 'color-options';
2264
+ const isPalette = colorsSource instanceof ColorPalette;
2265
+ const menuLabel = isOptionsMenu ? presetsLabel : defaultsLabel;
2266
+ let colorsArray = isPalette ? colorsSource.colors : colorsSource;
2267
+ colorsArray = colorsArray instanceof Array ? colorsArray : [];
2268
+ const colorsCount = colorsArray.length;
2269
+ const { lightSteps } = isPalette ? colorsSource : { lightSteps: null };
2270
+ let fit = lightSteps
2271
+ || Math.max(...[5, 6, 7, 8, 9, 10].filter((x) => colorsCount > (x * 2) && !(colorsCount % x)));
2272
+ fit = Number.isFinite(fit) ? fit : 5;
2273
+ const isMultiLine = isOptionsMenu && colorsCount > fit;
2274
+ let rowCountHover = 1;
2275
+ rowCountHover = isMultiLine && colorsCount < 27 ? 2 : rowCountHover;
2276
+ rowCountHover = colorsCount >= 27 ? 3 : rowCountHover;
2277
+ rowCountHover = colorsCount >= 36 ? 4 : rowCountHover;
2278
+ rowCountHover = colorsCount >= 45 ? 5 : rowCountHover;
2279
+ const rowCount = rowCountHover - (colorsCount < 27 ? 1 : 2);
2280
+ const isScrollable = isMultiLine && colorsCount > rowCountHover * fit;
2281
+ let finalClass = menuClass;
2282
+ finalClass += isScrollable ? ' scrollable' : '';
2283
+ finalClass += isMultiLine ? ' multiline' : '';
2284
+ const gap = isMultiLine ? '1px' : '0.25rem';
2285
+ let optionSize = isMultiLine ? 1.75 : 2;
2286
+ optionSize = !(colorsCount % 10) && isMultiLine ? 1.5 : optionSize;
2287
+ const menuHeight = `${(rowCount || 1) * optionSize}rem`;
2288
+ const menuHeightHover = `calc(${rowCountHover} * ${optionSize}rem + ${rowCountHover - 1} * ${gap})`;
2289
+ const gridTemplateColumns = `repeat(${fit}, ${optionSize}rem)`;
2290
+ const gridTemplateRows = `repeat(auto-fill, ${optionSize}rem)`;
2291
+
2292
+ const menu = createElement({
2293
+ tagName: 'ul',
2294
+ className: finalClass,
2295
+ });
2296
+ setAttribute(menu, 'role', 'listbox');
2297
+ setAttribute(menu, ariaLabel, `${menuLabel}`);
2298
+
2299
+ if (isOptionsMenu) {
2300
+ if (isScrollable) {
2301
+ const styleText = 'this.style.height=';
2302
+ setAttribute(menu, 'onmouseout', `${styleText}'${menuHeight}'`);
2303
+ setAttribute(menu, 'onmouseover', `${styleText}'${menuHeightHover}'`);
2304
+ }
2305
+ const menuStyle = {
2306
+ height: isScrollable ? menuHeight : '', gridTemplateColumns, gridTemplateRows, gap,
2307
+ };
2308
+ setElementStyle(menu, menuStyle);
2309
+ }
2310
+
2311
+ colorsArray.forEach((x) => {
2312
+ const [value, label] = x.trim().split(':');
2313
+ const xRealColor = new Color(value, format).toString();
2314
+ const isActive = xRealColor === getAttribute(input, 'value');
2315
+ const active = isActive ? ' active' : '';
2316
+
2317
+ const option = createElement({
2318
+ tagName: 'li',
2319
+ className: `color-option${active}`,
2320
+ innerText: `${label || x}`,
2321
+ });
2322
+
2323
+ setAttribute(option, 'tabindex', '0');
2324
+ setAttribute(option, 'data-value', `${value}`);
2325
+ setAttribute(option, 'role', 'option');
2326
+ setAttribute(option, ariaSelected, isActive ? 'true' : 'false');
2327
+
2328
+ if (isOptionsMenu) {
2329
+ setElementStyle(option, {
2330
+ width: `${optionSize}rem`, height: `${optionSize}rem`, backgroundColor: x,
2331
+ });
2332
+ }
2333
+
2334
+ menu.append(option);
2335
+ });
2336
+ return menu;
2337
+ }
2338
+
2339
+ /**
2340
+ * Check if a string is valid JSON string.
2341
+ * @param {string} str the string input
2342
+ * @returns {boolean} the query result
2343
+ */
2344
+ function isValidJSON(str) {
2345
+ try {
2346
+ JSON.parse(str);
2347
+ } catch (e) {
2348
+ return false;
2349
+ }
2350
+ return true;
2351
+ }
2352
+
2353
+ var version = "0.0.1alpha2";
2354
+
2355
+ // @ts-ignore
2356
+
2357
+ const Version = version;
2358
+
1700
2359
  // ColorPicker GC
1701
2360
  // ==============
1702
2361
  const colorPickerString = 'color-picker';
1703
2362
  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',
2363
+ const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2364
+ const colorPickerDefaults = {
2365
+ componentLabels: colorPickerLabels,
2366
+ colorLabels: colorNames,
2367
+ format: 'rgb',
2368
+ colorPresets: undefined,
2369
+ colorKeywords: nonColors,
1724
2370
  };
1725
2371
 
1726
2372
  // ColorPicker Static Methods
@@ -1735,165 +2381,94 @@
1735
2381
  // ColorPicker Private Methods
1736
2382
  // ===========================
1737
2383
 
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
2384
  /**
1758
2385
  * Generate HTML markup and update instance properties.
1759
2386
  * @param {ColorPicker} self
1760
2387
  */
1761
2388
  function initCallback(self) {
1762
2389
  const {
1763
- input, parent, format, id, componentLabels, keywords,
2390
+ input, parent, format, id, componentLabels, colorKeywords, colorPresets,
1764
2391
  } = self;
1765
2392
  const colorValue = getAttribute(input, 'value') || '#fff';
1766
2393
 
1767
2394
  const {
1768
- toggleLabel, menuLabel, formatLabel, pickerLabel, appearanceLabel,
2395
+ toggleLabel, pickerLabel, formatLabel, hexLabel,
1769
2396
  } = componentLabels;
1770
2397
 
1771
2398
  // update color
1772
2399
  const color = nonColors.includes(colorValue) ? '#fff' : colorValue;
1773
- self.color = new Color(color, { format });
2400
+ self.color = new Color(color, format);
1774
2401
 
1775
2402
  // set initial controls dimensions
1776
2403
  // make the controls smaller on mobile
1777
- const cv1w = isMobile ? 150 : 230;
1778
- const cvh = isMobile ? 150 : 230;
1779
- const cv2w = 21;
1780
2404
  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}`;
2405
+ const formatString = format === 'hex' ? hexLabel : format.toUpperCase();
1783
2406
 
1784
2407
  const pickerBtn = createElement({
2408
+ id: `picker-btn-${id}`,
1785
2409
  tagName: 'button',
1786
- className: 'picker-toggle button-appearance',
1787
- ariaExpanded: 'false',
1788
- ariaHasPopup: 'true',
1789
- ariaLive: 'polite',
2410
+ className: 'picker-toggle btn-appearance',
1790
2411
  });
1791
- setAttribute(pickerBtn, 'tabindex', '-1');
2412
+ setAttribute(pickerBtn, ariaExpanded, 'false');
2413
+ setAttribute(pickerBtn, ariaHasPopup, 'true');
1792
2414
  pickerBtn.append(createElement({
1793
2415
  tagName: 'span',
1794
2416
  className: vHidden,
1795
- innerText: 'Open Color Picker',
2417
+ innerText: `${pickerLabel}. ${formatLabel}: ${formatString}`,
1796
2418
  }));
1797
2419
 
1798
- const colorPickerDropdown = createElement({
2420
+ const pickerDropdown = createElement({
1799
2421
  tagName: 'div',
1800
2422
  className: `color-dropdown picker${dropClass}`,
1801
2423
  });
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
- );
1838
-
1839
- if (format !== 'hex') {
1840
- colorControls.append(
1841
- getColorControl(3, id, cv2w, cvh),
1842
- );
1843
- }
2424
+ setAttribute(pickerDropdown, ariaLabelledBy, `picker-btn-${id}`);
2425
+ setAttribute(pickerDropdown, 'role', 'group');
1844
2426
 
1845
- // @ts-ignore
2427
+ const colorControls = getColorControls(self);
1846
2428
  const colorForm = getColorForm(self);
1847
- colorPickerDropdown.append(colorControls, colorForm);
1848
- parent.append(pickerBtn, colorPickerDropdown);
1849
2429
 
1850
- // set color key menu template
1851
- if (keywords) {
1852
- const colorKeys = keywords;
2430
+ pickerDropdown.append(colorControls, colorForm);
2431
+ input.before(pickerBtn);
2432
+ parent.append(pickerDropdown);
2433
+
2434
+ // set colour key menu template
2435
+ if (colorKeywords || colorPresets) {
1853
2436
  const presetsDropdown = createElement({
1854
2437
  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);
2438
+ className: `color-dropdown scrollable menu${dropClass}`,
1881
2439
  });
2440
+
2441
+ // color presets
2442
+ if ((colorPresets instanceof Array && colorPresets.length)
2443
+ || (colorPresets instanceof ColorPalette && colorPresets.colors)) {
2444
+ const presetsMenu = getColorMenu(self, colorPresets, 'color-options');
2445
+ presetsDropdown.append(presetsMenu);
2446
+ }
2447
+
2448
+ // explicit defaults [reset, initial, inherit, transparent, currentColor]
2449
+ if (colorKeywords && colorKeywords.length) {
2450
+ const keywordsMenu = getColorMenu(self, colorKeywords, 'color-defaults');
2451
+ presetsDropdown.append(keywordsMenu);
2452
+ }
2453
+
1882
2454
  const presetsBtn = createElement({
1883
2455
  tagName: 'button',
1884
- className: 'menu-toggle button-appearance',
1885
- ariaExpanded: 'false',
1886
- ariaHasPopup: 'true',
2456
+ className: 'menu-toggle btn-appearance',
1887
2457
  });
2458
+ setAttribute(presetsBtn, 'tabindex', '-1');
2459
+ setAttribute(presetsBtn, ariaExpanded, 'false');
2460
+ setAttribute(presetsBtn, ariaHasPopup, 'true');
2461
+
1888
2462
  const xmlns = encodeURI('http://www.w3.org/2000/svg');
1889
2463
  const presetsIcon = createElementNS(xmlns, { tagName: 'svg' });
1890
2464
  setAttribute(presetsIcon, 'xmlns', xmlns);
1891
- setAttribute(presetsIcon, ariaHidden, 'true');
1892
2465
  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);
2466
+ setAttribute(presetsIcon, ariaHidden, 'true');
2467
+
2468
+ const path = createElementNS(xmlns, { tagName: 'path' });
2469
+ setAttribute(path, 'd', 'M98,158l157,156L411,158l27,27L255,368L71,185L98,158z');
2470
+ setAttribute(path, 'fill', '#fff');
2471
+ presetsIcon.append(path);
1897
2472
  presetsBtn.append(createElement({
1898
2473
  tagName: 'span',
1899
2474
  className: vHidden,
@@ -1904,9 +2479,29 @@
1904
2479
  }
1905
2480
 
1906
2481
  // solve non-colors after settings save
1907
- if (keywords && nonColors.includes(colorValue)) {
2482
+ if (colorKeywords && nonColors.includes(colorValue)) {
1908
2483
  self.value = colorValue;
1909
2484
  }
2485
+ setAttribute(input, 'tabindex', '-1');
2486
+ }
2487
+
2488
+ /**
2489
+ * Add / remove `ColorPicker` main event listeners.
2490
+ * @param {ColorPicker} self
2491
+ * @param {boolean=} action
2492
+ */
2493
+ function toggleEvents(self, action) {
2494
+ const fn = action ? addListener : removeListener;
2495
+ const { input, pickerToggle, menuToggle } = self;
2496
+
2497
+ fn(input, focusinEvent, self.showPicker);
2498
+ fn(pickerToggle, mouseclickEvent, self.togglePicker);
2499
+
2500
+ fn(input, keydownEvent, self.keyToggle);
2501
+
2502
+ if (menuToggle) {
2503
+ fn(menuToggle, mouseclickEvent, self.toggleMenu);
2504
+ }
1910
2505
  }
1911
2506
 
1912
2507
  /**
@@ -1916,26 +2511,33 @@
1916
2511
  */
1917
2512
  function toggleEventsOnShown(self, action) {
1918
2513
  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' };
2514
+ const { input, colorMenu, parent } = self;
2515
+ const doc = getDocument(input);
2516
+ const win = getWindow(input);
2517
+ const pointerEvents = `on${touchstartEvent}` in doc
2518
+ ? { down: touchstartEvent, move: touchmoveEvent, up: touchendEvent }
2519
+ : { down: mousedownEvent, move: mousemoveEvent, up: mouseupEvent };
1922
2520
 
1923
2521
  fn(self.controls, pointerEvents.down, self.pointerDown);
1924
- self.controlKnobs.forEach((x) => fn(x, 'keydown', self.handleKnobs));
2522
+ self.controlKnobs.forEach((x) => fn(x, keydownEvent, self.handleKnobs));
1925
2523
 
1926
- fn(window, 'scroll', self.handleScroll);
2524
+ // @ts-ignore -- this is `Window`
2525
+ fn(win, scrollEvent, self.handleScroll);
2526
+ // @ts-ignore -- this is `Window`
2527
+ fn(win, resizeEvent, self.update);
1927
2528
 
1928
- [self.input, ...self.inputs].forEach((x) => fn(x, 'change', self.changeHandler));
2529
+ [input, ...self.inputs].forEach((x) => fn(x, changeEvent, self.changeHandler));
1929
2530
 
1930
- if (self.colorMenu) {
1931
- fn(self.colorMenu, 'click', self.menuClickHandler);
1932
- fn(self.colorMenu, 'keydown', self.menuKeyHandler);
2531
+ if (colorMenu) {
2532
+ fn(colorMenu, mouseclickEvent, self.menuClickHandler);
2533
+ fn(colorMenu, keydownEvent, self.menuKeyHandler);
1933
2534
  }
1934
2535
 
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);
2536
+ fn(doc, pointerEvents.move, self.pointerMove);
2537
+ fn(doc, pointerEvents.up, self.pointerUp);
2538
+ fn(parent, focusoutEvent, self.handleFocusOut);
2539
+ // @ts-ignore -- this is `Window`
2540
+ fn(win, keyupEvent, self.handleDismiss);
1939
2541
  }
1940
2542
 
1941
2543
  /**
@@ -1947,61 +2549,82 @@
1947
2549
  }
1948
2550
 
1949
2551
  /**
1950
- * Toggles the visibility of a dropdown or returns false if none is visible.
2552
+ * Hides a visible dropdown.
1951
2553
  * @param {HTMLElement} element
1952
- * @param {boolean=} check
1953
- * @returns {void | boolean}
2554
+ * @returns {void}
1954
2555
  */
1955
- function classToggle(element, check) {
1956
- const fn1 = !check ? 'forEach' : 'some';
1957
- const fn2 = !check ? removeClass : hasClass;
1958
-
2556
+ function removePosition(element) {
1959
2557
  if (element) {
1960
- return ['show', 'show-top'][fn1]((x) => fn2(element, x));
2558
+ ['bottom', 'top'].forEach((x) => removeClass(element, x));
1961
2559
  }
1962
-
1963
- return false;
1964
2560
  }
1965
2561
 
1966
2562
  /**
1967
- * Shows the `ColorPicker` presets menu.
2563
+ * Shows a `ColorPicker` dropdown and close the curent open dropdown.
1968
2564
  * @param {ColorPicker} self
2565
+ * @param {HTMLElement | Element} dropdown
1969
2566
  */
1970
- function showMenu(self) {
1971
- classToggle(self.colorPicker);
1972
- addClass(self.colorMenu, 'show');
2567
+ function showDropdown(self, dropdown) {
2568
+ const {
2569
+ colorPicker, colorMenu, menuToggle, pickerToggle, parent,
2570
+ } = self;
2571
+ const isPicker = dropdown === colorPicker;
2572
+ const openDropdown = isPicker ? colorMenu : colorPicker;
2573
+ const activeBtn = isPicker ? menuToggle : pickerToggle;
2574
+ const nextBtn = !isPicker ? menuToggle : pickerToggle;
2575
+
2576
+ if (!hasClass(parent, 'open')) {
2577
+ addClass(parent, 'open');
2578
+ }
2579
+ if (openDropdown) {
2580
+ removeClass(openDropdown, 'show');
2581
+ removePosition(openDropdown);
2582
+ }
2583
+ addClass(dropdown, 'bottom');
2584
+ reflow(dropdown);
2585
+ addClass(dropdown, 'show');
2586
+ if (isPicker) self.update();
1973
2587
  self.show();
1974
- setAttribute(self.menuToggle, ariaExpanded, 'true');
2588
+ setAttribute(nextBtn, ariaExpanded, 'true');
2589
+ if (activeBtn) {
2590
+ setAttribute(activeBtn, ariaExpanded, 'false');
2591
+ }
1975
2592
  }
1976
2593
 
1977
2594
  /**
1978
- * Color Picker
2595
+ * Color Picker Web Component
1979
2596
  * @see http://thednp.github.io/color-picker
1980
2597
  */
1981
2598
  class ColorPicker {
1982
2599
  /**
1983
- * Returns a new ColorPicker instance.
2600
+ * Returns a new `ColorPicker` instance. The target of this constructor
2601
+ * must be an `HTMLInputElement`.
2602
+ *
1984
2603
  * @param {HTMLInputElement | string} target the target `<input>` element
2604
+ * @param {CP.ColorPickerOptions=} config instance options
1985
2605
  */
1986
- constructor(target) {
2606
+ constructor(target, config) {
1987
2607
  const self = this;
1988
2608
  /** @type {HTMLInputElement} */
1989
2609
  // @ts-ignore
1990
- self.input = querySelector(target);
2610
+ const input = querySelector(target);
2611
+
1991
2612
  // invalidate
1992
- if (!self.input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
1993
- const { input } = self;
2613
+ if (!input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
2614
+ self.input = input;
2615
+
2616
+ const parent = closest(input, colorPickerParentSelector);
2617
+ if (!parent) throw new TypeError('ColorPicker requires a specific markup to work.');
1994
2618
 
1995
2619
  /** @type {HTMLElement} */
1996
2620
  // @ts-ignore
1997
- self.parent = closest(input, `.${colorPickerString},${colorPickerString}`);
1998
- if (!self.parent) throw new TypeError('ColorPicker requires a specific markup to work.');
2621
+ self.parent = parent;
1999
2622
 
2000
2623
  /** @type {number} */
2001
2624
  self.id = getUID(input, colorPickerString);
2002
2625
 
2003
2626
  // set initial state
2004
- /** @type {HTMLCanvasElement?} */
2627
+ /** @type {HTMLElement?} */
2005
2628
  self.dragElement = null;
2006
2629
  /** @type {boolean} */
2007
2630
  self.isOpen = false;
@@ -2011,26 +2634,59 @@
2011
2634
  };
2012
2635
  /** @type {Record<string, string>} */
2013
2636
  self.colorLabels = {};
2014
- /** @type {Array<string> | false} */
2015
- self.keywords = false;
2016
- /** @type {Color} */
2017
- self.color = new Color('white', { format: self.format });
2637
+ /** @type {string[]=} */
2638
+ self.colorKeywords = undefined;
2639
+ /** @type {(ColorPalette | string[])=} */
2640
+ self.colorPresets = undefined;
2641
+
2642
+ // process options
2643
+ const {
2644
+ format, componentLabels, colorLabels, colorKeywords, colorPresets,
2645
+ } = normalizeOptions(this.isCE ? parent : input, colorPickerDefaults, config || {});
2646
+
2647
+ let translatedColorLabels = colorNames;
2648
+ if (colorLabels instanceof Array && colorLabels.length === 17) {
2649
+ translatedColorLabels = colorLabels;
2650
+ } else if (colorLabels && colorLabels.split(',').length === 17) {
2651
+ translatedColorLabels = colorLabels.split(',');
2652
+ }
2653
+
2654
+ // expose colour labels to all methods
2655
+ colorNames.forEach((c, i) => {
2656
+ self.colorLabels[c] = translatedColorLabels[i].trim();
2657
+ });
2658
+
2659
+ // update and expose component labels
2660
+ const tempLabels = ObjectAssign({}, colorPickerLabels);
2661
+ const jsonLabels = componentLabels && isValidJSON(componentLabels)
2662
+ ? JSON.parse(componentLabels) : componentLabels || {};
2663
+
2018
2664
  /** @type {Record<string, string>} */
2019
- self.componentLabels = ObjectAssign({}, colorPickerLabels);
2665
+ self.componentLabels = ObjectAssign(tempLabels, jsonLabels);
2020
2666
 
2021
- const { componentLabels, colorLabels, keywords } = input.dataset;
2022
- const temp = componentLabels ? JSON.parse(componentLabels) : {};
2023
- self.componentLabels = ObjectAssign(self.componentLabels, temp);
2667
+ /** @type {Color} */
2668
+ self.color = new Color('white', format);
2024
2669
 
2025
- const translatedColorLabels = colorLabels && colorLabels.split(',').length === 17
2026
- ? colorLabels.split(',') : colorNames$1;
2670
+ /** @type {CP.ColorFormats} */
2671
+ self.format = format;
2027
2672
 
2028
- // expose color labels to all methods
2029
- colorNames$1.forEach((c, i) => { self.colorLabels[c] = translatedColorLabels[i]; });
2673
+ // set colour defaults
2674
+ if (colorKeywords instanceof Array) {
2675
+ self.colorKeywords = colorKeywords;
2676
+ } else if (typeof colorKeywords === 'string' && colorKeywords.length) {
2677
+ self.colorKeywords = colorKeywords.split(',');
2678
+ }
2030
2679
 
2031
2680
  // set colour presets
2032
- if (keywords !== 'false') {
2033
- self.keywords = keywords ? keywords.split(',') : nonColors;
2681
+ if (colorPresets instanceof Array) {
2682
+ self.colorPresets = colorPresets;
2683
+ } else if (typeof colorPresets === 'string' && colorPresets.length) {
2684
+ if (isValidJSON(colorPresets)) {
2685
+ const { hue, hueSteps, lightSteps } = JSON.parse(colorPresets);
2686
+ self.colorPresets = new ColorPalette(hue, hueSteps, lightSteps);
2687
+ } else {
2688
+ self.colorPresets = colorPresets.split(',').map((x) => x.trim());
2689
+ }
2034
2690
  }
2035
2691
 
2036
2692
  // bind events
@@ -2042,17 +2698,18 @@
2042
2698
  self.pointerDown = self.pointerDown.bind(self);
2043
2699
  self.pointerMove = self.pointerMove.bind(self);
2044
2700
  self.pointerUp = self.pointerUp.bind(self);
2701
+ self.update = self.update.bind(self);
2045
2702
  self.handleScroll = self.handleScroll.bind(self);
2046
2703
  self.handleFocusOut = self.handleFocusOut.bind(self);
2047
2704
  self.changeHandler = self.changeHandler.bind(self);
2048
2705
  self.handleDismiss = self.handleDismiss.bind(self);
2049
- self.keyHandler = self.keyHandler.bind(self);
2706
+ self.keyToggle = self.keyToggle.bind(self);
2050
2707
  self.handleKnobs = self.handleKnobs.bind(self);
2051
2708
 
2052
2709
  // generate markup
2053
2710
  initCallback(self);
2054
2711
 
2055
- const { parent } = self;
2712
+ const [colorPicker, colorMenu] = getElementsByClassName('color-dropdown', parent);
2056
2713
  // set main elements
2057
2714
  /** @type {HTMLElement} */
2058
2715
  // @ts-ignore
@@ -2062,68 +2719,24 @@
2062
2719
  self.menuToggle = querySelector('.menu-toggle', parent);
2063
2720
  /** @type {HTMLElement} */
2064
2721
  // @ts-ignore
2065
- self.colorMenu = querySelector('.color-dropdown.menu', parent);
2066
- /** @type {HTMLElement} */
2067
- // @ts-ignore
2068
- self.colorPicker = querySelector('.color-dropdown.picker', parent);
2722
+ self.colorPicker = colorPicker;
2069
2723
  /** @type {HTMLElement} */
2070
2724
  // @ts-ignore
2071
- self.controls = querySelector('.color-controls', parent);
2725
+ self.colorMenu = colorMenu;
2072
2726
  /** @type {HTMLInputElement[]} */
2073
2727
  // @ts-ignore
2074
- self.inputs = [...querySelectorAll('.color-input', parent)];
2728
+ self.inputs = [...getElementsByClassName('color-input', parent)];
2729
+ const [controls] = getElementsByClassName('color-controls', parent);
2730
+ self.controls = controls;
2731
+ /** @type {(HTMLElement | Element)[]} */
2732
+ self.controlKnobs = [...getElementsByClassName('knob', controls)];
2075
2733
  /** @type {(HTMLElement)[]} */
2076
2734
  // @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);
2735
+ self.visuals = [...getElementsByClassName('visual-control', controls)];
2105
2736
 
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
- }
2737
+ // update colour picker controls, inputs and visuals
2738
+ self.update();
2119
2739
 
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
2740
  // add main events listeners
2128
2741
  toggleEvents(self, true);
2129
2742
 
@@ -2131,65 +2744,52 @@
2131
2744
  Data.set(input, colorPickerString, self);
2132
2745
  }
2133
2746
 
2134
- /** Returns the current color value */
2747
+ /** Returns the current colour value */
2135
2748
  get value() { return this.input.value; }
2136
2749
 
2137
2750
  /**
2138
- * Sets a new color value.
2139
- * @param {string} v new color value
2751
+ * Sets a new colour value.
2752
+ * @param {string} v new colour value
2140
2753
  */
2141
2754
  set value(v) { this.input.value = v; }
2142
2755
 
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. */
2756
+ /** Check if the colour presets include any non-colour. */
2163
2757
  get includeNonColor() {
2164
- return this.keywords instanceof Array
2165
- && this.keywords.some((x) => nonColors.includes(x));
2758
+ return this.colorKeywords instanceof Array
2759
+ && this.colorKeywords.some((x) => nonColors.includes(x));
2166
2760
  }
2167
2761
 
2168
- /** Returns hexadecimal value of the current color. */
2169
- get hex() { return this.color.toHex(); }
2762
+ /** Check if the parent of the target is a `ColorPickerElement` instance. */
2763
+ get isCE() { return this.parent.localName === colorPickerString; }
2170
2764
 
2171
- /** Returns the current color value in {h,s,v,a} object format. */
2765
+ /** Returns hexadecimal value of the current colour. */
2766
+ get hex() { return this.color.toHex(true); }
2767
+
2768
+ /** Returns the current colour value in {h,s,v,a} object format. */
2172
2769
  get hsv() { return this.color.toHsv(); }
2173
2770
 
2174
- /** Returns the current color value in {h,s,l,a} object format. */
2771
+ /** Returns the current colour value in {h,s,l,a} object format. */
2175
2772
  get hsl() { return this.color.toHsl(); }
2176
2773
 
2177
- /** Returns the current color value in {r,g,b,a} object format. */
2774
+ /** Returns the current colour value in {h,w,b,a} object format. */
2775
+ get hwb() { return this.color.toHwb(); }
2776
+
2777
+ /** Returns the current colour value in {r,g,b,a} object format. */
2178
2778
  get rgb() { return this.color.toRgb(); }
2179
2779
 
2180
- /** Returns the current color brightness. */
2780
+ /** Returns the current colour brightness. */
2181
2781
  get brightness() { return this.color.brightness; }
2182
2782
 
2183
- /** Returns the current color luminance. */
2783
+ /** Returns the current colour luminance. */
2184
2784
  get luminance() { return this.color.luminance; }
2185
2785
 
2186
- /** Checks if the current colour requires a light text color. */
2786
+ /** Checks if the current colour requires a light text colour. */
2187
2787
  get isDark() {
2188
- const { rgb, brightness } = this;
2189
- return brightness < 120 && rgb.a > 0.33;
2788
+ const { color, brightness } = this;
2789
+ return brightness < 120 && color.a > 0.33;
2190
2790
  }
2191
2791
 
2192
- /** Checks if the current input value is a valid color. */
2792
+ /** Checks if the current input value is a valid colour. */
2193
2793
  get isValid() {
2194
2794
  const inputValue = this.input.value;
2195
2795
  return inputValue !== '' && new Color(inputValue).isValid;
@@ -2199,89 +2799,79 @@
2199
2799
  updateVisuals() {
2200
2800
  const self = this;
2201
2801
  const {
2202
- color, format, controlPositions,
2203
- width1, width2, width3,
2204
- height1, height2, height3,
2205
- ctx1, ctx2, ctx3,
2802
+ format, controlPositions, visuals,
2206
2803
  } = self;
2207
- const { r, g, b } = color;
2804
+ const [v1, v2, v3] = visuals;
2805
+ const { offsetWidth, offsetHeight } = v1;
2806
+ const hue = format === 'hsl'
2807
+ ? controlPositions.c1x / offsetWidth
2808
+ : controlPositions.c2y / offsetHeight;
2809
+ // @ts-ignore - `hslToRgb` is assigned to `Color` as static method
2810
+ const { r, g, b } = Color.hslToRgb(hue, 1, 0.5);
2811
+ const whiteGrad = 'linear-gradient(rgb(255,255,255) 0%, rgb(255,255,255) 100%)';
2812
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
2813
+ const roundA = Math.round((alpha * 100)) / 100;
2208
2814
 
2209
2815
  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);
2816
+ const fill = new Color({
2817
+ h: hue, s: 1, l: 0.5, a: alpha,
2818
+ }).toRgbString();
2819
+ const hueGradient = `linear-gradient(
2820
+ rgb(255,0,0) 0%, rgb(255,255,0) 16.67%,
2821
+ rgb(0,255,0) 33.33%, rgb(0,255,255) 50%,
2822
+ rgb(0,0,255) 66.67%, rgb(255,0,255) 83.33%,
2823
+ rgb(255,0,0) 100%)`;
2824
+ setElementStyle(v1, {
2825
+ background: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,${roundA}) 100%),
2826
+ linear-gradient(to right, rgba(255,255,255,${roundA}) 0%, ${fill} 100%),
2827
+ ${whiteGrad}`,
2828
+ });
2829
+ setElementStyle(v2, { background: hueGradient });
2236
2830
  } 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);
2831
+ const saturation = Math.round((controlPositions.c2y / offsetHeight) * 100);
2832
+ const fill0 = new Color({
2833
+ r: 255, g: 0, b: 0, a: alpha,
2834
+ }).saturate(-saturation).toRgbString();
2835
+ const fill1 = new Color({
2836
+ r: 255, g: 255, b: 0, a: alpha,
2837
+ }).saturate(-saturation).toRgbString();
2838
+ const fill2 = new Color({
2839
+ r: 0, g: 255, b: 0, a: alpha,
2840
+ }).saturate(-saturation).toRgbString();
2841
+ const fill3 = new Color({
2842
+ r: 0, g: 255, b: 255, a: alpha,
2843
+ }).saturate(-saturation).toRgbString();
2844
+ const fill4 = new Color({
2845
+ r: 0, g: 0, b: 255, a: alpha,
2846
+ }).saturate(-saturation).toRgbString();
2847
+ const fill5 = new Color({
2848
+ r: 255, g: 0, b: 255, a: alpha,
2849
+ }).saturate(-saturation).toRgbString();
2850
+ const fill6 = new Color({
2851
+ r: 255, g: 0, b: 0, a: alpha,
2852
+ }).saturate(-saturation).toRgbString();
2853
+ const fillGradient = `linear-gradient(to right,
2854
+ ${fill0} 0%, ${fill1} 16.67%, ${fill2} 33.33%, ${fill3} 50%,
2855
+ ${fill4} 66.67%, ${fill5} 83.33%, ${fill6} 100%)`;
2856
+ const lightGrad = `linear-gradient(rgba(255,255,255,${roundA}) 0%, rgba(255,255,255,0) 50%),
2857
+ linear-gradient(rgba(0,0,0,0) 50%, rgba(0,0,0,${roundA}) 100%)`;
2858
+
2859
+ setElementStyle(v1, { background: `${lightGrad},${fillGradient},${whiteGrad}` });
2860
+ const {
2861
+ r: gr, g: gg, b: gb,
2862
+ } = new Color({ r, g, b }).greyscale().toRgb();
2863
+
2864
+ setElementStyle(v2, {
2865
+ background: `linear-gradient(rgb(${r},${g},${b}) 0%, rgb(${gr},${gg},${gb}) 100%)`,
2866
+ });
2280
2867
  }
2868
+ setElementStyle(v3, {
2869
+ background: `linear-gradient(rgba(${r},${g},${b},1) 0%,rgba(${r},${g},${b},0) 100%)`,
2870
+ });
2281
2871
  }
2282
2872
 
2283
2873
  /**
2284
- * Handles the `focusout` listener of the `ColorPicker`.
2874
+ * The `ColorPicker` *focusout* event listener when open.
2285
2875
  * @param {FocusEvent} e
2286
2876
  * @this {ColorPicker}
2287
2877
  */
@@ -2293,7 +2883,7 @@
2293
2883
  }
2294
2884
 
2295
2885
  /**
2296
- * Handles the `focusout` listener of the `ColorPicker`.
2886
+ * The `ColorPicker` *keyup* event listener when open.
2297
2887
  * @param {KeyboardEvent} e
2298
2888
  * @this {ColorPicker}
2299
2889
  */
@@ -2305,14 +2895,13 @@
2305
2895
  }
2306
2896
 
2307
2897
  /**
2308
- * Handles the `ColorPicker` scroll listener when open.
2898
+ * The `ColorPicker` *scroll* event listener when open.
2309
2899
  * @param {Event} e
2310
2900
  * @this {ColorPicker}
2311
2901
  */
2312
2902
  handleScroll(e) {
2313
2903
  const self = this;
2314
- /** @type {*} */
2315
- const { activeElement } = document;
2904
+ const { activeElement } = getDocument(self.input);
2316
2905
 
2317
2906
  if ((isMobile && self.dragElement)
2318
2907
  || (activeElement && self.controlKnobs.includes(activeElement))) {
@@ -2324,22 +2913,51 @@
2324
2913
  }
2325
2914
 
2326
2915
  /**
2327
- * Handles all `ColorPicker` click listeners.
2916
+ * The `ColorPicker` keyboard event listener for menu navigation.
2328
2917
  * @param {KeyboardEvent} e
2329
2918
  * @this {ColorPicker}
2330
2919
  */
2331
2920
  menuKeyHandler(e) {
2332
2921
  const { target, code } = e;
2333
-
2334
- if ([keyArrowDown, keyArrowUp].includes(code)) {
2922
+ // @ts-ignore
2923
+ const { previousElementSibling, nextElementSibling, parentElement } = target;
2924
+ const isColorOptionsMenu = parentElement && hasClass(parentElement, 'color-options');
2925
+ const allSiblings = [...parentElement.children];
2926
+ const columnsCount = isColorOptionsMenu
2927
+ && getElementStyle(parentElement, 'grid-template-columns').split(' ').length;
2928
+ const currentIndex = allSiblings.indexOf(target);
2929
+ const previousElement = currentIndex > -1
2930
+ && columnsCount && allSiblings[currentIndex - columnsCount];
2931
+ const nextElement = currentIndex > -1
2932
+ && columnsCount && allSiblings[currentIndex + columnsCount];
2933
+
2934
+ if ([keyArrowDown, keyArrowUp, keySpace].includes(code)) {
2935
+ // prevent scroll when navigating the menu via arrow keys / Space
2335
2936
  e.preventDefault();
2336
- } else if ([keyEnter, keySpace].includes(code)) {
2937
+ }
2938
+ if (isColorOptionsMenu) {
2939
+ if (previousElement && code === keyArrowUp) {
2940
+ focus(previousElement);
2941
+ } else if (nextElement && code === keyArrowDown) {
2942
+ focus(nextElement);
2943
+ } else if (previousElementSibling && code === keyArrowLeft) {
2944
+ focus(previousElementSibling);
2945
+ } else if (nextElementSibling && code === keyArrowRight) {
2946
+ focus(nextElementSibling);
2947
+ }
2948
+ } else if (previousElementSibling && [keyArrowLeft, keyArrowUp].includes(code)) {
2949
+ focus(previousElementSibling);
2950
+ } else if (nextElementSibling && [keyArrowRight, keyArrowDown].includes(code)) {
2951
+ focus(nextElementSibling);
2952
+ }
2953
+
2954
+ if ([keyEnter, keySpace].includes(code)) {
2337
2955
  this.menuClickHandler({ target });
2338
2956
  }
2339
2957
  }
2340
2958
 
2341
2959
  /**
2342
- * Handles all `ColorPicker` click listeners.
2960
+ * The `ColorPicker` click event listener for the colour menu presets / defaults.
2343
2961
  * @param {Partial<Event>} e
2344
2962
  * @this {ColorPicker}
2345
2963
  */
@@ -2347,16 +2965,23 @@
2347
2965
  const self = this;
2348
2966
  /** @type {*} */
2349
2967
  const { target } = e;
2350
- const { format } = self;
2968
+ const { colorMenu } = self;
2351
2969
  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();
2970
+ // invalidate for targets other than color options
2971
+ if (!newOption.length) return;
2972
+ const currentActive = querySelector('li.active', colorMenu);
2973
+ let newColor = nonColors.includes(newOption) ? 'white' : newOption;
2974
+ newColor = newOption === 'transparent' ? 'rgba(0,0,0,0)' : newOption;
2975
+
2976
+ const {
2977
+ r, g, b, a,
2978
+ } = new Color(newColor);
2979
+
2980
+ ObjectAssign(self.color, {
2981
+ r, g, b, a,
2982
+ });
2983
+
2984
+ self.update();
2360
2985
 
2361
2986
  if (currentActive) {
2362
2987
  removeClass(currentActive, 'active');
@@ -2369,29 +2994,28 @@
2369
2994
 
2370
2995
  if (nonColors.includes(newOption)) {
2371
2996
  self.value = newOption;
2372
- firePickerChange(self);
2373
2997
  }
2998
+ firePickerChange(self);
2374
2999
  }
2375
3000
  }
2376
3001
 
2377
3002
  /**
2378
- * Handles the `ColorPicker` touchstart / mousedown events listeners.
3003
+ * The `ColorPicker` *touchstart* / *mousedown* events listener for control knobs.
2379
3004
  * @param {TouchEvent} e
2380
3005
  * @this {ColorPicker}
2381
3006
  */
2382
3007
  pointerDown(e) {
2383
3008
  const self = this;
3009
+ /** @type {*} */
2384
3010
  const {
2385
- // @ts-ignore
2386
3011
  type, target, touches, pageX, pageY,
2387
3012
  } = e;
2388
- const { visuals, controlKnobs, format } = self;
3013
+ const { colorMenu, visuals, controlKnobs } = self;
2389
3014
  const [v1, v2, v3] = visuals;
2390
3015
  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);
3016
+ /** @type {HTMLElement} */
3017
+ const visual = hasClass(target, 'visual-control')
3018
+ ? target : querySelector('.visual-control', target.parentElement);
2395
3019
  const visualRect = getBoundingClientRect(visual);
2396
3020
  const X = type === 'touchstart' ? touches[0].pageX : pageX;
2397
3021
  const Y = type === 'touchstart' ? touches[0].pageY : pageY;
@@ -2400,42 +3024,53 @@
2400
3024
 
2401
3025
  if (target === v1 || target === c1) {
2402
3026
  self.dragElement = visual;
2403
- self.changeControl1({ offsetX, offsetY });
3027
+ self.changeControl1(offsetX, offsetY);
2404
3028
  } else if (target === v2 || target === c2) {
2405
3029
  self.dragElement = visual;
2406
- self.changeControl2({ offsetY });
2407
- } else if (format !== 'hex' && (target === v3 || target === c3)) {
3030
+ self.changeControl2(offsetY);
3031
+ } else if (target === v3 || target === c3) {
2408
3032
  self.dragElement = visual;
2409
- self.changeAlpha({ offsetY });
3033
+ self.changeAlpha(offsetY);
3034
+ }
3035
+
3036
+ if (colorMenu) {
3037
+ const currentActive = querySelector('li.active', colorMenu);
3038
+ if (currentActive) {
3039
+ removeClass(currentActive, 'active');
3040
+ removeAttribute(currentActive, ariaSelected);
3041
+ }
2410
3042
  }
2411
3043
  e.preventDefault();
2412
3044
  }
2413
3045
 
2414
3046
  /**
2415
- * Handles the `ColorPicker` touchend / mouseup events listeners.
3047
+ * The `ColorPicker` *touchend* / *mouseup* events listener for control knobs.
2416
3048
  * @param {TouchEvent} e
2417
3049
  * @this {ColorPicker}
2418
3050
  */
2419
3051
  pointerUp({ target }) {
2420
3052
  const self = this;
2421
- const selection = document.getSelection();
3053
+ const { parent } = self;
3054
+ const doc = getDocument(parent);
3055
+ const currentOpen = querySelector(`${colorPickerParentSelector}.open`, doc) !== null;
3056
+ const selection = doc.getSelection();
2422
3057
  // @ts-ignore
2423
3058
  if (!self.dragElement && !selection.toString().length
2424
3059
  // @ts-ignore
2425
- && !self.parent.contains(target)) {
2426
- self.hide();
3060
+ && !parent.contains(target)) {
3061
+ self.hide(currentOpen);
2427
3062
  }
2428
3063
 
2429
3064
  self.dragElement = null;
2430
3065
  }
2431
3066
 
2432
3067
  /**
2433
- * Handles the `ColorPicker` touchmove / mousemove events listeners.
3068
+ * The `ColorPicker` *touchmove* / *mousemove* events listener for control knobs.
2434
3069
  * @param {TouchEvent} e
2435
3070
  */
2436
3071
  pointerMove(e) {
2437
3072
  const self = this;
2438
- const { dragElement, visuals, format } = self;
3073
+ const { dragElement, visuals } = self;
2439
3074
  const [v1, v2, v3] = visuals;
2440
3075
  const {
2441
3076
  // @ts-ignore
@@ -2451,20 +3086,20 @@
2451
3086
  const offsetY = Y - window.pageYOffset - controlRect.top;
2452
3087
 
2453
3088
  if (dragElement === v1) {
2454
- self.changeControl1({ offsetX, offsetY });
3089
+ self.changeControl1(offsetX, offsetY);
2455
3090
  }
2456
3091
 
2457
3092
  if (dragElement === v2) {
2458
- self.changeControl2({ offsetY });
3093
+ self.changeControl2(offsetY);
2459
3094
  }
2460
3095
 
2461
- if (dragElement === v3 && format !== 'hex') {
2462
- self.changeAlpha({ offsetY });
3096
+ if (dragElement === v3) {
3097
+ self.changeAlpha(offsetY);
2463
3098
  }
2464
3099
  }
2465
3100
 
2466
3101
  /**
2467
- * Handles the `ColorPicker` events listeners associated with the color knobs.
3102
+ * The `ColorPicker` *keydown* event listener for control knobs.
2468
3103
  * @param {KeyboardEvent} e
2469
3104
  */
2470
3105
  handleKnobs(e) {
@@ -2475,10 +3110,10 @@
2475
3110
  if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
2476
3111
  e.preventDefault();
2477
3112
 
2478
- const { activeElement } = document;
2479
3113
  const { controlKnobs } = self;
2480
- const currentKnob = controlKnobs.find((x) => x === activeElement);
2481
3114
  const [c1, c2, c3] = controlKnobs;
3115
+ const { activeElement } = getDocument(c1);
3116
+ const currentKnob = controlKnobs.find((x) => x === activeElement);
2482
3117
 
2483
3118
  if (currentKnob) {
2484
3119
  let offsetX = 0;
@@ -2492,37 +3127,36 @@
2492
3127
 
2493
3128
  offsetX = self.controlPositions.c1x;
2494
3129
  offsetY = self.controlPositions.c1y;
2495
- self.changeControl1({ offsetX, offsetY });
3130
+ self.changeControl1(offsetX, offsetY);
2496
3131
  } else if (target === c2) {
2497
3132
  self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
2498
3133
  offsetY = self.controlPositions.c2y;
2499
- self.changeControl2({ offsetY });
3134
+ self.changeControl2(offsetY);
2500
3135
  } else if (target === c3) {
2501
3136
  self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
2502
3137
  offsetY = self.controlPositions.c3y;
2503
- self.changeAlpha({ offsetY });
3138
+ self.changeAlpha(offsetY);
2504
3139
  }
2505
-
2506
- self.setColorAppearence();
2507
- self.updateInputs();
2508
- self.updateControls();
2509
- self.updateVisuals();
2510
3140
  self.handleScroll(e);
2511
3141
  }
2512
3142
  }
2513
3143
 
2514
- /** Handles the event listeners of the color form. */
3144
+ /** The event listener of the colour form inputs. */
2515
3145
  changeHandler() {
2516
3146
  const self = this;
2517
3147
  let colorSource;
2518
- /** @type {HTMLInputElement} */
2519
- // @ts-ignore
2520
- const { activeElement } = document;
2521
3148
  const {
2522
- inputs, format, value: currentValue, input,
3149
+ inputs, format, value: currentValue, input, controlPositions, visuals,
2523
3150
  } = self;
2524
- const [i1, i2, i3, i4] = inputs;
3151
+ /** @type {*} */
3152
+ const { activeElement } = getDocument(input);
3153
+ const { offsetHeight } = visuals[0];
3154
+ const [i1,,, i4] = inputs;
3155
+ const [v1, v2, v3, v4] = format === 'rgb'
3156
+ ? inputs.map((i) => parseFloat(i.value) / (i === i4 ? 100 : 1))
3157
+ : inputs.map((i) => parseFloat(i.value) / (i !== i1 ? 100 : 360));
2525
3158
  const isNonColorValue = self.includeNonColor && nonColors.includes(currentValue);
3159
+ const alpha = i4 ? v4 : (1 - controlPositions.c3y / offsetHeight);
2526
3160
 
2527
3161
  if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
2528
3162
  if (activeElement === input) {
@@ -2534,14 +3168,28 @@
2534
3168
  } else if (format === 'hex') {
2535
3169
  colorSource = i1.value;
2536
3170
  } else if (format === 'hsl') {
2537
- colorSource = `hsla(${i1.value},${i2.value}%,${i3.value}%,${i4.value})`;
3171
+ colorSource = {
3172
+ h: v1, s: v2, l: v3, a: alpha,
3173
+ };
3174
+ } else if (format === 'hwb') {
3175
+ colorSource = {
3176
+ h: v1, w: v2, b: v3, a: alpha,
3177
+ };
2538
3178
  } else {
2539
- colorSource = `rgba(${inputs.map((x) => x.value).join(',')})`;
3179
+ colorSource = {
3180
+ r: v1, g: v2, b: v3, a: alpha,
3181
+ };
2540
3182
  }
2541
3183
 
2542
- self.color = new Color(colorSource, { format });
3184
+ const {
3185
+ r, g, b, a,
3186
+ } = new Color(colorSource);
3187
+
3188
+ ObjectAssign(self.color, {
3189
+ r, g, b, a,
3190
+ });
2543
3191
  self.setControlPositions();
2544
- self.setColorAppearence();
3192
+ self.updateAppearance();
2545
3193
  self.updateInputs();
2546
3194
  self.updateControls();
2547
3195
  self.updateVisuals();
@@ -2558,49 +3206,57 @@
2558
3206
  * * `lightness` and `saturation` for HEX/RGB;
2559
3207
  * * `lightness` and `hue` for HSL.
2560
3208
  *
2561
- * @param {Record<string, number>} offsets
3209
+ * @param {number} X the X component of the offset
3210
+ * @param {number} Y the Y component of the offset
2562
3211
  */
2563
- changeControl1(offsets) {
3212
+ changeControl1(X, Y) {
2564
3213
  const self = this;
2565
3214
  let [offsetX, offsetY] = [0, 0];
2566
- const { offsetX: X, offsetY: Y } = offsets;
2567
3215
  const {
2568
- format, controlPositions,
2569
- height1, height2, height3, width1,
3216
+ format, controlPositions, visuals,
2570
3217
  } = self;
3218
+ const { offsetHeight, offsetWidth } = visuals[0];
2571
3219
 
2572
- if (X > width1) {
2573
- offsetX = width1;
2574
- } else if (X >= 0) {
2575
- offsetX = X;
2576
- }
3220
+ if (X > offsetWidth) offsetX = offsetWidth;
3221
+ else if (X >= 0) offsetX = X;
2577
3222
 
2578
- if (Y > height1) {
2579
- offsetY = height1;
2580
- } else if (Y >= 0) {
2581
- offsetY = Y;
2582
- }
3223
+ if (Y > offsetHeight) offsetY = offsetHeight;
3224
+ else if (Y >= 0) offsetY = Y;
2583
3225
 
2584
- const hue = format !== 'hsl'
2585
- ? Math.round((controlPositions.c2y / height2) * 360)
2586
- : Math.round((offsetX / width1) * 360);
3226
+ const hue = format === 'hsl'
3227
+ ? offsetX / offsetWidth
3228
+ : controlPositions.c2y / offsetHeight;
2587
3229
 
2588
- const saturation = format !== 'hsl'
2589
- ? Math.round((offsetX / width1) * 100)
2590
- : Math.round((1 - controlPositions.c2y / height2) * 100);
3230
+ const saturation = format === 'hsl'
3231
+ ? 1 - controlPositions.c2y / offsetHeight
3232
+ : offsetX / offsetWidth;
2591
3233
 
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';
3234
+ const lightness = 1 - offsetY / offsetHeight;
3235
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
3236
+
3237
+ const colorObject = format === 'hsl'
3238
+ ? {
3239
+ h: hue, s: saturation, l: lightness, a: alpha,
3240
+ }
3241
+ : {
3242
+ h: hue, s: saturation, v: lightness, a: alpha,
3243
+ };
2595
3244
 
2596
3245
  // new color
2597
- self.color = new Color(`${tempFormat}(${hue},${saturation}%,${lightness}%,${alpha})`, { format });
3246
+ const {
3247
+ r, g, b, a,
3248
+ } = new Color(colorObject);
3249
+
3250
+ ObjectAssign(self.color, {
3251
+ r, g, b, a,
3252
+ });
3253
+
2598
3254
  // new positions
2599
3255
  self.controlPositions.c1x = offsetX;
2600
3256
  self.controlPositions.c1y = offsetY;
2601
3257
 
2602
3258
  // update color picker
2603
- self.setColorAppearence();
3259
+ self.updateAppearance();
2604
3260
  self.updateInputs();
2605
3261
  self.updateControls();
2606
3262
  self.updateVisuals();
@@ -2608,37 +3264,52 @@
2608
3264
 
2609
3265
  /**
2610
3266
  * Updates `ColorPicker` second control:
2611
- * * `hue` for HEX/RGB;
3267
+ * * `hue` for HEX/RGB/HWB;
2612
3268
  * * `saturation` for HSL.
2613
3269
  *
2614
- * @param {Record<string, number>} offset
3270
+ * @param {number} Y the Y offset
2615
3271
  */
2616
- changeControl2(offset) {
3272
+ changeControl2(Y) {
2617
3273
  const self = this;
2618
- const { offsetY: Y } = offset;
2619
3274
  const {
2620
- format, width1, height1, height2, height3, controlPositions,
3275
+ format, controlPositions, visuals,
2621
3276
  } = self;
2622
- let offsetY = 0;
3277
+ const { offsetHeight, offsetWidth } = visuals[0];
2623
3278
 
2624
- if (Y > height2) {
2625
- offsetY = height2;
2626
- } else if (Y >= 0) {
2627
- offsetY = Y;
2628
- }
3279
+ let offsetY = 0;
2629
3280
 
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';
3281
+ if (Y > offsetHeight) offsetY = offsetHeight;
3282
+ else if (Y >= 0) offsetY = Y;
3283
+
3284
+ const hue = format === 'hsl'
3285
+ ? controlPositions.c1x / offsetWidth
3286
+ : offsetY / offsetHeight;
3287
+ const saturation = format === 'hsl'
3288
+ ? 1 - offsetY / offsetHeight
3289
+ : controlPositions.c1x / offsetWidth;
3290
+ const lightness = 1 - controlPositions.c1y / offsetHeight;
3291
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
3292
+ const colorObject = format === 'hsl'
3293
+ ? {
3294
+ h: hue, s: saturation, l: lightness, a: alpha,
3295
+ }
3296
+ : {
3297
+ h: hue, s: saturation, v: lightness, a: alpha,
3298
+ };
2635
3299
 
2636
3300
  // new color
2637
- self.color = new Color(`${colorFormat}(${hue},${saturation}%,${lightness}%,${alpha})`, { format });
3301
+ const {
3302
+ r, g, b, a,
3303
+ } = new Color(colorObject);
3304
+
3305
+ ObjectAssign(self.color, {
3306
+ r, g, b, a,
3307
+ });
3308
+
2638
3309
  // new position
2639
3310
  self.controlPositions.c2y = offsetY;
2640
3311
  // update color picker
2641
- self.setColorAppearence();
3312
+ self.updateAppearance();
2642
3313
  self.updateInputs();
2643
3314
  self.updateControls();
2644
3315
  self.updateVisuals();
@@ -2646,92 +3317,105 @@
2646
3317
 
2647
3318
  /**
2648
3319
  * Updates `ColorPicker` last control,
2649
- * the `alpha` channel for RGB/HSL.
3320
+ * the `alpha` channel.
2650
3321
  *
2651
- * @param {Record<string, number>} offset
3322
+ * @param {number} Y
2652
3323
  */
2653
- changeAlpha(offset) {
3324
+ changeAlpha(Y) {
2654
3325
  const self = this;
2655
- const { height3 } = self;
2656
- const { offsetY: Y } = offset;
3326
+ const { visuals } = self;
3327
+ const { offsetHeight } = visuals[0];
2657
3328
  let offsetY = 0;
2658
3329
 
2659
- if (Y > height3) {
2660
- offsetY = height3;
2661
- } else if (Y >= 0) {
2662
- offsetY = Y;
2663
- }
3330
+ if (Y > offsetHeight) offsetY = offsetHeight;
3331
+ else if (Y >= 0) offsetY = Y;
2664
3332
 
2665
3333
  // update color alpha
2666
- const alpha = Math.round((1 - offsetY / height3) * 100);
2667
- self.color.setAlpha(alpha / 100);
3334
+ const alpha = 1 - offsetY / offsetHeight;
3335
+ self.color.setAlpha(alpha);
2668
3336
  // update position
2669
3337
  self.controlPositions.c3y = offsetY;
2670
3338
  // update color picker
3339
+ self.updateAppearance();
2671
3340
  self.updateInputs();
2672
3341
  self.updateControls();
2673
- // alpha?
2674
3342
  self.updateVisuals();
2675
3343
  }
2676
3344
 
2677
- /** Update opened dropdown position on scroll. */
3345
+ /**
3346
+ * Updates `ColorPicker` control positions on:
3347
+ * * initialization
3348
+ * * window resize
3349
+ */
3350
+ update() {
3351
+ const self = this;
3352
+ self.updateDropdownPosition();
3353
+ self.updateAppearance();
3354
+ self.setControlPositions();
3355
+ self.updateInputs(true);
3356
+ self.updateControls();
3357
+ self.updateVisuals();
3358
+ }
3359
+
3360
+ /** Updates the open dropdown position on *scroll* event. */
2678
3361
  updateDropdownPosition() {
2679
3362
  const self = this;
2680
3363
  const { input, colorPicker, colorMenu } = self;
2681
3364
  const elRect = getBoundingClientRect(input);
3365
+ const { top, bottom } = elRect;
2682
3366
  const { offsetHeight: elHeight } = input;
2683
- const windowHeight = document.documentElement.clientHeight;
2684
- const isPicker = classToggle(colorPicker, true);
3367
+ const windowHeight = getDocumentElement(input).clientHeight;
3368
+ const isPicker = hasClass(colorPicker, 'show');
2685
3369
  const dropdown = isPicker ? colorPicker : colorMenu;
3370
+ if (!dropdown) return;
2686
3371
  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');
3372
+ const distanceBottom = windowHeight - bottom;
3373
+ const distanceTop = top;
3374
+ const bottomExceed = top + dropHeight + elHeight > windowHeight; // show
3375
+ const topExceed = top - dropHeight < 0; // show-top
3376
+
3377
+ if ((hasClass(dropdown, 'bottom') || !topExceed) && distanceBottom < distanceTop && bottomExceed) {
3378
+ removeClass(dropdown, 'bottom');
3379
+ addClass(dropdown, 'top');
3380
+ } else {
3381
+ removeClass(dropdown, 'top');
3382
+ addClass(dropdown, 'bottom');
2699
3383
  }
2700
3384
  }
2701
3385
 
2702
- /** Update control knobs' positions. */
3386
+ /** Updates control knobs' positions. */
2703
3387
  setControlPositions() {
2704
3388
  const self = this;
2705
3389
  const {
2706
- hsv, hsl, format, height1, height2, height3, width1,
3390
+ format, visuals, color, hsl, hsv,
2707
3391
  } = self;
3392
+ const { offsetHeight, offsetWidth } = visuals[0];
3393
+ const alpha = color.a;
2708
3394
  const hue = hsl.h;
3395
+
2709
3396
  const saturation = format !== 'hsl' ? hsv.s : hsl.s;
2710
3397
  const lightness = format !== 'hsl' ? hsv.v : hsl.l;
2711
- const alpha = hsv.a;
2712
3398
 
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
-
2717
- if (format !== 'hex') {
2718
- self.controlPositions.c3y = (1 - alpha) * height3;
2719
- }
3399
+ self.controlPositions.c1x = format !== 'hsl' ? saturation * offsetWidth : hue * offsetWidth;
3400
+ self.controlPositions.c1y = (1 - lightness) * offsetHeight;
3401
+ self.controlPositions.c2y = format !== 'hsl' ? hue * offsetHeight : (1 - saturation) * offsetHeight;
3402
+ self.controlPositions.c3y = (1 - alpha) * offsetHeight;
2720
3403
  }
2721
3404
 
2722
- /** Update the visual appearance label. */
2723
- setColorAppearence() {
3405
+ /** Update the visual appearance label and control knob labels. */
3406
+ updateAppearance() {
2724
3407
  const self = this;
2725
3408
  const {
2726
- componentLabels, colorLabels, hsl, hsv, hex, format, knobLabels,
3409
+ componentLabels, colorLabels, color, parent,
3410
+ hsl, hsv, hex, format, controlKnobs,
2727
3411
  } = self;
2728
3412
  const {
2729
- lightnessLabel, saturationLabel, hueLabel, alphaLabel, appearanceLabel, hexLabel,
3413
+ appearanceLabel, hexLabel, valueLabel,
2730
3414
  } = componentLabels;
2731
- let { requiredLabel } = componentLabels;
2732
- const [knob1Lbl, knob2Lbl, knob3Lbl] = knobLabels;
2733
- const hue = Math.round(hsl.h);
2734
- const alpha = hsv.a;
3415
+ const { r, g, b } = color.toRgb();
3416
+ const [knob1, knob2, knob3] = controlKnobs;
3417
+ const hue = Math.round(hsl.h * 360);
3418
+ const alpha = color.a;
2735
3419
  const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
2736
3420
  const saturation = Math.round(saturationSource * 100);
2737
3421
  const lightness = Math.round(hsl.l * 100);
@@ -2770,99 +3454,111 @@
2770
3454
  colorName = colorLabels.pink;
2771
3455
  }
2772
3456
 
3457
+ let colorLabel = `${hexLabel} ${hex.split('').join(' ')}`;
3458
+
2773
3459
  if (format === 'hsl') {
2774
- knob1Lbl.innerText = `${hueLabel}: ${hue}°. ${lightnessLabel}: ${lightness}%`;
2775
- knob2Lbl.innerText = `${saturationLabel}: ${saturation}%`;
3460
+ colorLabel = `HSL: ${hue}°, ${saturation}%, ${lightness}%`;
3461
+ setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3462
+ setAttribute(knob1, ariaValueText, `${hue}° & ${lightness}%`);
3463
+ setAttribute(knob1, ariaValueNow, `${hue}`);
3464
+ setAttribute(knob2, ariaValueText, `${saturation}%`);
3465
+ setAttribute(knob2, ariaValueNow, `${saturation}`);
3466
+ } else if (format === 'hwb') {
3467
+ const { hwb } = self;
3468
+ const whiteness = Math.round(hwb.w * 100);
3469
+ const blackness = Math.round(hwb.b * 100);
3470
+ colorLabel = `HWB: ${hue}°, ${whiteness}%, ${blackness}%`;
3471
+ setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3472
+ setAttribute(knob1, ariaValueText, `${whiteness}% & ${blackness}%`);
3473
+ setAttribute(knob1, ariaValueNow, `${whiteness}`);
3474
+ setAttribute(knob2, ariaValueText, `${hue}%`);
3475
+ setAttribute(knob2, ariaValueNow, `${hue}`);
2776
3476
  } else {
2777
- knob1Lbl.innerText = `${lightnessLabel}: ${lightness}%. ${saturationLabel}: ${saturation}%`;
2778
- knob2Lbl.innerText = `${hueLabel}: ${hue}°`;
3477
+ colorLabel = format === 'rgb' ? `RGB: ${r}, ${g}, ${b}` : colorLabel;
3478
+ setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3479
+ setAttribute(knob1, ariaValueText, `${lightness}% & ${saturation}%`);
3480
+ setAttribute(knob1, ariaValueNow, `${lightness}`);
3481
+ setAttribute(knob2, ariaValueText, `${hue}°`);
3482
+ setAttribute(knob2, ariaValueNow, `${hue}`);
2779
3483
  }
2780
3484
 
2781
- if (format !== 'hex') {
2782
- const alphaValue = Math.round(alpha * 100);
2783
- knob3Lbl.innerText = `${alphaLabel}: ${alphaValue}%`;
2784
- }
3485
+ const alphaValue = Math.round(alpha * 100);
3486
+ setAttribute(knob3, ariaValueText, `${alphaValue}%`);
3487
+ setAttribute(knob3, ariaValueNow, `${alphaValue}`);
2785
3488
 
2786
- // update color labels
2787
- self.appearance.innerText = `${appearanceLabel}: ${colorName}.`;
2788
- const colorLabel = format === 'hex'
2789
- ? `${hexLabel} ${hex.split('').join(' ')}.`
2790
- : self.value.toUpperCase();
3489
+ // update the input backgroundColor
3490
+ const newColor = color.toString();
3491
+ setElementStyle(self.input, { backgroundColor: newColor });
2791
3492
 
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}`;
3493
+ // toggle dark/light classes will also style the placeholder
3494
+ // dark sets color white, light sets color black
3495
+ // isDark ? '#000' : '#fff'
3496
+ if (!self.isDark) {
3497
+ if (hasClass(parent, 'txt-dark')) removeClass(parent, 'txt-dark');
3498
+ if (!hasClass(parent, 'txt-light')) addClass(parent, 'txt-light');
3499
+ } else {
3500
+ if (hasClass(parent, 'txt-light')) removeClass(parent, 'txt-light');
3501
+ if (!hasClass(parent, 'txt-dark')) addClass(parent, 'txt-dark');
2799
3502
  }
2800
3503
  }
2801
3504
 
2802
- /** Updates the control knobs positions. */
3505
+ /** Updates the control knobs actual positions. */
2803
3506
  updateControls() {
2804
- const { format, controlKnobs, controlPositions } = this;
3507
+ const { controlKnobs, controlPositions } = this;
2805
3508
  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)`;
2808
-
2809
- if (format !== 'hex') {
2810
- control3.style.transform = `translate3d(0,${controlPositions.c3y - 3}px,0)`;
2811
- }
3509
+ setElementStyle(control1, { transform: `translate3d(${controlPositions.c1x - 4}px,${controlPositions.c1y - 4}px,0)` });
3510
+ setElementStyle(control2, { transform: `translate3d(0,${controlPositions.c2y - 4}px,0)` });
3511
+ setElementStyle(control3, { transform: `translate3d(0,${controlPositions.c3y - 4}px,0)` });
2812
3512
  }
2813
3513
 
2814
3514
  /**
2815
- * Update all color form inputs.
3515
+ * Updates all color form inputs.
2816
3516
  * @param {boolean=} isPrevented when `true`, the component original event is prevented
2817
3517
  */
2818
3518
  updateInputs(isPrevented) {
2819
3519
  const self = this;
2820
3520
  const {
2821
- value: oldColor, rgb, hsl, hsv, format, parent, input, inputs,
3521
+ value: oldColor, format, inputs, color, hsl,
2822
3522
  } = self;
2823
3523
  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);
3524
+ const alpha = Math.round(color.a * 100);
3525
+ const hue = Math.round(hsl.h * 360);
2830
3526
  let newColor;
2831
3527
 
2832
3528
  if (format === 'hex') {
2833
- newColor = self.color.toHexString();
3529
+ newColor = self.color.toHexString(true);
2834
3530
  i1.value = self.hex;
2835
3531
  } else if (format === 'hsl') {
3532
+ const lightness = Math.round(hsl.l * 100);
3533
+ const saturation = Math.round(hsl.s * 100);
2836
3534
  newColor = self.color.toHslString();
2837
3535
  i1.value = `${hue}`;
2838
3536
  i2.value = `${saturation}`;
2839
3537
  i3.value = `${lightness}`;
2840
3538
  i4.value = `${alpha}`;
3539
+ } else if (format === 'hwb') {
3540
+ const { w, b } = self.hwb;
3541
+ const whiteness = Math.round(w * 100);
3542
+ const blackness = Math.round(b * 100);
3543
+
3544
+ newColor = self.color.toHwbString();
3545
+ i1.value = `${hue}`;
3546
+ i2.value = `${whiteness}`;
3547
+ i3.value = `${blackness}`;
3548
+ i4.value = `${alpha}`;
2841
3549
  } else if (format === 'rgb') {
3550
+ const { r, g, b } = self.rgb;
3551
+
2842
3552
  newColor = self.color.toRgbString();
2843
- i1.value = `${rgb.r}`;
2844
- i2.value = `${rgb.g}`;
2845
- i3.value = `${rgb.b}`;
3553
+ i1.value = `${r}`;
3554
+ i2.value = `${g}`;
3555
+ i3.value = `${b}`;
2846
3556
  i4.value = `${alpha}`;
2847
3557
  }
2848
3558
 
2849
3559
  // update the color value
2850
3560
  self.value = `${newColor}`;
2851
3561
 
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
3562
  // don't trigger the custom event unless it's really changed
2867
3563
  if (!isPrevented && newColor !== oldColor) {
2868
3564
  firePickerChange(self);
@@ -2870,14 +3566,15 @@
2870
3566
  }
2871
3567
 
2872
3568
  /**
2873
- * Handles the `Space` and `Enter` keys inputs.
3569
+ * The `Space` & `Enter` keys specific event listener.
3570
+ * Toggle visibility of the `ColorPicker` / the presets menu, showing one will hide the other.
2874
3571
  * @param {KeyboardEvent} e
2875
3572
  * @this {ColorPicker}
2876
3573
  */
2877
- keyHandler(e) {
3574
+ keyToggle(e) {
2878
3575
  const self = this;
2879
3576
  const { menuToggle } = self;
2880
- const { activeElement } = document;
3577
+ const { activeElement } = getDocument(menuToggle);
2881
3578
  const { code } = e;
2882
3579
 
2883
3580
  if ([keyEnter, keySpace].includes(code)) {
@@ -2900,80 +3597,94 @@
2900
3597
  togglePicker(e) {
2901
3598
  e.preventDefault();
2902
3599
  const self = this;
2903
- const pickerIsOpen = classToggle(self.colorPicker, true);
3600
+ const { colorPicker } = self;
2904
3601
 
2905
- if (self.isOpen && pickerIsOpen) {
3602
+ if (self.isOpen && hasClass(colorPicker, 'show')) {
2906
3603
  self.hide(true);
2907
3604
  } else {
2908
- self.showPicker();
3605
+ showDropdown(self, colorPicker);
2909
3606
  }
2910
3607
  }
2911
3608
 
2912
3609
  /** Shows the `ColorPicker` dropdown. */
2913
3610
  showPicker() {
2914
3611
  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');
3612
+ const { colorPicker } = self;
3613
+
3614
+ if (!hasClass(colorPicker, 'show')) {
3615
+ showDropdown(self, colorPicker);
3616
+ }
2920
3617
  }
2921
3618
 
2922
3619
  /** Toggles the visibility of the `ColorPicker` presets menu. */
2923
3620
  toggleMenu() {
2924
3621
  const self = this;
2925
- const menuIsOpen = classToggle(self.colorMenu, true);
3622
+ const { colorMenu } = self;
2926
3623
 
2927
- if (self.isOpen && menuIsOpen) {
3624
+ if (self.isOpen && hasClass(colorMenu, 'show')) {
2928
3625
  self.hide(true);
2929
3626
  } else {
2930
- showMenu(self);
3627
+ showDropdown(self, colorMenu);
2931
3628
  }
2932
3629
  }
2933
3630
 
2934
- /** Show the dropdown. */
3631
+ /** Shows the `ColorPicker` dropdown or the presets menu. */
2935
3632
  show() {
2936
3633
  const self = this;
3634
+ const { menuToggle } = self;
2937
3635
  if (!self.isOpen) {
2938
- addClass(self.parent, 'open');
2939
3636
  toggleEventsOnShown(self, true);
2940
3637
  self.updateDropdownPosition();
2941
3638
  self.isOpen = true;
3639
+ setAttribute(self.input, 'tabindex', '0');
3640
+ if (menuToggle) {
3641
+ setAttribute(menuToggle, 'tabindex', '0');
3642
+ }
2942
3643
  }
2943
3644
  }
2944
3645
 
2945
3646
  /**
2946
- * Hides the currently opened dropdown.
3647
+ * Hides the currently open `ColorPicker` dropdown.
2947
3648
  * @param {boolean=} focusPrevented
2948
3649
  */
2949
3650
  hide(focusPrevented) {
2950
3651
  const self = this;
2951
3652
  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');
3653
+ const {
3654
+ pickerToggle, menuToggle, colorPicker, colorMenu, parent, input,
3655
+ } = self;
3656
+ const openPicker = hasClass(colorPicker, 'show');
3657
+ const openDropdown = openPicker ? colorPicker : colorMenu;
3658
+ const relatedBtn = openPicker ? pickerToggle : menuToggle;
3659
+ const animationDuration = openDropdown && getElementTransitionDuration(openDropdown);
3660
+
3661
+ if (openDropdown) {
3662
+ removeClass(openDropdown, 'show');
3663
+ setAttribute(relatedBtn, ariaExpanded, 'false');
3664
+ setTimeout(() => {
3665
+ removePosition(openDropdown);
3666
+ if (!querySelector('.show', parent)) {
3667
+ removeClass(parent, 'open');
3668
+ toggleEventsOnShown(self);
3669
+ self.isOpen = false;
3670
+ }
3671
+ }, animationDuration);
2963
3672
  }
2964
3673
 
2965
3674
  if (!self.isValid) {
2966
3675
  self.value = self.color.toString();
2967
3676
  }
2968
-
2969
- self.isOpen = false;
2970
-
2971
3677
  if (!focusPrevented) {
2972
- pickerToggle.focus();
3678
+ focus(pickerToggle);
3679
+ }
3680
+ setAttribute(input, 'tabindex', '-1');
3681
+ if (menuToggle) {
3682
+ setAttribute(menuToggle, 'tabindex', '-1');
2973
3683
  }
2974
3684
  }
2975
3685
  }
2976
3686
 
3687
+ /** Removes `ColorPicker` from target `<input>`. */
2977
3688
  dispose() {
2978
3689
  const self = this;
2979
3690
  const { input, parent } = self;
@@ -2982,25 +3693,20 @@
2982
3693
  [...parent.children].forEach((el) => {
2983
3694
  if (el !== input) el.remove();
2984
3695
  });
3696
+ setElementStyle(input, { backgroundColor: '' });
3697
+ ['txt-light', 'txt-dark'].forEach((c) => removeClass(parent, c));
2985
3698
  Data.remove(input, colorPickerString);
2986
3699
  }
2987
3700
  }
2988
3701
 
2989
3702
  ObjectAssign(ColorPicker, {
2990
3703
  Color,
3704
+ Version,
2991
3705
  getInstance: getColorPickerInstance,
2992
3706
  init: initColorPicker,
2993
3707
  selector: colorPickerSelector,
2994
3708
  });
2995
3709
 
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
3710
  return ColorPicker;
3005
3711
 
3006
3712
  })));