@thednp/color-picker 0.0.1-alpha3 → 0.0.2-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.
@@ -1,13 +1,13 @@
1
1
  /*!
2
- * ColorPicker v0.0.1alpha3 (http://thednp.github.io/color-picker)
2
+ * ColorPicker v0.0.2alpha2 (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
  */
6
6
  (function (global, factory) {
7
7
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
8
8
  typeof define === 'function' && define.amd ? define(factory) :
9
- (global = global || self, global.ColorPicker = factory());
10
- }(this, (function () { 'use strict';
9
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ColorPicker = factory());
10
+ })(this, (function () { 'use strict';
11
11
 
12
12
  /** @type {Record<string, any>} */
13
13
  const EventRegistry = {};
@@ -135,24 +135,6 @@
135
135
  */
136
136
  const ariaValueNow = 'aria-valuenow';
137
137
 
138
- /**
139
- * A global namespace for aria-haspopup.
140
- * @type {string}
141
- */
142
- const ariaHasPopup = 'aria-haspopup';
143
-
144
- /**
145
- * A global namespace for aria-hidden.
146
- * @type {string}
147
- */
148
- const ariaHidden = 'aria-hidden';
149
-
150
- /**
151
- * A global namespace for aria-labelledby.
152
- * @type {string}
153
- */
154
- const ariaLabelledBy = 'aria-labelledby';
155
-
156
138
  /**
157
139
  * A global namespace for `ArrowDown` key.
158
140
  * @type {string} e.which = 40 equivalent
@@ -279,37 +261,6 @@
279
261
  */
280
262
  const focusoutEvent = 'focusout';
281
263
 
282
- // @ts-ignore
283
- const { userAgentData: uaDATA } = navigator;
284
-
285
- /**
286
- * A global namespace for `userAgentData` object.
287
- */
288
- const userAgentData = uaDATA;
289
-
290
- const { userAgent: userAgentString } = navigator;
291
-
292
- /**
293
- * A global namespace for `navigator.userAgent` string.
294
- */
295
- const userAgent = userAgentString;
296
-
297
- const mobileBrands = /iPhone|iPad|iPod|Android/i;
298
- let isMobileCheck = false;
299
-
300
- if (userAgentData) {
301
- isMobileCheck = userAgentData.brands
302
- .some((/** @type {Record<String, any>} */x) => mobileBrands.test(x.brand));
303
- } else {
304
- isMobileCheck = mobileBrands.test(userAgent);
305
- }
306
-
307
- /**
308
- * A global `boolean` for mobile detection.
309
- * @type {boolean}
310
- */
311
- const isMobile = isMobileCheck;
312
-
313
264
  /**
314
265
  * Returns the `document` or the `#document` element.
315
266
  * @see https://github.com/floating-ui/floating-ui
@@ -530,60 +481,6 @@
530
481
  return lookUp.getElementsByClassName(selector);
531
482
  }
532
483
 
533
- /**
534
- * Shortcut for `Object.assign()` static method.
535
- * @param {Record<string, any>} obj a target object
536
- * @param {Record<string, any>} source a source object
537
- */
538
- const ObjectAssign = (obj, source) => Object.assign(obj, source);
539
-
540
- /**
541
- * This is a shortie for `document.createElement` method
542
- * which allows you to create a new `HTMLElement` for a given `tagName`
543
- * or based on an object with specific non-readonly attributes:
544
- * `id`, `className`, `textContent`, `style`, etc.
545
- * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
546
- *
547
- * @param {Record<string, string> | string} param `tagName` or object
548
- * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
549
- */
550
- function createElement(param) {
551
- if (typeof param === 'string') {
552
- return getDocument().createElement(param);
553
- }
554
-
555
- const { tagName } = param;
556
- const attr = { ...param };
557
- const newElement = createElement(tagName);
558
- delete attr.tagName;
559
- ObjectAssign(newElement, attr);
560
- return newElement;
561
- }
562
-
563
- /**
564
- * This is a shortie for `document.createElementNS` method
565
- * which allows you to create a new `HTMLElement` for a given `tagName`
566
- * or based on an object with specific non-readonly attributes:
567
- * `id`, `className`, `textContent`, `style`, etc.
568
- * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
569
- *
570
- * @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
571
- * @param {Record<string, string> | string} param `tagName` or object
572
- * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
573
- */
574
- function createElementNS(namespace, param) {
575
- if (typeof param === 'string') {
576
- return getDocument().createElementNS(namespace, param);
577
- }
578
-
579
- const { tagName } = param;
580
- const attr = { ...param };
581
- const newElement = createElementNS(namespace, tagName);
582
- delete attr.tagName;
583
- ObjectAssign(newElement, attr);
584
- return newElement;
585
- }
586
-
587
484
  /**
588
485
  * Shortcut for the `Element.dispatchEvent(Event)` method.
589
486
  *
@@ -592,6 +489,13 @@
592
489
  */
593
490
  const dispatchEvent = (element, event) => element.dispatchEvent(event);
594
491
 
492
+ /**
493
+ * Shortcut for `Object.assign()` static method.
494
+ * @param {Record<string, any>} obj a target object
495
+ * @param {Record<string, any>} source a source object
496
+ */
497
+ const ObjectAssign = (obj, source) => Object.assign(obj, source);
498
+
595
499
  /** @type {Map<string, Map<HTMLElement | Element, Record<string, any>>>} */
596
500
  const componentData = new Map();
597
501
  /**
@@ -667,20 +571,13 @@
667
571
  */
668
572
  const getInstance = (target, component) => Data.get(target, component);
669
573
 
670
- /**
671
- * Shortcut for `Object.keys()` static method.
672
- * @param {Record<string, any>} obj a target object
673
- * @returns {string[]}
674
- */
675
- const ObjectKeys = (obj) => Object.keys(obj);
676
-
677
574
  /**
678
575
  * Shortcut for multiple uses of `HTMLElement.style.propertyName` method.
679
576
  * @param {HTMLElement | Element} element target element
680
577
  * @param {Partial<CSSStyleDeclaration>} styles attribute value
681
578
  */
682
579
  // @ts-ignore
683
- const setElementStyle = (element, styles) => ObjectAssign(element.style, styles);
580
+ const setElementStyle = (element, styles) => { ObjectAssign(element.style, styles); };
684
581
 
685
582
  /**
686
583
  * Shortcut for `HTMLElement.getAttribute()` method.
@@ -723,6 +620,13 @@
723
620
  return value;
724
621
  }
725
622
 
623
+ /**
624
+ * Shortcut for `Object.keys()` static method.
625
+ * @param {Record<string, any>} obj a target object
626
+ * @returns {string[]}
627
+ */
628
+ const ObjectKeys = (obj) => Object.keys(obj);
629
+
726
630
  /**
727
631
  * Shortcut for `String.toLowerCase()`.
728
632
  *
@@ -843,32 +747,10 @@
843
747
  */
844
748
  const removeAttribute = (element, attribute) => element.removeAttribute(attribute);
845
749
 
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
750
  /**
868
- * A list of 17 color names used for WAI-ARIA compliance.
869
- * @type {string[]}
751
+ * A global namespace for `document.head`.
870
752
  */
871
- const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
753
+ const { head: documentHead } = document;
872
754
 
873
755
  /**
874
756
  * A list of explicit default non-color values.
@@ -876,297 +758,99 @@
876
758
  const nonColors = ['transparent', 'currentColor', 'inherit', 'revert', 'initial'];
877
759
 
878
760
  /**
879
- * Shortcut for `String.toUpperCase()`.
880
- *
881
- * @param {string} source input string
882
- * @returns {string} uppercase output string
761
+ * Round colour components, for all formats except HEX.
762
+ * @param {number} v one of the colour components
763
+ * @returns {number} the rounded number
883
764
  */
884
- const toUpperCase = (source) => source.toUpperCase();
765
+ function roundPart(v) {
766
+ const floor = Math.floor(v);
767
+ return v - floor < 0.5 ? floor : Math.round(v);
768
+ }
885
769
 
886
- const vHidden = 'v-hidden';
770
+ // Color supported formats
771
+ const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsv', 'hwb'];
887
772
 
888
- /**
889
- * Returns the color form for `ColorPicker`.
890
- *
891
- * @param {CP.ColorPicker} self the `ColorPicker` instance
892
- * @returns {HTMLElement | Element} a new `<div>` element with color component `<input>`
893
- */
894
- function getColorForm(self) {
895
- const { format, id, componentLabels } = self;
896
- const colorForm = createElement({
897
- tagName: 'div',
898
- className: `color-form ${format}`,
899
- });
773
+ // Hue angles
774
+ const ANGLES = 'deg|rad|grad|turn';
900
775
 
901
- let components = ['hex'];
902
- if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
903
- else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
904
- else if (format === 'hwb') components = ['hue', 'whiteness', 'blackness', 'alpha'];
776
+ // <http://www.w3.org/TR/css3-values/#integers>
777
+ const CSS_INTEGER = '[-\\+]?\\d+%?';
905
778
 
906
- components.forEach((c) => {
907
- const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
908
- const cID = `color_${format}_${c}_${id}`;
909
- const formatLabel = componentLabels[`${c}Label`];
910
- const cInputLabel = createElement({ tagName: 'label' });
911
- setAttribute(cInputLabel, 'for', cID);
912
- cInputLabel.append(
913
- createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
914
- createElement({ tagName: 'span', className: vHidden, innerText: formatLabel }),
915
- );
916
- const cInput = createElement({
917
- tagName: 'input',
918
- id: cID,
919
- // name: cID, - prevent saving the value to a form
920
- type: format === 'hex' ? 'text' : 'number',
921
- value: c === 'alpha' ? '100' : '0',
922
- className: `color-input ${c}`,
923
- });
924
- setAttribute(cInput, 'autocomplete', 'off');
925
- setAttribute(cInput, 'spellcheck', 'false');
779
+ // Include CSS3 Module
780
+ // <http://www.w3.org/TR/css3-values/#number-value>
781
+ const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
926
782
 
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';
935
- }
936
- }
937
- ObjectAssign(cInput, {
938
- min: '0',
939
- max,
940
- step,
941
- });
942
- colorForm.append(cInputLabel, cInput);
943
- });
944
- return colorForm;
945
- }
783
+ // Include CSS4 Module Hue degrees unit
784
+ // <https://www.w3.org/TR/css3-values/#angle-value>
785
+ const CSS_ANGLE = `[-\\+]?\\d*\\.?\\d+(?:${ANGLES})?`;
786
+
787
+ // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
788
+ const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
789
+
790
+ // Add angles to the mix
791
+ const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
792
+
793
+ // Start & end
794
+ const START_MATCH = '(?:[\\s|\\(\\s|\\s\\(\\s]+)?';
795
+ const END_MATCH = '(?:[\\s|\\)\\s]+)?';
796
+ // Components separation
797
+ const SEP = '(?:[,|\\s]+)';
798
+ const SEP2 = '(?:[,|\\/\\s]*)?';
799
+
800
+ // Actual matching.
801
+ // Parentheses and commas are optional, but not required.
802
+ // Whitespace can take the place of commas or opening paren
803
+ const PERMISSIVE_MATCH = `${START_MATCH}(${CSS_UNIT2})${SEP}(${CSS_UNIT})${SEP}(${CSS_UNIT})${SEP2}(${CSS_UNIT})?${END_MATCH}`;
804
+
805
+ const matchers = {
806
+ CSS_UNIT: new RegExp(CSS_UNIT2),
807
+ hwb: new RegExp(`hwb${PERMISSIVE_MATCH}`),
808
+ rgb: new RegExp(`rgb(?:a)?${PERMISSIVE_MATCH}`),
809
+ hsl: new RegExp(`hsl(?:a)?${PERMISSIVE_MATCH}`),
810
+ hsv: new RegExp(`hsv(?:a)?${PERMISSIVE_MATCH}`),
811
+ hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
812
+ hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
813
+ hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
814
+ hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
815
+ };
946
816
 
947
817
  /**
948
- * A global namespace for aria-label.
949
- * @type {string}
818
+ * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
819
+ * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
820
+ * @param {string} n testing number
821
+ * @returns {boolean} the query result
950
822
  */
951
- const ariaLabel = 'aria-label';
823
+ function isOnePointZero(n) {
824
+ return `${n}`.includes('.') && parseFloat(n) === 1;
825
+ }
952
826
 
953
827
  /**
954
- * A global namespace for aria-valuemin.
955
- * @type {string}
828
+ * Check to see if string passed in is a percentage
829
+ * @param {string} n testing number
830
+ * @returns {boolean} the query result
956
831
  */
957
- const ariaValueMin = 'aria-valuemin';
958
-
959
- /**
960
- * A global namespace for aria-valuemax.
961
- * @type {string}
962
- */
963
- const ariaValueMax = 'aria-valuemax';
964
-
965
- const tabIndex = 'tabindex';
966
-
967
- /**
968
- * Returns all color controls for `ColorPicker`.
969
- *
970
- * @param {CP.ColorPicker} self the `ColorPicker` instance
971
- * @returns {HTMLElement | Element} color controls
972
- */
973
- function getColorControls(self) {
974
- const { format, componentLabels } = self;
975
- const {
976
- hueLabel, alphaLabel, lightnessLabel, saturationLabel,
977
- whitenessLabel, blacknessLabel,
978
- } = componentLabels;
979
-
980
- const max1 = format === 'hsl' ? 360 : 100;
981
- const max2 = format === 'hsl' ? 100 : 360;
982
- const max3 = 100;
983
-
984
- let ctrl1Label = format === 'hsl'
985
- ? `${hueLabel} & ${lightnessLabel}`
986
- : `${lightnessLabel} & ${saturationLabel}`;
987
-
988
- ctrl1Label = format === 'hwb'
989
- ? `${whitenessLabel} & ${blacknessLabel}`
990
- : ctrl1Label;
991
-
992
- const ctrl2Label = format === 'hsl'
993
- ? `${saturationLabel}`
994
- : `${hueLabel}`;
995
-
996
- const colorControls = createElement({
997
- tagName: 'div',
998
- className: `color-controls ${format}`,
999
- });
1000
-
1001
- const colorPointer = 'color-pointer';
1002
- const colorSlider = 'color-slider';
1003
-
1004
- const controls = [
1005
- {
1006
- i: 1,
1007
- c: colorPointer,
1008
- l: ctrl1Label,
1009
- min: 0,
1010
- max: max1,
1011
- },
1012
- {
1013
- i: 2,
1014
- c: colorSlider,
1015
- l: ctrl2Label,
1016
- min: 0,
1017
- max: max2,
1018
- },
1019
- {
1020
- i: 3,
1021
- c: colorSlider,
1022
- l: alphaLabel,
1023
- min: 0,
1024
- max: max3,
1025
- },
1026
- ];
1027
-
1028
- controls.forEach((template) => {
1029
- const {
1030
- i, c, l, min, max,
1031
- } = template;
1032
- const control = createElement({
1033
- tagName: 'div',
1034
- className: 'color-control',
1035
- });
1036
- setAttribute(control, 'role', 'presentation');
1037
-
1038
- control.append(
1039
- createElement({
1040
- tagName: 'div',
1041
- className: `visual-control visual-control${i}`,
1042
- }),
1043
- );
1044
-
1045
- const knob = createElement({
1046
- tagName: 'div',
1047
- className: `${c} knob`,
1048
- ariaLive: 'polite',
1049
- });
1050
-
1051
- setAttribute(knob, ariaLabel, l);
1052
- setAttribute(knob, 'role', 'slider');
1053
- setAttribute(knob, tabIndex, '0');
1054
- setAttribute(knob, ariaValueMin, `${min}`);
1055
- setAttribute(knob, ariaValueMax, `${max}`);
1056
- control.append(knob);
1057
- colorControls.append(control);
1058
- });
1059
-
1060
- return colorControls;
1061
- }
1062
-
1063
- /**
1064
- * Helps setting CSS variables to the color-menu.
1065
- * @param {HTMLElement} element
1066
- * @param {Record<string,any>} props
1067
- */
1068
- function setCSSProperties(element, props) {
1069
- ObjectKeys(props).forEach((prop) => {
1070
- element.style.setProperty(prop, props[prop]);
1071
- });
1072
- }
1073
-
1074
- /**
1075
- * Returns the `document.head` or the `<head>` element.
1076
- *
1077
- * @param {(Node | HTMLElement | Element | globalThis)=} node
1078
- * @returns {HTMLElement | HTMLHeadElement}
1079
- */
1080
- function getDocumentHead(node) {
1081
- return getDocument(node).head;
1082
- }
1083
-
1084
- /**
1085
- * Round colour components, for all formats except HEX.
1086
- * @param {number} v one of the colour components
1087
- * @returns {number} the rounded number
1088
- */
1089
- function roundPart(v) {
1090
- const floor = Math.floor(v);
1091
- return v - floor < 0.5 ? floor : Math.round(v);
1092
- }
1093
-
1094
- // Color supported formats
1095
- const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsb', 'hwb'];
1096
-
1097
- // Hue angles
1098
- const ANGLES = 'deg|rad|grad|turn';
1099
-
1100
- // <http://www.w3.org/TR/css3-values/#integers>
1101
- const CSS_INTEGER = '[-\\+]?\\d+%?';
1102
-
1103
- // Include CSS3 Module
1104
- // <http://www.w3.org/TR/css3-values/#number-value>
1105
- const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
1106
-
1107
- // Include CSS4 Module Hue degrees unit
1108
- // <https://www.w3.org/TR/css3-values/#angle-value>
1109
- const CSS_ANGLE = `[-\\+]?\\d*\\.?\\d+(?:${ANGLES})?`;
1110
-
1111
- // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
1112
- const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
1113
-
1114
- // Add angles to the mix
1115
- const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
1116
-
1117
- // Actual matching.
1118
- // Parentheses and commas are optional, but not required.
1119
- // Whitespace can take the place of commas or opening paren
1120
- const PERMISSIVE_MATCH = `[\\s|\\(]+(${CSS_UNIT2})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s|\\/\\s]*(${CSS_UNIT})?\\s*\\)?`;
1121
-
1122
- const matchers = {
1123
- CSS_UNIT: new RegExp(CSS_UNIT2),
1124
- hwb: new RegExp(`hwb${PERMISSIVE_MATCH}`),
1125
- rgb: new RegExp(`rgb(?:a)?${PERMISSIVE_MATCH}`),
1126
- hsl: new RegExp(`hsl(?:a)?${PERMISSIVE_MATCH}`),
1127
- hsv: new RegExp(`hsv(?:a)?${PERMISSIVE_MATCH}`),
1128
- hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1129
- hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
1130
- hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1131
- hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
1132
- };
1133
-
1134
- /**
1135
- * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
1136
- * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
1137
- * @param {string} n testing number
1138
- * @returns {boolean} the query result
1139
- */
1140
- function isOnePointZero(n) {
1141
- return `${n}`.includes('.') && parseFloat(n) === 1;
1142
- }
1143
-
1144
- /**
1145
- * Check to see if string passed in is a percentage
1146
- * @param {string} n testing number
1147
- * @returns {boolean} the query result
1148
- */
1149
- function isPercentage(n) {
1150
- return `${n}`.includes('%');
1151
- }
1152
-
1153
- /**
1154
- * Check to see if string passed in is an angle
1155
- * @param {string} n testing string
1156
- * @returns {boolean} the query result
1157
- */
1158
- function isAngle(n) {
1159
- return ANGLES.split('|').some((a) => `${n}`.includes(a));
1160
- }
832
+ function isPercentage(n) {
833
+ return `${n}`.includes('%');
834
+ }
1161
835
 
1162
836
  /**
1163
837
  * Check to see if string passed is a web safe colour.
838
+ * @see https://stackoverflow.com/a/16994164
1164
839
  * @param {string} color a colour name, EG: *red*
1165
840
  * @returns {boolean} the query result
1166
841
  */
1167
842
  function isColorName(color) {
1168
- return !['#', ...COLOR_FORMAT].some((s) => color.includes(s))
1169
- && !/[0-9]/.test(color);
843
+ if (nonColors.includes(color)
844
+ || ['#', ...COLOR_FORMAT].some((f) => color.includes(f))) return false;
845
+
846
+ if (['black', 'white'].includes(color)) return true;
847
+
848
+ return ['rgb(255, 255, 255)', 'rgb(0, 0, 0)'].every((c) => {
849
+ setElementStyle(documentHead, { color });
850
+ const computedColor = getElementStyle(documentHead, 'color');
851
+ setElementStyle(documentHead, { color: '' });
852
+ return computedColor !== c;
853
+ });
1170
854
  }
1171
855
 
1172
856
  /**
@@ -1187,15 +871,20 @@
1187
871
  */
1188
872
  function bound01(N, max) {
1189
873
  let n = N;
1190
- if (isOnePointZero(n)) n = '100%';
1191
874
 
1192
- n = max === 360 ? n : Math.min(max, Math.max(0, parseFloat(n)));
875
+ if (typeof N === 'number'
876
+ && Math.min(N, 0) === 0 // round values to 6 decimals Math.round(N * (10 ** 6)) / 10 ** 6
877
+ && Math.max(N, 1) === 1) return N;
878
+
879
+ if (isOnePointZero(N)) n = '100%';
1193
880
 
1194
- // Handle hue angles
1195
- if (isAngle(N)) n = N.replace(new RegExp(ANGLES), '');
881
+ const processPercent = isPercentage(n);
882
+ n = max === 360
883
+ ? parseFloat(n)
884
+ : Math.min(max, Math.max(0, parseFloat(n)));
1196
885
 
1197
886
  // Automatically convert percentage into number
1198
- if (isPercentage(n)) n = parseInt(String(n * max), 10) / 100;
887
+ if (processPercent) n = (n * max) / 100;
1199
888
 
1200
889
  // Handle floating point rounding errors
1201
890
  if (Math.abs(n - max) < 0.000001) {
@@ -1206,11 +895,11 @@
1206
895
  // If n is a hue given in degrees,
1207
896
  // wrap around out-of-range values into [0, 360] range
1208
897
  // then convert into [0, 1].
1209
- n = (n < 0 ? (n % max) + max : n % max) / parseFloat(String(max));
898
+ n = (n < 0 ? (n % max) + max : n % max) / max;
1210
899
  } else {
1211
900
  // If n not a hue given in degrees
1212
901
  // Convert into [0, 1] range if it isn't already.
1213
- n = (n % max) / parseFloat(String(max));
902
+ n = (n % max) / max;
1214
903
  }
1215
904
  return n;
1216
905
  }
@@ -1245,7 +934,6 @@
1245
934
  * @returns {string}
1246
935
  */
1247
936
  function getRGBFromName(name) {
1248
- const documentHead = getDocumentHead();
1249
937
  setElementStyle(documentHead, { color: name });
1250
938
  const colorName = getElementStyle(documentHead, 'color');
1251
939
  setElementStyle(documentHead, { color: '' });
@@ -1291,15 +979,12 @@
1291
979
  /**
1292
980
  * Converts an RGB colour value to HSL.
1293
981
  *
1294
- * @param {number} R Red component [0, 255]
1295
- * @param {number} G Green component [0, 255]
1296
- * @param {number} B Blue component [0, 255]
982
+ * @param {number} r Red component [0, 1]
983
+ * @param {number} g Green component [0, 1]
984
+ * @param {number} b Blue component [0, 1]
1297
985
  * @returns {CP.HSL} {h,s,l} object with [0, 1] ranged values
1298
986
  */
1299
- function rgbToHsl(R, G, B) {
1300
- const r = R / 255;
1301
- const g = G / 255;
1302
- const b = B / 255;
987
+ function rgbToHsl(r, g, b) {
1303
988
  const max = Math.max(r, g, b);
1304
989
  const min = Math.min(r, g, b);
1305
990
  let h = 0;
@@ -1311,17 +996,10 @@
1311
996
  } else {
1312
997
  const d = max - min;
1313
998
  s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
1314
- switch (max) {
1315
- case r:
1316
- h = (g - b) / d + (g < b ? 6 : 0);
1317
- break;
1318
- case g:
1319
- h = (b - r) / d + 2;
1320
- break;
1321
- case b:
1322
- h = (r - g) / d + 4;
1323
- break;
1324
- }
999
+ if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
1000
+ if (max === g) h = (b - r) / d + 2;
1001
+ if (max === b) h = (r - g) / d + 4;
1002
+
1325
1003
  h /= 6;
1326
1004
  }
1327
1005
  return { h, s, l };
@@ -1344,21 +1022,46 @@
1344
1022
  return p;
1345
1023
  }
1346
1024
 
1025
+ /**
1026
+ * Converts an HSL colour value to RGB.
1027
+ *
1028
+ * @param {number} h Hue Angle [0, 1]
1029
+ * @param {number} s Saturation [0, 1]
1030
+ * @param {number} l Lightness Angle [0, 1]
1031
+ * @returns {CP.RGB} {r,g,b} object with [0, 1] ranged values
1032
+ */
1033
+ function hslToRgb(h, s, l) {
1034
+ let r = 0;
1035
+ let g = 0;
1036
+ let b = 0;
1037
+
1038
+ if (s === 0) {
1039
+ // achromatic
1040
+ g = l;
1041
+ b = l;
1042
+ r = l;
1043
+ } else {
1044
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1045
+ const p = 2 * l - q;
1046
+ r = hueToRgb(p, q, h + 1 / 3);
1047
+ g = hueToRgb(p, q, h);
1048
+ b = hueToRgb(p, q, h - 1 / 3);
1049
+ }
1050
+
1051
+ return { r, g, b };
1052
+ }
1053
+
1347
1054
  /**
1348
1055
  * Returns an HWB colour object from an RGB colour object.
1349
1056
  * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
1350
1057
  * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
1351
1058
  *
1352
- * @param {number} R Red component [0, 255]
1353
- * @param {number} G Green [0, 255]
1354
- * @param {number} B Blue [0, 255]
1059
+ * @param {number} r Red component [0, 1]
1060
+ * @param {number} g Green [0, 1]
1061
+ * @param {number} b Blue [0, 1]
1355
1062
  * @return {CP.HWB} {h,w,b} object with [0, 1] ranged values
1356
1063
  */
1357
- function rgbToHwb(R, G, B) {
1358
- const r = R / 255;
1359
- const g = G / 255;
1360
- const b = B / 255;
1361
-
1064
+ function rgbToHwb(r, g, b) {
1362
1065
  let f = 0;
1363
1066
  let i = 0;
1364
1067
  const whiteness = Math.min(r, g, b);
@@ -1388,50 +1091,18 @@
1388
1091
  * @param {number} H Hue Angle [0, 1]
1389
1092
  * @param {number} W Whiteness [0, 1]
1390
1093
  * @param {number} B Blackness [0, 1]
1391
- * @return {CP.RGB} {r,g,b} object with [0, 255] ranged values
1094
+ * @return {CP.RGB} {r,g,b} object with [0, 1] ranged values
1392
1095
  *
1393
1096
  * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
1394
1097
  * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
1395
1098
  */
1396
1099
  function hwbToRgb(H, W, B) {
1397
1100
  if (W + B >= 1) {
1398
- const gray = (W / (W + B)) * 255;
1101
+ const gray = W / (W + B);
1399
1102
  return { r: gray, g: gray, b: gray };
1400
1103
  }
1401
1104
  let { r, g, b } = hslToRgb(H, 1, 0.5);
1402
- [r, g, b] = [r, g, b]
1403
- .map((v) => (v / 255) * (1 - W - B) + W)
1404
- .map((v) => v * 255);
1405
-
1406
- return { r, g, b };
1407
- }
1408
-
1409
- /**
1410
- * Converts an HSL colour value to RGB.
1411
- *
1412
- * @param {number} h Hue Angle [0, 1]
1413
- * @param {number} s Saturation [0, 1]
1414
- * @param {number} l Lightness Angle [0, 1]
1415
- * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1416
- */
1417
- function hslToRgb(h, s, l) {
1418
- let r = 0;
1419
- let g = 0;
1420
- let b = 0;
1421
-
1422
- if (s === 0) {
1423
- // achromatic
1424
- g = l;
1425
- b = l;
1426
- r = l;
1427
- } else {
1428
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1429
- const p = 2 * l - q;
1430
- r = hueToRgb(p, q, h + 1 / 3);
1431
- g = hueToRgb(p, q, h);
1432
- b = hueToRgb(p, q, h - 1 / 3);
1433
- }
1434
- [r, g, b] = [r, g, b].map((x) => x * 255);
1105
+ [r, g, b] = [r, g, b].map((v) => v * (1 - W - B) + W);
1435
1106
 
1436
1107
  return { r, g, b };
1437
1108
  }
@@ -1439,15 +1110,12 @@
1439
1110
  /**
1440
1111
  * Converts an RGB colour value to HSV.
1441
1112
  *
1442
- * @param {number} R Red component [0, 255]
1443
- * @param {number} G Green [0, 255]
1444
- * @param {number} B Blue [0, 255]
1113
+ * @param {number} r Red component [0, 1]
1114
+ * @param {number} g Green [0, 1]
1115
+ * @param {number} b Blue [0, 1]
1445
1116
  * @returns {CP.HSV} {h,s,v} object with [0, 1] ranged values
1446
1117
  */
1447
- function rgbToHsv(R, G, B) {
1448
- const r = R / 255;
1449
- const g = G / 255;
1450
- const b = B / 255;
1118
+ function rgbToHsv(r, g, b) {
1451
1119
  const max = Math.max(r, g, b);
1452
1120
  const min = Math.min(r, g, b);
1453
1121
  let h = 0;
@@ -1457,17 +1125,10 @@
1457
1125
  if (max === min) {
1458
1126
  h = 0; // achromatic
1459
1127
  } else {
1460
- switch (max) {
1461
- case r:
1462
- h = (g - b) / d + (g < b ? 6 : 0);
1463
- break;
1464
- case g:
1465
- h = (b - r) / d + 2;
1466
- break;
1467
- case b:
1468
- h = (r - g) / d + 4;
1469
- break;
1470
- }
1128
+ if (r === max) h = (g - b) / d + (g < b ? 6 : 0);
1129
+ if (g === max) h = (b - r) / d + 2;
1130
+ if (b === max) h = (r - g) / d + 4;
1131
+
1471
1132
  h /= 6;
1472
1133
  }
1473
1134
  return { h, s, v };
@@ -1494,7 +1155,7 @@
1494
1155
  const r = [v, q, p, p, t, v][mod];
1495
1156
  const g = [t, v, v, q, p, p][mod];
1496
1157
  const b = [p, p, t, v, v, q][mod];
1497
- return { r: r * 255, g: g * 255, b: b * 255 };
1158
+ return { r, g, b };
1498
1159
  }
1499
1160
 
1500
1161
  /**
@@ -1518,7 +1179,7 @@
1518
1179
  // Return a 3 character hex if possible
1519
1180
  if (allow3Char && hex[0].charAt(0) === hex[0].charAt(1)
1520
1181
  && hex[1].charAt(0) === hex[1].charAt(1)
1521
- && hex[2].charAt(0) === hex[2].charAt(1)) {
1182
+ && hex[2].charAt(0) === hex[2].charAt(1)) {
1522
1183
  return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
1523
1184
  }
1524
1185
 
@@ -1546,51 +1207,34 @@
1546
1207
  // Return a 4 character hex if possible
1547
1208
  if (allow4Char && hex[0].charAt(0) === hex[0].charAt(1)
1548
1209
  && hex[1].charAt(0) === hex[1].charAt(1)
1549
- && hex[2].charAt(0) === hex[2].charAt(1)
1550
- && hex[3].charAt(0) === hex[3].charAt(1)) {
1210
+ && hex[2].charAt(0) === hex[2].charAt(1)
1211
+ && hex[3].charAt(0) === hex[3].charAt(1)) {
1551
1212
  return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
1552
1213
  }
1553
1214
  return hex.join('');
1554
1215
  }
1555
1216
 
1556
- /**
1557
- * Returns a colour object corresponding to a given number.
1558
- * @param {number} color input number
1559
- * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1560
- */
1561
- function numberInputToObject(color) {
1562
- /* eslint-disable no-bitwise */
1563
- return {
1564
- r: color >> 16,
1565
- g: (color & 0xff00) >> 8,
1566
- b: color & 0xff,
1567
- };
1568
- /* eslint-enable no-bitwise */
1569
- }
1570
-
1571
1217
  /**
1572
1218
  * Permissive string parsing. Take in a number of formats, and output an object
1573
1219
  * based on detected format. Returns {r,g,b} or {h,s,l} or {h,s,v}
1574
1220
  * @param {string} input colour value in any format
1575
- * @returns {Record<string, (number | string)> | false} an object matching the RegExp
1221
+ * @returns {Record<string, (number | string | boolean)> | false} an object matching the RegExp
1576
1222
  */
1577
1223
  function stringInputToObject(input) {
1578
- let color = input.trim().toLowerCase();
1224
+ let color = toLowerCase(input.trim());
1225
+
1579
1226
  if (color.length === 0) {
1580
1227
  return {
1581
- r: 0, g: 0, b: 0, a: 0,
1228
+ r: 0, g: 0, b: 0, a: 1,
1582
1229
  };
1583
1230
  }
1584
- let named = false;
1231
+
1585
1232
  if (isColorName(color)) {
1586
1233
  color = getRGBFromName(color);
1587
- named = true;
1588
1234
  } else if (nonColors.includes(color)) {
1589
- const isTransparent = color === 'transparent';
1590
- const rgb = isTransparent ? 0 : 255;
1591
- const a = isTransparent ? 0 : 1;
1235
+ const a = color === 'transparent' ? 0 : 1;
1592
1236
  return {
1593
- r: rgb, g: rgb, b: rgb, a, format: 'rgb',
1237
+ r: 0, g: 0, b: 0, a, format: 'rgb', ok: true,
1594
1238
  };
1595
1239
  }
1596
1240
 
@@ -1605,24 +1249,28 @@
1605
1249
  r: m1, g: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'rgb',
1606
1250
  };
1607
1251
  }
1252
+
1608
1253
  [, m1, m2, m3, m4] = matchers.hsl.exec(color) || [];
1609
1254
  if (m1 && m2 && m3/* && m4 */) {
1610
1255
  return {
1611
1256
  h: m1, s: m2, l: m3, a: m4 !== undefined ? m4 : 1, format: 'hsl',
1612
1257
  };
1613
1258
  }
1259
+
1614
1260
  [, m1, m2, m3, m4] = matchers.hsv.exec(color) || [];
1615
1261
  if (m1 && m2 && m3/* && m4 */) {
1616
1262
  return {
1617
1263
  h: m1, s: m2, v: m3, a: m4 !== undefined ? m4 : 1, format: 'hsv',
1618
1264
  };
1619
1265
  }
1266
+
1620
1267
  [, m1, m2, m3, m4] = matchers.hwb.exec(color) || [];
1621
1268
  if (m1 && m2 && m3) {
1622
1269
  return {
1623
1270
  h: m1, w: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'hwb',
1624
1271
  };
1625
1272
  }
1273
+
1626
1274
  [, m1, m2, m3, m4] = matchers.hex8.exec(color) || [];
1627
1275
  if (m1 && m2 && m3 && m4) {
1628
1276
  return {
@@ -1630,19 +1278,20 @@
1630
1278
  g: parseIntFromHex(m2),
1631
1279
  b: parseIntFromHex(m3),
1632
1280
  a: convertHexToDecimal(m4),
1633
- // format: named ? 'rgb' : 'hex8',
1634
- format: named ? 'rgb' : 'hex',
1281
+ format: 'hex',
1635
1282
  };
1636
1283
  }
1284
+
1637
1285
  [, m1, m2, m3] = matchers.hex6.exec(color) || [];
1638
1286
  if (m1 && m2 && m3) {
1639
1287
  return {
1640
1288
  r: parseIntFromHex(m1),
1641
1289
  g: parseIntFromHex(m2),
1642
1290
  b: parseIntFromHex(m3),
1643
- format: named ? 'rgb' : 'hex',
1291
+ format: 'hex',
1644
1292
  };
1645
1293
  }
1294
+
1646
1295
  [, m1, m2, m3, m4] = matchers.hex4.exec(color) || [];
1647
1296
  if (m1 && m2 && m3 && m4) {
1648
1297
  return {
@@ -1650,19 +1299,20 @@
1650
1299
  g: parseIntFromHex(m2 + m2),
1651
1300
  b: parseIntFromHex(m3 + m3),
1652
1301
  a: convertHexToDecimal(m4 + m4),
1653
- // format: named ? 'rgb' : 'hex8',
1654
- format: named ? 'rgb' : 'hex',
1302
+ format: 'hex',
1655
1303
  };
1656
1304
  }
1305
+
1657
1306
  [, m1, m2, m3] = matchers.hex3.exec(color) || [];
1658
1307
  if (m1 && m2 && m3) {
1659
1308
  return {
1660
1309
  r: parseIntFromHex(m1 + m1),
1661
1310
  g: parseIntFromHex(m2 + m2),
1662
1311
  b: parseIntFromHex(m3 + m3),
1663
- format: named ? 'rgb' : 'hex',
1312
+ format: 'hex',
1664
1313
  };
1665
1314
  }
1315
+
1666
1316
  return false;
1667
1317
  }
1668
1318
 
@@ -1693,7 +1343,9 @@
1693
1343
  */
1694
1344
  function inputToRGB(input) {
1695
1345
  let rgb = { r: 0, g: 0, b: 0 };
1346
+ /** @type {*} */
1696
1347
  let color = input;
1348
+ /** @type {string | number} */
1697
1349
  let a = 1;
1698
1350
  let s = null;
1699
1351
  let v = null;
@@ -1704,58 +1356,67 @@
1704
1356
  let r = null;
1705
1357
  let g = null;
1706
1358
  let ok = false;
1707
- let format = 'hex';
1359
+ const inputFormat = typeof color === 'object' && color.format;
1360
+ let format = inputFormat && COLOR_FORMAT.includes(inputFormat) ? inputFormat : 'rgb';
1708
1361
 
1709
1362
  if (typeof input === 'string') {
1710
- // @ts-ignore -- this now is converted to object
1711
1363
  color = stringInputToObject(input);
1712
1364
  if (color) ok = true;
1713
1365
  }
1714
1366
  if (typeof color === 'object') {
1715
1367
  if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
1716
1368
  ({ r, g, b } = color);
1717
- [r, g, b] = [...[r, g, b]]
1718
- .map((n) => bound01(n, isPercentage(n) ? 100 : 255) * 255).map(roundPart);
1719
- rgb = { r, g, b }; // RGB values now are all in [0, 255] range
1369
+ // RGB values now are all in [0, 1] range
1370
+ [r, g, b] = [r, g, b].map((n) => bound01(n, isPercentage(n) ? 100 : 255));
1371
+ rgb = { r, g, b };
1720
1372
  ok = true;
1721
- format = 'rgb';
1722
- } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
1373
+ format = color.format || 'rgb';
1374
+ }
1375
+ if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
1723
1376
  ({ h, s, v } = color);
1724
- h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1725
- s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1726
- v = typeof v === 'number' ? v : bound01(v, 100); // brightness can be `5%` or a [0, 1] value
1377
+ h = bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1378
+ s = bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1379
+ v = bound01(v, 100); // brightness can be `5%` or a [0, 1] value
1727
1380
  rgb = hsvToRgb(h, s, v);
1728
1381
  ok = true;
1729
1382
  format = 'hsv';
1730
- } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
1383
+ }
1384
+ if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
1731
1385
  ({ h, s, l } = color);
1732
- h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1733
- s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1734
- l = typeof l === 'number' ? l : bound01(l, 100); // lightness can be `5%` or a [0, 1] value
1386
+ h = bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1387
+ s = bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1388
+ l = bound01(l, 100); // lightness can be `5%` or a [0, 1] value
1735
1389
  rgb = hslToRgb(h, s, l);
1736
1390
  ok = true;
1737
1391
  format = 'hsl';
1738
- } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.w) && isValidCSSUnit(color.b)) {
1392
+ }
1393
+ if (isValidCSSUnit(color.h) && isValidCSSUnit(color.w) && isValidCSSUnit(color.b)) {
1739
1394
  ({ h, w, b } = color);
1740
- h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1741
- w = typeof w === 'number' ? w : bound01(w, 100); // whiteness can be `5%` or a [0, 1] value
1742
- b = typeof b === 'number' ? b : bound01(b, 100); // blackness can be `5%` or a [0, 1] value
1395
+ h = bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1396
+ w = bound01(w, 100); // whiteness can be `5%` or a [0, 1] value
1397
+ b = bound01(b, 100); // blackness can be `5%` or a [0, 1] value
1743
1398
  rgb = hwbToRgb(h, w, b);
1744
1399
  ok = true;
1745
1400
  format = 'hwb';
1746
1401
  }
1747
1402
  if (isValidCSSUnit(color.a)) {
1748
- a = color.a;
1749
- a = isPercentage(`${a}`) ? bound01(a, 100) : a;
1403
+ a = color.a; // @ts-ignore -- `parseFloat` works with numbers too
1404
+ a = isPercentage(`${a}`) || parseFloat(a) > 1 ? bound01(a, 100) : a;
1750
1405
  }
1751
1406
  }
1407
+ if (typeof color === 'undefined') {
1408
+ ok = true;
1409
+ }
1752
1410
 
1753
1411
  return {
1754
- ok, // @ts-ignore
1755
- format: color.format || format,
1756
- r: Math.min(255, Math.max(rgb.r, 0)),
1757
- g: Math.min(255, Math.max(rgb.g, 0)),
1758
- b: Math.min(255, Math.max(rgb.b, 0)),
1412
+ ok,
1413
+ format,
1414
+ // r: Math.min(255, Math.max(rgb.r, 0)),
1415
+ // g: Math.min(255, Math.max(rgb.g, 0)),
1416
+ // b: Math.min(255, Math.max(rgb.b, 0)),
1417
+ r: rgb.r,
1418
+ g: rgb.g,
1419
+ b: rgb.b,
1759
1420
  a: boundAlpha(a),
1760
1421
  };
1761
1422
  }
@@ -1774,15 +1435,13 @@
1774
1435
  constructor(input, config) {
1775
1436
  let color = input;
1776
1437
  const configFormat = config && COLOR_FORMAT.includes(config)
1777
- ? config : 'rgb';
1438
+ ? config : '';
1778
1439
 
1779
- // If input is already a `Color`, return itself
1440
+ // If input is already a `Color`, clone its values
1780
1441
  if (color instanceof Color) {
1781
1442
  color = inputToRGB(color);
1782
1443
  }
1783
- if (typeof color === 'number') {
1784
- color = numberInputToObject(color);
1785
- }
1444
+
1786
1445
  const {
1787
1446
  r, g, b, a, ok, format,
1788
1447
  } = inputToRGB(color);
@@ -1791,7 +1450,7 @@
1791
1450
  const self = this;
1792
1451
 
1793
1452
  /** @type {CP.ColorInput} */
1794
- self.originalInput = color;
1453
+ self.originalInput = input;
1795
1454
  /** @type {number} */
1796
1455
  self.r = r;
1797
1456
  /** @type {number} */
@@ -1832,24 +1491,21 @@
1832
1491
  let R = 0;
1833
1492
  let G = 0;
1834
1493
  let B = 0;
1835
- const rp = r / 255;
1836
- const rg = g / 255;
1837
- const rb = b / 255;
1838
1494
 
1839
- if (rp <= 0.03928) {
1840
- R = rp / 12.92;
1495
+ if (r <= 0.03928) {
1496
+ R = r / 12.92;
1841
1497
  } else {
1842
- R = ((rp + 0.055) / 1.055) ** 2.4;
1498
+ R = ((r + 0.055) / 1.055) ** 2.4;
1843
1499
  }
1844
- if (rg <= 0.03928) {
1845
- G = rg / 12.92;
1500
+ if (g <= 0.03928) {
1501
+ G = g / 12.92;
1846
1502
  } else {
1847
- G = ((rg + 0.055) / 1.055) ** 2.4;
1503
+ G = ((g + 0.055) / 1.055) ** 2.4;
1848
1504
  }
1849
- if (rb <= 0.03928) {
1850
- B = rb / 12.92;
1505
+ if (b <= 0.03928) {
1506
+ B = b / 12.92;
1851
1507
  } else {
1852
- B = ((rb + 0.055) / 1.055) ** 2.4;
1508
+ B = ((b + 0.055) / 1.055) ** 2.4;
1853
1509
  }
1854
1510
  return 0.2126 * R + 0.7152 * G + 0.0722 * B;
1855
1511
  }
@@ -1859,7 +1515,7 @@
1859
1515
  * @returns {number} a number in the [0, 255] range
1860
1516
  */
1861
1517
  get brightness() {
1862
- const { r, g, b } = this;
1518
+ const { r, g, b } = this.toRgb();
1863
1519
  return (r * 299 + g * 587 + b * 114) / 1000;
1864
1520
  }
1865
1521
 
@@ -1868,16 +1524,14 @@
1868
1524
  * @returns {CP.RGBA} an {r,g,b,a} object with [0, 255] ranged values
1869
1525
  */
1870
1526
  toRgb() {
1871
- const {
1527
+ let {
1872
1528
  r, g, b, a,
1873
1529
  } = this;
1874
- const [R, G, B] = [r, g, b].map((x) => roundPart(x));
1875
1530
 
1531
+ [r, g, b] = [r, g, b].map((n) => roundPart(n * 255 * 100) / 100);
1532
+ a = roundPart(a * 100) / 100;
1876
1533
  return {
1877
- r: R,
1878
- g: G,
1879
- b: B,
1880
- a: roundPart(a * 100) / 100,
1534
+ r, g, b, a,
1881
1535
  };
1882
1536
  }
1883
1537
 
@@ -1891,10 +1545,11 @@
1891
1545
  const {
1892
1546
  r, g, b, a,
1893
1547
  } = this.toRgb();
1548
+ const [R, G, B] = [r, g, b].map(roundPart);
1894
1549
 
1895
1550
  return a === 1
1896
- ? `rgb(${r}, ${g}, ${b})`
1897
- : `rgba(${r}, ${g}, ${b}, ${a})`;
1551
+ ? `rgb(${R}, ${G}, ${B})`
1552
+ : `rgba(${R}, ${G}, ${B}, ${a})`;
1898
1553
  }
1899
1554
 
1900
1555
  /**
@@ -1907,9 +1562,10 @@
1907
1562
  const {
1908
1563
  r, g, b, a,
1909
1564
  } = this.toRgb();
1565
+ const [R, G, B] = [r, g, b].map(roundPart);
1910
1566
  const A = a === 1 ? '' : ` / ${roundPart(a * 100)}%`;
1911
1567
 
1912
- return `rgb(${r} ${g} ${b}${A})`;
1568
+ return `rgb(${R} ${G} ${B}${A})`;
1913
1569
  }
1914
1570
 
1915
1571
  /**
@@ -1969,7 +1625,7 @@
1969
1625
  toHsv() {
1970
1626
  const {
1971
1627
  r, g, b, a,
1972
- } = this.toRgb();
1628
+ } = this;
1973
1629
  const { h, s, v } = rgbToHsv(r, g, b);
1974
1630
 
1975
1631
  return {
@@ -1984,7 +1640,7 @@
1984
1640
  toHsl() {
1985
1641
  const {
1986
1642
  r, g, b, a,
1987
- } = this.toRgb();
1643
+ } = this;
1988
1644
  const { h, s, l } = rgbToHsl(r, g, b);
1989
1645
 
1990
1646
  return {
@@ -2069,6 +1725,7 @@
2069
1725
  */
2070
1726
  setAlpha(alpha) {
2071
1727
  const self = this;
1728
+ if (typeof alpha !== 'number') return self;
2072
1729
  self.a = boundAlpha(alpha);
2073
1730
  return self;
2074
1731
  }
@@ -2183,6 +1840,7 @@
2183
1840
  isOnePointZero,
2184
1841
  isPercentage,
2185
1842
  isValidCSSUnit,
1843
+ isColorName,
2186
1844
  pad2,
2187
1845
  clamp01,
2188
1846
  bound01,
@@ -2200,10 +1858,11 @@
2200
1858
  hueToRgb,
2201
1859
  hwbToRgb,
2202
1860
  parseIntFromHex,
2203
- numberInputToObject,
2204
1861
  stringInputToObject,
2205
1862
  inputToRGB,
2206
1863
  roundPart,
1864
+ getElementStyle,
1865
+ setElementStyle,
2207
1866
  ObjectAssign,
2208
1867
  });
2209
1868
 
@@ -2212,7 +1871,7 @@
2212
1871
  * Returns a color palette with a given set of parameters.
2213
1872
  * @example
2214
1873
  * new ColorPalette(0, 12, 10);
2215
- * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: array }
1874
+ * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: Array<Color> }
2216
1875
  */
2217
1876
  class ColorPalette {
2218
1877
  /**
@@ -2232,48 +1891,352 @@
2232
1891
  [hue, hueSteps, lightSteps] = args;
2233
1892
  } else if (args.length === 2) {
2234
1893
  [hueSteps, lightSteps] = args;
2235
- } else {
2236
- throw TypeError('ColorPalette requires minimum 2 arguments');
1894
+ if ([hueSteps, lightSteps].some((n) => n < 1)) {
1895
+ throw TypeError('ColorPalette: both arguments must be higher than 0.');
1896
+ }
2237
1897
  }
2238
1898
 
2239
- /** @type {string[]} */
1899
+ /** @type {*} */
2240
1900
  const colors = [];
2241
-
2242
1901
  const hueStep = 360 / hueSteps;
2243
1902
  const half = roundPart((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
2244
- const estimatedStep = 100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100;
1903
+ const steps1To13 = [0.25, 0.2, 0.15, 0.11, 0.09, 0.075];
1904
+ const lightSets = [[1, 2, 3], [4, 5], [6, 7], [8, 9], [10, 11], [12, 13]];
1905
+ const closestSet = lightSets.find((set) => set.includes(lightSteps));
1906
+
1907
+ // find a lightStep that won't go beyond black and white
1908
+ // something within the [10-90] range of lightness
1909
+ const lightStep = closestSet
1910
+ ? steps1To13[lightSets.indexOf(closestSet)]
1911
+ : (100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100);
1912
+
1913
+ // light tints
1914
+ for (let i = 1; i < half + 1; i += 1) {
1915
+ lightnessArray = [...lightnessArray, (0.5 + lightStep * (i))];
1916
+ }
1917
+
1918
+ // dark tints
1919
+ for (let i = 1; i < lightSteps - half; i += 1) {
1920
+ lightnessArray = [(0.5 - lightStep * (i)), ...lightnessArray];
1921
+ }
1922
+
1923
+ // feed `colors` Array
1924
+ for (let i = 0; i < hueSteps; i += 1) {
1925
+ const currentHue = ((hue + i * hueStep) % 360) / 360;
1926
+ lightnessArray.forEach((l) => {
1927
+ colors.push(new Color({ h: currentHue, s: 1, l }));
1928
+ });
1929
+ }
1930
+
1931
+ this.hue = hue;
1932
+ this.hueSteps = hueSteps;
1933
+ this.lightSteps = lightSteps;
1934
+ this.colors = colors;
1935
+ }
1936
+ }
1937
+
1938
+ ObjectAssign(ColorPalette, { Color });
1939
+
1940
+ /** @type {Record<string, string>} */
1941
+ const colorPickerLabels = {
1942
+ pickerLabel: 'Colour Picker',
1943
+ appearanceLabel: 'Colour Appearance',
1944
+ valueLabel: 'Colour Value',
1945
+ toggleLabel: 'Select Colour',
1946
+ presetsLabel: 'Colour Presets',
1947
+ defaultsLabel: 'Colour Defaults',
1948
+ formatLabel: 'Format',
1949
+ alphaLabel: 'Alpha',
1950
+ hexLabel: 'Hexadecimal',
1951
+ hueLabel: 'Hue',
1952
+ whitenessLabel: 'Whiteness',
1953
+ blacknessLabel: 'Blackness',
1954
+ saturationLabel: 'Saturation',
1955
+ lightnessLabel: 'Lightness',
1956
+ redLabel: 'Red',
1957
+ greenLabel: 'Green',
1958
+ blueLabel: 'Blue',
1959
+ };
1960
+
1961
+ /**
1962
+ * A list of 17 color names used for WAI-ARIA compliance.
1963
+ * @type {string[]}
1964
+ */
1965
+ const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
1966
+
1967
+ const tabIndex = 'tabindex';
1968
+
1969
+ /**
1970
+ * Check if a string is valid JSON string.
1971
+ * @param {string} str the string input
1972
+ * @returns {boolean} the query result
1973
+ */
1974
+ function isValidJSON(str) {
1975
+ try {
1976
+ JSON.parse(str);
1977
+ } catch (e) {
1978
+ return false;
1979
+ }
1980
+ return true;
1981
+ }
1982
+
1983
+ /**
1984
+ * Shortcut for `String.toUpperCase()`.
1985
+ *
1986
+ * @param {string} source input string
1987
+ * @returns {string} uppercase output string
1988
+ */
1989
+ const toUpperCase = (source) => source.toUpperCase();
1990
+
1991
+ /**
1992
+ * A global namespace for aria-haspopup.
1993
+ * @type {string}
1994
+ */
1995
+ const ariaHasPopup = 'aria-haspopup';
1996
+
1997
+ /**
1998
+ * A global namespace for aria-hidden.
1999
+ * @type {string}
2000
+ */
2001
+ const ariaHidden = 'aria-hidden';
2002
+
2003
+ /**
2004
+ * A global namespace for aria-labelledby.
2005
+ * @type {string}
2006
+ */
2007
+ const ariaLabelledBy = 'aria-labelledby';
2008
+
2009
+ /**
2010
+ * This is a shortie for `document.createElement` method
2011
+ * which allows you to create a new `HTMLElement` for a given `tagName`
2012
+ * or based on an object with specific non-readonly attributes:
2013
+ * `id`, `className`, `textContent`, `style`, etc.
2014
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
2015
+ *
2016
+ * @param {Record<string, string> | string} param `tagName` or object
2017
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
2018
+ */
2019
+ function createElement(param) {
2020
+ if (typeof param === 'string') {
2021
+ return getDocument().createElement(param);
2022
+ }
2023
+
2024
+ const { tagName } = param;
2025
+ const attr = { ...param };
2026
+ const newElement = createElement(tagName);
2027
+ delete attr.tagName;
2028
+ ObjectAssign(newElement, attr);
2029
+ return newElement;
2030
+ }
2031
+
2032
+ /**
2033
+ * This is a shortie for `document.createElementNS` method
2034
+ * which allows you to create a new `HTMLElement` for a given `tagName`
2035
+ * or based on an object with specific non-readonly attributes:
2036
+ * `id`, `className`, `textContent`, `style`, etc.
2037
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
2038
+ *
2039
+ * @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
2040
+ * @param {Record<string, string> | string} param `tagName` or object
2041
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
2042
+ */
2043
+ function createElementNS(namespace, param) {
2044
+ if (typeof param === 'string') {
2045
+ return getDocument().createElementNS(namespace, param);
2046
+ }
2047
+
2048
+ const { tagName } = param;
2049
+ const attr = { ...param };
2050
+ const newElement = createElementNS(namespace, tagName);
2051
+ delete attr.tagName;
2052
+ ObjectAssign(newElement, attr);
2053
+ return newElement;
2054
+ }
2055
+
2056
+ const vHidden = 'v-hidden';
2057
+
2058
+ /**
2059
+ * Returns the color form for `ColorPicker`.
2060
+ *
2061
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
2062
+ * @returns {HTMLElement | Element} a new `<div>` element with color component `<input>`
2063
+ */
2064
+ function getColorForm(self) {
2065
+ const { format, id, componentLabels } = self;
2066
+ const colorForm = createElement({
2067
+ tagName: 'div',
2068
+ className: `color-form ${format}`,
2069
+ });
2070
+
2071
+ let components = ['hex'];
2072
+ if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
2073
+ else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
2074
+ else if (format === 'hwb') components = ['hue', 'whiteness', 'blackness', 'alpha'];
2075
+
2076
+ components.forEach((c) => {
2077
+ const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
2078
+ const cID = `color_${format}_${c}_${id}`;
2079
+ const formatLabel = componentLabels[`${c}Label`];
2080
+ const cInputLabel = createElement({ tagName: 'label' });
2081
+ setAttribute(cInputLabel, 'for', cID);
2082
+ cInputLabel.append(
2083
+ createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
2084
+ createElement({ tagName: 'span', className: vHidden, innerText: formatLabel }),
2085
+ );
2086
+ const cInput = createElement({
2087
+ tagName: 'input',
2088
+ id: cID,
2089
+ // name: cID, - prevent saving the value to a form
2090
+ type: format === 'hex' ? 'text' : 'number',
2091
+ value: c === 'alpha' ? '100' : '0',
2092
+ className: `color-input ${c}`,
2093
+ });
2094
+ setAttribute(cInput, 'autocomplete', 'off');
2095
+ setAttribute(cInput, 'spellcheck', 'false');
2096
+
2097
+ // alpha
2098
+ let max = '100';
2099
+ let step = '1';
2100
+ if (c !== 'alpha') {
2101
+ if (format === 'rgb') {
2102
+ max = '255'; step = '1';
2103
+ } else if (c === 'hue') {
2104
+ max = '360'; step = '1';
2105
+ }
2106
+ }
2107
+ ObjectAssign(cInput, {
2108
+ min: '0',
2109
+ max,
2110
+ step,
2111
+ });
2112
+ colorForm.append(cInputLabel, cInput);
2113
+ });
2114
+ return colorForm;
2115
+ }
2116
+
2117
+ /**
2118
+ * A global namespace for aria-label.
2119
+ * @type {string}
2120
+ */
2121
+ const ariaLabel = 'aria-label';
2122
+
2123
+ /**
2124
+ * A global namespace for aria-valuemin.
2125
+ * @type {string}
2126
+ */
2127
+ const ariaValueMin = 'aria-valuemin';
2128
+
2129
+ /**
2130
+ * A global namespace for aria-valuemax.
2131
+ * @type {string}
2132
+ */
2133
+ const ariaValueMax = 'aria-valuemax';
2134
+
2135
+ /**
2136
+ * Returns all color controls for `ColorPicker`.
2137
+ *
2138
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
2139
+ * @returns {HTMLElement | Element} color controls
2140
+ */
2141
+ function getColorControls(self) {
2142
+ const { format, componentLabels } = self;
2143
+ const {
2144
+ hueLabel, alphaLabel, lightnessLabel, saturationLabel,
2145
+ whitenessLabel, blacknessLabel,
2146
+ } = componentLabels;
2147
+
2148
+ const max1 = format === 'hsl' ? 360 : 100;
2149
+ const max2 = format === 'hsl' ? 100 : 360;
2150
+ const max3 = 100;
2151
+
2152
+ let ctrl1Label = format === 'hsl'
2153
+ ? `${hueLabel} & ${lightnessLabel}`
2154
+ : `${lightnessLabel} & ${saturationLabel}`;
2155
+
2156
+ ctrl1Label = format === 'hwb'
2157
+ ? `${whitenessLabel} & ${blacknessLabel}`
2158
+ : ctrl1Label;
2159
+
2160
+ const ctrl2Label = format === 'hsl'
2161
+ ? `${saturationLabel}`
2162
+ : `${hueLabel}`;
2163
+
2164
+ const colorControls = createElement({
2165
+ tagName: 'div',
2166
+ className: `color-controls ${format}`,
2167
+ });
2168
+
2169
+ const colorPointer = 'color-pointer';
2170
+ const colorSlider = 'color-slider';
2171
+
2172
+ const controls = [
2173
+ {
2174
+ i: 1,
2175
+ c: colorPointer,
2176
+ l: ctrl1Label,
2177
+ min: 0,
2178
+ max: max1,
2179
+ },
2180
+ {
2181
+ i: 2,
2182
+ c: colorSlider,
2183
+ l: ctrl2Label,
2184
+ min: 0,
2185
+ max: max2,
2186
+ },
2187
+ {
2188
+ i: 3,
2189
+ c: colorSlider,
2190
+ l: alphaLabel,
2191
+ min: 0,
2192
+ max: max3,
2193
+ },
2194
+ ];
2195
+
2196
+ controls.forEach((template) => {
2197
+ const {
2198
+ i, c, l, min, max,
2199
+ } = template;
2200
+ const control = createElement({
2201
+ tagName: 'div',
2202
+ className: 'color-control',
2203
+ });
2204
+ setAttribute(control, 'role', 'presentation');
2245
2205
 
2246
- let lightStep = 0.25;
2247
- lightStep = [4, 5].includes(lightSteps) ? 0.2 : lightStep;
2248
- lightStep = [6, 7].includes(lightSteps) ? 0.15 : lightStep;
2249
- lightStep = [8, 9].includes(lightSteps) ? 0.11 : lightStep;
2250
- lightStep = [10, 11].includes(lightSteps) ? 0.09 : lightStep;
2251
- lightStep = [12, 13].includes(lightSteps) ? 0.075 : lightStep;
2252
- lightStep = lightSteps > 13 ? estimatedStep : lightStep;
2206
+ control.append(
2207
+ createElement({
2208
+ tagName: 'div',
2209
+ className: `visual-control visual-control${i}`,
2210
+ }),
2211
+ );
2253
2212
 
2254
- // light tints
2255
- for (let i = 1; i < half + 1; i += 1) {
2256
- lightnessArray = [...lightnessArray, (0.5 + lightStep * (i))];
2257
- }
2213
+ const knob = createElement({
2214
+ tagName: 'div',
2215
+ className: `${c} knob`,
2216
+ ariaLive: 'polite',
2217
+ });
2258
2218
 
2259
- // dark tints
2260
- for (let i = 1; i < lightSteps - half; i += 1) {
2261
- lightnessArray = [(0.5 - lightStep * (i)), ...lightnessArray];
2262
- }
2219
+ setAttribute(knob, ariaLabel, l);
2220
+ setAttribute(knob, 'role', 'slider');
2221
+ setAttribute(knob, tabIndex, '0');
2222
+ setAttribute(knob, ariaValueMin, `${min}`);
2223
+ setAttribute(knob, ariaValueMax, `${max}`);
2224
+ control.append(knob);
2225
+ colorControls.append(control);
2226
+ });
2263
2227
 
2264
- // feed `colors` Array
2265
- for (let i = 0; i < hueSteps; i += 1) {
2266
- const currentHue = ((hue + i * hueStep) % 360) / 360;
2267
- lightnessArray.forEach((l) => {
2268
- colors.push(new Color({ h: currentHue, s: 1, l }).toHexString());
2269
- });
2270
- }
2228
+ return colorControls;
2229
+ }
2271
2230
 
2272
- this.hue = hue;
2273
- this.hueSteps = hueSteps;
2274
- this.lightSteps = lightSteps;
2275
- this.colors = colors;
2276
- }
2231
+ /**
2232
+ * Helps setting CSS variables to the color-menu.
2233
+ * @param {HTMLElement} element
2234
+ * @param {Record<string,any>} props
2235
+ */
2236
+ function setCSSProperties(element, props) {
2237
+ ObjectKeys(props).forEach((prop) => {
2238
+ element.style.setProperty(prop, props[prop]);
2239
+ });
2277
2240
  }
2278
2241
 
2279
2242
  /**
@@ -2309,7 +2272,8 @@
2309
2272
  optionSize = fit > 5 && isMultiLine ? 1.5 : optionSize;
2310
2273
  const menuHeight = `${(rowCount || 1) * optionSize}rem`;
2311
2274
  const menuHeightHover = `calc(${rowCountHover} * ${optionSize}rem + ${rowCountHover - 1} * ${gap})`;
2312
-
2275
+ /** @type {HTMLUListElement} */
2276
+ // @ts-ignore -- <UL> is an `HTMLElement`
2313
2277
  const menu = createElement({
2314
2278
  tagName: 'ul',
2315
2279
  className: finalClass,
@@ -2317,7 +2281,7 @@
2317
2281
  setAttribute(menu, 'role', 'listbox');
2318
2282
  setAttribute(menu, ariaLabel, menuLabel);
2319
2283
 
2320
- if (isScrollable) { // @ts-ignore
2284
+ if (isScrollable) {
2321
2285
  setCSSProperties(menu, {
2322
2286
  '--grid-item-size': `${optionSize}rem`,
2323
2287
  '--grid-fit': fit,
@@ -2328,15 +2292,19 @@
2328
2292
  }
2329
2293
 
2330
2294
  colorsArray.forEach((x) => {
2331
- const [value, label] = x.trim().split(':');
2332
- const xRealColor = new Color(value, format).toString();
2333
- const isActive = xRealColor === getAttribute(input, 'value');
2295
+ let [value, label] = typeof x === 'string' ? x.trim().split(':') : [];
2296
+ if (x instanceof Color) {
2297
+ value = x.toHexString();
2298
+ label = value;
2299
+ }
2300
+ const color = new Color(x instanceof Color ? x : value, format);
2301
+ const isActive = color.toString() === getAttribute(input, 'value');
2334
2302
  const active = isActive ? ' active' : '';
2335
2303
 
2336
2304
  const option = createElement({
2337
2305
  tagName: 'li',
2338
2306
  className: `color-option${active}`,
2339
- innerText: `${label || x}`,
2307
+ innerText: `${label || value}`,
2340
2308
  });
2341
2309
 
2342
2310
  setAttribute(option, tabIndex, '0');
@@ -2345,7 +2313,7 @@
2345
2313
  setAttribute(option, ariaSelected, isActive ? 'true' : 'false');
2346
2314
 
2347
2315
  if (isOptionsMenu) {
2348
- setElementStyle(option, { backgroundColor: x });
2316
+ setElementStyle(option, { backgroundColor: value });
2349
2317
  }
2350
2318
 
2351
2319
  menu.append(option);
@@ -2354,55 +2322,10 @@
2354
2322
  }
2355
2323
 
2356
2324
  /**
2357
- * Check if a string is valid JSON string.
2358
- * @param {string} str the string input
2359
- * @returns {boolean} the query result
2360
- */
2361
- function isValidJSON(str) {
2362
- try {
2363
- JSON.parse(str);
2364
- } catch (e) {
2365
- return false;
2366
- }
2367
- return true;
2368
- }
2369
-
2370
- var version = "0.0.1alpha3";
2371
-
2372
- // @ts-ignore
2373
-
2374
- const Version = version;
2375
-
2376
- // ColorPicker GC
2377
- // ==============
2378
- const colorPickerString = 'color-picker';
2379
- const colorPickerSelector = `[data-function="${colorPickerString}"]`;
2380
- const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2381
- const colorPickerDefaults = {
2382
- componentLabels: colorPickerLabels,
2383
- colorLabels: colorNames,
2384
- format: 'rgb',
2385
- colorPresets: false,
2386
- colorKeywords: false,
2387
- };
2388
-
2389
- // ColorPicker Static Methods
2390
- // ==========================
2391
-
2392
- /** @type {CP.GetInstance<ColorPicker>} */
2393
- const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
2394
-
2395
- /** @type {CP.InitCallback<ColorPicker>} */
2396
- const initColorPicker = (element) => new ColorPicker(element);
2397
-
2398
- // ColorPicker Private Methods
2399
- // ===========================
2400
-
2401
- /**
2402
- * Generate HTML markup and update instance properties.
2403
- * @param {ColorPicker} self
2404
- */
2405
- function initCallback(self) {
2325
+ * Generate HTML markup and update instance properties.
2326
+ * @param {CP.ColorPicker} self
2327
+ */
2328
+ function setMarkup(self) {
2406
2329
  const {
2407
2330
  input, parent, format, id, componentLabels, colorKeywords, colorPresets,
2408
2331
  } = self;
@@ -2417,9 +2340,7 @@
2417
2340
  self.color = new Color(color, format);
2418
2341
 
2419
2342
  // set initial controls dimensions
2420
- // make the controls smaller on mobile
2421
- const dropClass = isMobile ? ' mobile' : '';
2422
- const formatString = format === 'hex' ? hexLabel : format.toUpperCase();
2343
+ const formatString = format === 'hex' ? hexLabel : toUpperCase(format);
2423
2344
 
2424
2345
  const pickerBtn = createElement({
2425
2346
  id: `picker-btn-${id}`,
@@ -2436,7 +2357,7 @@
2436
2357
 
2437
2358
  const pickerDropdown = createElement({
2438
2359
  tagName: 'div',
2439
- className: `color-dropdown picker${dropClass}`,
2360
+ className: 'color-dropdown picker',
2440
2361
  });
2441
2362
  setAttribute(pickerDropdown, ariaLabelledBy, `picker-btn-${id}`);
2442
2363
  setAttribute(pickerDropdown, 'role', 'group');
@@ -2452,7 +2373,7 @@
2452
2373
  if (colorKeywords || colorPresets) {
2453
2374
  const presetsDropdown = createElement({
2454
2375
  tagName: 'div',
2455
- className: `color-dropdown scrollable menu${dropClass}`,
2376
+ className: 'color-dropdown scrollable menu',
2456
2377
  });
2457
2378
 
2458
2379
  // color presets
@@ -2502,6 +2423,37 @@
2502
2423
  setAttribute(input, tabIndex, '-1');
2503
2424
  }
2504
2425
 
2426
+ var version = "0.0.2alpha2";
2427
+
2428
+ // @ts-ignore
2429
+
2430
+ const Version = version;
2431
+
2432
+ // ColorPicker GC
2433
+ // ==============
2434
+ const colorPickerString = 'color-picker';
2435
+ const colorPickerSelector = `[data-function="${colorPickerString}"]`;
2436
+ const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2437
+ const colorPickerDefaults = {
2438
+ componentLabels: colorPickerLabels,
2439
+ colorLabels: colorNames,
2440
+ format: 'rgb',
2441
+ colorPresets: false,
2442
+ colorKeywords: false,
2443
+ };
2444
+
2445
+ // ColorPicker Static Methods
2446
+ // ==========================
2447
+
2448
+ /** @type {CP.GetInstance<ColorPicker>} */
2449
+ const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
2450
+
2451
+ /** @type {CP.InitCallback<ColorPicker>} */
2452
+ const initColorPicker = (element) => new ColorPicker(element);
2453
+
2454
+ // ColorPicker Private Methods
2455
+ // ===========================
2456
+
2505
2457
  /**
2506
2458
  * Add / remove `ColorPicker` main event listeners.
2507
2459
  * @param {ColorPicker} self
@@ -2514,8 +2466,6 @@
2514
2466
  fn(input, focusinEvent, self.showPicker);
2515
2467
  fn(pickerToggle, mouseclickEvent, self.togglePicker);
2516
2468
 
2517
- fn(input, keydownEvent, self.keyToggle);
2518
-
2519
2469
  if (menuToggle) {
2520
2470
  fn(menuToggle, mouseclickEvent, self.toggleMenu);
2521
2471
  }
@@ -2553,8 +2503,7 @@
2553
2503
  fn(doc, pointerEvents.move, self.pointerMove);
2554
2504
  fn(doc, pointerEvents.up, self.pointerUp);
2555
2505
  fn(parent, focusoutEvent, self.handleFocusOut);
2556
- // @ts-ignore -- this is `Window`
2557
- fn(win, keyupEvent, self.handleDismiss);
2506
+ fn(doc, keyupEvent, self.handleDismiss);
2558
2507
  }
2559
2508
 
2560
2509
  /**
@@ -2638,7 +2587,7 @@
2638
2587
  const input = querySelector(target);
2639
2588
 
2640
2589
  // invalidate
2641
- if (!input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
2590
+ if (!input) throw new TypeError(`ColorPicker target "${target}" cannot be found.`);
2642
2591
  self.input = input;
2643
2592
 
2644
2593
  const parent = closest(input, colorPickerParentSelector);
@@ -2685,15 +2634,14 @@
2685
2634
  });
2686
2635
 
2687
2636
  // update and expose component labels
2688
- const tempLabels = ObjectAssign({}, colorPickerLabels);
2689
- const jsonLabels = componentLabels && isValidJSON(componentLabels)
2690
- ? JSON.parse(componentLabels) : componentLabels || {};
2637
+ const tempComponentLabels = componentLabels && isValidJSON(componentLabels)
2638
+ ? JSON.parse(componentLabels) : componentLabels;
2691
2639
 
2692
2640
  /** @type {Record<string, string>} */
2693
- self.componentLabels = ObjectAssign(tempLabels, jsonLabels);
2641
+ self.componentLabels = ObjectAssign(colorPickerLabels, tempComponentLabels);
2694
2642
 
2695
2643
  /** @type {Color} */
2696
- self.color = new Color('white', format);
2644
+ self.color = new Color(input.value || '#fff', format);
2697
2645
 
2698
2646
  /** @type {CP.ColorFormats} */
2699
2647
  self.format = format;
@@ -2702,7 +2650,7 @@
2702
2650
  if (colorKeywords instanceof Array) {
2703
2651
  self.colorKeywords = colorKeywords;
2704
2652
  } else if (typeof colorKeywords === 'string' && colorKeywords.length) {
2705
- self.colorKeywords = colorKeywords.split(',');
2653
+ self.colorKeywords = colorKeywords.split(',').map((x) => x.trim());
2706
2654
  }
2707
2655
 
2708
2656
  // set colour presets
@@ -2731,11 +2679,10 @@
2731
2679
  self.handleFocusOut = self.handleFocusOut.bind(self);
2732
2680
  self.changeHandler = self.changeHandler.bind(self);
2733
2681
  self.handleDismiss = self.handleDismiss.bind(self);
2734
- self.keyToggle = self.keyToggle.bind(self);
2735
2682
  self.handleKnobs = self.handleKnobs.bind(self);
2736
2683
 
2737
2684
  // generate markup
2738
- initCallback(self);
2685
+ setMarkup(self);
2739
2686
 
2740
2687
  const [colorPicker, colorMenu] = getElementsByClassName('color-dropdown', parent);
2741
2688
  // set main elements
@@ -2823,76 +2770,83 @@
2823
2770
  return inputValue !== '' && new Color(inputValue).isValid;
2824
2771
  }
2825
2772
 
2773
+ /** Returns the colour appearance, usually the closest colour name for the current value. */
2774
+ get appearance() {
2775
+ const {
2776
+ colorLabels, hsl, hsv, format,
2777
+ } = this;
2778
+
2779
+ const hue = roundPart(hsl.h * 360);
2780
+ const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
2781
+ const saturation = roundPart(saturationSource * 100);
2782
+ const lightness = roundPart(hsl.l * 100);
2783
+ const hsvl = hsv.v * 100;
2784
+
2785
+ let colorName;
2786
+
2787
+ // determine color appearance
2788
+ if (lightness === 100 && saturation === 0) {
2789
+ colorName = colorLabels.white;
2790
+ } else if (lightness === 0) {
2791
+ colorName = colorLabels.black;
2792
+ } else if (saturation === 0) {
2793
+ colorName = colorLabels.grey;
2794
+ } else if (hue < 15 || hue >= 345) {
2795
+ colorName = colorLabels.red;
2796
+ } else if (hue >= 15 && hue < 45) {
2797
+ colorName = hsvl > 80 && saturation > 80 ? colorLabels.orange : colorLabels.brown;
2798
+ } else if (hue >= 45 && hue < 75) {
2799
+ const isGold = hue > 46 && hue < 54 && hsvl < 80 && saturation > 90;
2800
+ const isOlive = hue >= 54 && hue < 75 && hsvl < 80;
2801
+ colorName = isGold ? colorLabels.gold : colorLabels.yellow;
2802
+ colorName = isOlive ? colorLabels.olive : colorName;
2803
+ } else if (hue >= 75 && hue < 155) {
2804
+ colorName = hsvl < 68 ? colorLabels.green : colorLabels.lime;
2805
+ } else if (hue >= 155 && hue < 175) {
2806
+ colorName = colorLabels.teal;
2807
+ } else if (hue >= 175 && hue < 195) {
2808
+ colorName = colorLabels.cyan;
2809
+ } else if (hue >= 195 && hue < 255) {
2810
+ colorName = colorLabels.blue;
2811
+ } else if (hue >= 255 && hue < 270) {
2812
+ colorName = colorLabels.violet;
2813
+ } else if (hue >= 270 && hue < 295) {
2814
+ colorName = colorLabels.magenta;
2815
+ } else if (hue >= 295 && hue < 345) {
2816
+ colorName = colorLabels.pink;
2817
+ }
2818
+ return colorName;
2819
+ }
2820
+
2826
2821
  /** Updates `ColorPicker` visuals. */
2827
2822
  updateVisuals() {
2828
2823
  const self = this;
2829
2824
  const {
2830
- format, controlPositions, visuals,
2825
+ controlPositions, visuals,
2831
2826
  } = self;
2832
2827
  const [v1, v2, v3] = visuals;
2833
- const { offsetWidth, offsetHeight } = v1;
2834
- const hue = format === 'hsl'
2835
- ? controlPositions.c1x / offsetWidth
2836
- : controlPositions.c2y / offsetHeight;
2837
- // @ts-ignore - `hslToRgb` is assigned to `Color` as static method
2838
- const { r, g, b } = Color.hslToRgb(hue, 1, 0.5);
2828
+ const { offsetHeight } = v1;
2829
+ const hue = controlPositions.c2y / offsetHeight;
2830
+ const { r, g, b } = new Color({ h: hue, s: 1, l: 0.5 }).toRgb();
2839
2831
  const whiteGrad = 'linear-gradient(rgb(255,255,255) 0%, rgb(255,255,255) 100%)';
2840
2832
  const alpha = 1 - controlPositions.c3y / offsetHeight;
2841
2833
  const roundA = roundPart((alpha * 100)) / 100;
2842
2834
 
2843
- if (format !== 'hsl') {
2844
- const fill = new Color({
2845
- h: hue, s: 1, l: 0.5, a: alpha,
2846
- }).toRgbString();
2847
- const hueGradient = `linear-gradient(
2848
- rgb(255,0,0) 0%, rgb(255,255,0) 16.67%,
2849
- rgb(0,255,0) 33.33%, rgb(0,255,255) 50%,
2850
- rgb(0,0,255) 66.67%, rgb(255,0,255) 83.33%,
2851
- rgb(255,0,0) 100%)`;
2852
- setElementStyle(v1, {
2853
- background: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,${roundA}) 100%),
2854
- linear-gradient(to right, rgba(255,255,255,${roundA}) 0%, ${fill} 100%),
2855
- ${whiteGrad}`,
2856
- });
2857
- setElementStyle(v2, { background: hueGradient });
2858
- } else {
2859
- const saturation = roundPart((controlPositions.c2y / offsetHeight) * 100);
2860
- const fill0 = new Color({
2861
- r: 255, g: 0, b: 0, a: alpha,
2862
- }).saturate(-saturation).toRgbString();
2863
- const fill1 = new Color({
2864
- r: 255, g: 255, b: 0, a: alpha,
2865
- }).saturate(-saturation).toRgbString();
2866
- const fill2 = new Color({
2867
- r: 0, g: 255, b: 0, a: alpha,
2868
- }).saturate(-saturation).toRgbString();
2869
- const fill3 = new Color({
2870
- r: 0, g: 255, b: 255, a: alpha,
2871
- }).saturate(-saturation).toRgbString();
2872
- const fill4 = new Color({
2873
- r: 0, g: 0, b: 255, a: alpha,
2874
- }).saturate(-saturation).toRgbString();
2875
- const fill5 = new Color({
2876
- r: 255, g: 0, b: 255, a: alpha,
2877
- }).saturate(-saturation).toRgbString();
2878
- const fill6 = new Color({
2879
- r: 255, g: 0, b: 0, a: alpha,
2880
- }).saturate(-saturation).toRgbString();
2881
- const fillGradient = `linear-gradient(to right,
2882
- ${fill0} 0%, ${fill1} 16.67%, ${fill2} 33.33%, ${fill3} 50%,
2883
- ${fill4} 66.67%, ${fill5} 83.33%, ${fill6} 100%)`;
2884
- const lightGrad = `linear-gradient(rgba(255,255,255,${roundA}) 0%, rgba(255,255,255,0) 50%),
2885
- linear-gradient(rgba(0,0,0,0) 50%, rgba(0,0,0,${roundA}) 100%)`;
2886
-
2887
- setElementStyle(v1, { background: `${lightGrad},${fillGradient},${whiteGrad}` });
2888
- const {
2889
- r: gr, g: gg, b: gb,
2890
- } = new Color({ r, g, b }).greyscale().toRgb();
2835
+ const fill = new Color({
2836
+ h: hue, s: 1, l: 0.5, a: alpha,
2837
+ }).toRgbString();
2838
+ const hueGradient = `linear-gradient(
2839
+ rgb(255,0,0) 0%, rgb(255,255,0) 16.67%,
2840
+ rgb(0,255,0) 33.33%, rgb(0,255,255) 50%,
2841
+ rgb(0,0,255) 66.67%, rgb(255,0,255) 83.33%,
2842
+ rgb(255,0,0) 100%)`;
2843
+ setElementStyle(v1, {
2844
+ background: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,${roundA}) 100%),
2845
+ linear-gradient(to right, rgba(255,255,255,${roundA}) 0%, ${fill} 100%),
2846
+ ${whiteGrad}`,
2847
+ });
2848
+ setElementStyle(v2, { background: hueGradient });
2891
2849
 
2892
- setElementStyle(v2, {
2893
- background: `linear-gradient(rgb(${r},${g},${b}) 0%, rgb(${gr},${gg},${gb}) 100%)`,
2894
- });
2895
- }
2896
2850
  setElementStyle(v3, {
2897
2851
  background: `linear-gradient(rgba(${r},${g},${b},1) 0%,rgba(${r},${g},${b},0) 100%)`,
2898
2852
  });
@@ -2931,7 +2885,7 @@
2931
2885
  const self = this;
2932
2886
  const { activeElement } = getDocument(self.input);
2933
2887
 
2934
- if ((isMobile && self.dragElement)
2888
+ if ((e.type === touchmoveEvent && self.dragElement)
2935
2889
  || (activeElement && self.controlKnobs.includes(activeElement))) {
2936
2890
  e.stopPropagation();
2937
2891
  e.preventDefault();
@@ -3042,13 +2996,13 @@
3042
2996
  const [v1, v2, v3] = visuals;
3043
2997
  const [c1, c2, c3] = controlKnobs;
3044
2998
  /** @type {HTMLElement} */
3045
- const visual = hasClass(target, 'visual-control')
3046
- ? target : querySelector('.visual-control', target.parentElement);
2999
+ const visual = controlKnobs.includes(target) ? target.previousElementSibling : target;
3047
3000
  const visualRect = getBoundingClientRect(visual);
3001
+ const html = getDocumentElement(v1);
3048
3002
  const X = type === 'touchstart' ? touches[0].pageX : pageX;
3049
3003
  const Y = type === 'touchstart' ? touches[0].pageY : pageY;
3050
- const offsetX = X - window.pageXOffset - visualRect.left;
3051
- const offsetY = Y - window.pageYOffset - visualRect.top;
3004
+ const offsetX = X - html.scrollLeft - visualRect.left;
3005
+ const offsetY = Y - html.scrollTop - visualRect.top;
3052
3006
 
3053
3007
  if (target === v1 || target === c1) {
3054
3008
  self.dragElement = visual;
@@ -3108,10 +3062,11 @@
3108
3062
  if (!dragElement) return;
3109
3063
 
3110
3064
  const controlRect = getBoundingClientRect(dragElement);
3111
- const X = type === 'touchmove' ? touches[0].pageX : pageX;
3112
- const Y = type === 'touchmove' ? touches[0].pageY : pageY;
3113
- const offsetX = X - window.pageXOffset - controlRect.left;
3114
- const offsetY = Y - window.pageYOffset - controlRect.top;
3065
+ const win = getDocumentElement(v1);
3066
+ const X = type === touchmoveEvent ? touches[0].pageX : pageX;
3067
+ const Y = type === touchmoveEvent ? touches[0].pageY : pageY;
3068
+ const offsetX = X - win.scrollLeft - controlRect.left;
3069
+ const offsetY = Y - win.scrollTop - controlRect.top;
3115
3070
 
3116
3071
  if (dragElement === v1) {
3117
3072
  self.changeControl1(offsetX, offsetY);
@@ -3138,30 +3093,41 @@
3138
3093
  if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
3139
3094
  e.preventDefault();
3140
3095
 
3141
- const { controlKnobs } = self;
3096
+ const { controlKnobs, visuals } = self;
3097
+ const { offsetWidth, offsetHeight } = visuals[0];
3142
3098
  const [c1, c2, c3] = controlKnobs;
3143
3099
  const { activeElement } = getDocument(c1);
3144
3100
  const currentKnob = controlKnobs.find((x) => x === activeElement);
3101
+ const yRatio = offsetHeight / 360;
3145
3102
 
3146
3103
  if (currentKnob) {
3147
3104
  let offsetX = 0;
3148
3105
  let offsetY = 0;
3106
+
3149
3107
  if (target === c1) {
3108
+ const xRatio = offsetWidth / 100;
3109
+
3150
3110
  if ([keyArrowLeft, keyArrowRight].includes(code)) {
3151
- self.controlPositions.c1x += code === keyArrowRight ? +1 : -1;
3111
+ self.controlPositions.c1x += code === keyArrowRight ? xRatio : -xRatio;
3152
3112
  } else if ([keyArrowUp, keyArrowDown].includes(code)) {
3153
- self.controlPositions.c1y += code === keyArrowDown ? +1 : -1;
3113
+ self.controlPositions.c1y += code === keyArrowDown ? yRatio : -yRatio;
3154
3114
  }
3155
3115
 
3156
3116
  offsetX = self.controlPositions.c1x;
3157
3117
  offsetY = self.controlPositions.c1y;
3158
3118
  self.changeControl1(offsetX, offsetY);
3159
3119
  } else if (target === c2) {
3160
- self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3120
+ self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code)
3121
+ ? yRatio
3122
+ : -yRatio;
3123
+
3161
3124
  offsetY = self.controlPositions.c2y;
3162
3125
  self.changeControl2(offsetY);
3163
3126
  } else if (target === c3) {
3164
- self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3127
+ self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code)
3128
+ ? yRatio
3129
+ : -yRatio;
3130
+
3165
3131
  offsetY = self.controlPositions.c3y;
3166
3132
  self.changeAlpha(offsetY);
3167
3133
  }
@@ -3189,7 +3155,7 @@
3189
3155
  if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
3190
3156
  if (activeElement === input) {
3191
3157
  if (isNonColorValue) {
3192
- colorSource = 'white';
3158
+ colorSource = currentValue === 'transparent' ? 'rgba(0,0,0,0)' : 'rgb(0,0,0)';
3193
3159
  } else {
3194
3160
  colorSource = currentValue;
3195
3161
  }
@@ -3240,9 +3206,7 @@
3240
3206
  changeControl1(X, Y) {
3241
3207
  const self = this;
3242
3208
  let [offsetX, offsetY] = [0, 0];
3243
- const {
3244
- format, controlPositions, visuals,
3245
- } = self;
3209
+ const { controlPositions, visuals } = self;
3246
3210
  const { offsetHeight, offsetWidth } = visuals[0];
3247
3211
 
3248
3212
  if (X > offsetWidth) offsetX = offsetWidth;
@@ -3251,29 +3215,19 @@
3251
3215
  if (Y > offsetHeight) offsetY = offsetHeight;
3252
3216
  else if (Y >= 0) offsetY = Y;
3253
3217
 
3254
- const hue = format === 'hsl'
3255
- ? offsetX / offsetWidth
3256
- : controlPositions.c2y / offsetHeight;
3218
+ const hue = controlPositions.c2y / offsetHeight;
3257
3219
 
3258
- const saturation = format === 'hsl'
3259
- ? 1 - controlPositions.c2y / offsetHeight
3260
- : offsetX / offsetWidth;
3220
+ const saturation = offsetX / offsetWidth;
3261
3221
 
3262
3222
  const lightness = 1 - offsetY / offsetHeight;
3263
3223
  const alpha = 1 - controlPositions.c3y / offsetHeight;
3264
3224
 
3265
- const colorObject = format === 'hsl'
3266
- ? {
3267
- h: hue, s: saturation, l: lightness, a: alpha,
3268
- }
3269
- : {
3270
- h: hue, s: saturation, v: lightness, a: alpha,
3271
- };
3272
-
3273
3225
  // new color
3274
3226
  const {
3275
3227
  r, g, b, a,
3276
- } = new Color(colorObject);
3228
+ } = new Color({
3229
+ h: hue, s: saturation, v: lightness, a: alpha,
3230
+ });
3277
3231
 
3278
3232
  ObjectAssign(self.color, {
3279
3233
  r, g, b, a,
@@ -3300,7 +3254,7 @@
3300
3254
  changeControl2(Y) {
3301
3255
  const self = this;
3302
3256
  const {
3303
- format, controlPositions, visuals,
3257
+ controlPositions, visuals,
3304
3258
  } = self;
3305
3259
  const { offsetHeight, offsetWidth } = visuals[0];
3306
3260
 
@@ -3309,26 +3263,17 @@
3309
3263
  if (Y > offsetHeight) offsetY = offsetHeight;
3310
3264
  else if (Y >= 0) offsetY = Y;
3311
3265
 
3312
- const hue = format === 'hsl'
3313
- ? controlPositions.c1x / offsetWidth
3314
- : offsetY / offsetHeight;
3315
- const saturation = format === 'hsl'
3316
- ? 1 - offsetY / offsetHeight
3317
- : controlPositions.c1x / offsetWidth;
3266
+ const hue = offsetY / offsetHeight;
3267
+ const saturation = controlPositions.c1x / offsetWidth;
3318
3268
  const lightness = 1 - controlPositions.c1y / offsetHeight;
3319
3269
  const alpha = 1 - controlPositions.c3y / offsetHeight;
3320
- const colorObject = format === 'hsl'
3321
- ? {
3322
- h: hue, s: saturation, l: lightness, a: alpha,
3323
- }
3324
- : {
3325
- h: hue, s: saturation, v: lightness, a: alpha,
3326
- };
3327
3270
 
3328
3271
  // new color
3329
3272
  const {
3330
3273
  r, g, b, a,
3331
- } = new Color(colorObject);
3274
+ } = new Color({
3275
+ h: hue, s: saturation, v: lightness, a: alpha,
3276
+ });
3332
3277
 
3333
3278
  ObjectAssign(self.color, {
3334
3279
  r, g, b, a,
@@ -3415,18 +3360,18 @@
3415
3360
  setControlPositions() {
3416
3361
  const self = this;
3417
3362
  const {
3418
- format, visuals, color, hsl, hsv,
3363
+ visuals, color, hsv,
3419
3364
  } = self;
3420
3365
  const { offsetHeight, offsetWidth } = visuals[0];
3421
3366
  const alpha = color.a;
3422
- const hue = hsl.h;
3367
+ const hue = hsv.h;
3423
3368
 
3424
- const saturation = format !== 'hsl' ? hsv.s : hsl.s;
3425
- const lightness = format !== 'hsl' ? hsv.v : hsl.l;
3369
+ const saturation = hsv.s;
3370
+ const lightness = hsv.v;
3426
3371
 
3427
- self.controlPositions.c1x = format !== 'hsl' ? saturation * offsetWidth : hue * offsetWidth;
3372
+ self.controlPositions.c1x = saturation * offsetWidth;
3428
3373
  self.controlPositions.c1y = (1 - lightness) * offsetHeight;
3429
- self.controlPositions.c2y = format !== 'hsl' ? hue * offsetHeight : (1 - saturation) * offsetHeight;
3374
+ self.controlPositions.c2y = hue * offsetHeight;
3430
3375
  self.controlPositions.c3y = (1 - alpha) * offsetHeight;
3431
3376
  }
3432
3377
 
@@ -3434,78 +3379,40 @@
3434
3379
  updateAppearance() {
3435
3380
  const self = this;
3436
3381
  const {
3437
- componentLabels, colorLabels, color, parent,
3438
- hsl, hsv, hex, format, controlKnobs,
3382
+ componentLabels, color, parent,
3383
+ hsv, hex, format, controlKnobs,
3439
3384
  } = self;
3440
3385
  const {
3441
3386
  appearanceLabel, hexLabel, valueLabel,
3442
3387
  } = componentLabels;
3443
- const { r, g, b } = color.toRgb();
3388
+ let { r, g, b } = color.toRgb();
3444
3389
  const [knob1, knob2, knob3] = controlKnobs;
3445
- const hue = roundPart(hsl.h * 360);
3390
+ const hue = roundPart(hsv.h * 360);
3446
3391
  const alpha = color.a;
3447
- const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
3448
- const saturation = roundPart(saturationSource * 100);
3449
- const lightness = roundPart(hsl.l * 100);
3450
- const hsvl = hsv.v * 100;
3451
- let colorName;
3452
-
3453
- // determine color appearance
3454
- if (lightness === 100 && saturation === 0) {
3455
- colorName = colorLabels.white;
3456
- } else if (lightness === 0) {
3457
- colorName = colorLabels.black;
3458
- } else if (saturation === 0) {
3459
- colorName = colorLabels.grey;
3460
- } else if (hue < 15 || hue >= 345) {
3461
- colorName = colorLabels.red;
3462
- } else if (hue >= 15 && hue < 45) {
3463
- colorName = hsvl > 80 && saturation > 80 ? colorLabels.orange : colorLabels.brown;
3464
- } else if (hue >= 45 && hue < 75) {
3465
- const isGold = hue > 46 && hue < 54 && hsvl < 80 && saturation > 90;
3466
- const isOlive = hue >= 54 && hue < 75 && hsvl < 80;
3467
- colorName = isGold ? colorLabels.gold : colorLabels.yellow;
3468
- colorName = isOlive ? colorLabels.olive : colorName;
3469
- } else if (hue >= 75 && hue < 155) {
3470
- colorName = hsvl < 68 ? colorLabels.green : colorLabels.lime;
3471
- } else if (hue >= 155 && hue < 175) {
3472
- colorName = colorLabels.teal;
3473
- } else if (hue >= 175 && hue < 195) {
3474
- colorName = colorLabels.cyan;
3475
- } else if (hue >= 195 && hue < 255) {
3476
- colorName = colorLabels.blue;
3477
- } else if (hue >= 255 && hue < 270) {
3478
- colorName = colorLabels.violet;
3479
- } else if (hue >= 270 && hue < 295) {
3480
- colorName = colorLabels.magenta;
3481
- } else if (hue >= 295 && hue < 345) {
3482
- colorName = colorLabels.pink;
3483
- }
3392
+ const saturation = roundPart(hsv.s * 100);
3393
+ const lightness = roundPart(hsv.v * 100);
3394
+ const colorName = self.appearance;
3484
3395
 
3485
3396
  let colorLabel = `${hexLabel} ${hex.split('').join(' ')}`;
3486
3397
 
3487
- if (format === 'hsl') {
3488
- colorLabel = `HSL: ${hue}°, ${saturation}%, ${lightness}%`;
3489
- setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3490
- setAttribute(knob1, ariaValueText, `${hue}° & ${lightness}%`);
3491
- setAttribute(knob1, ariaValueNow, `${hue}`);
3492
- setAttribute(knob2, ariaValueText, `${saturation}%`);
3493
- setAttribute(knob2, ariaValueNow, `${saturation}`);
3494
- } else if (format === 'hwb') {
3398
+ if (format === 'hwb') {
3495
3399
  const { hwb } = self;
3496
3400
  const whiteness = roundPart(hwb.w * 100);
3497
3401
  const blackness = roundPart(hwb.b * 100);
3498
3402
  colorLabel = `HWB: ${hue}°, ${whiteness}%, ${blackness}%`;
3499
- setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3500
3403
  setAttribute(knob1, ariaValueText, `${whiteness}% & ${blackness}%`);
3501
3404
  setAttribute(knob1, ariaValueNow, `${whiteness}`);
3405
+ setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3502
3406
  setAttribute(knob2, ariaValueText, `${hue}%`);
3503
3407
  setAttribute(knob2, ariaValueNow, `${hue}`);
3504
3408
  } else {
3409
+ [r, g, b] = [r, g, b].map(roundPart);
3410
+ colorLabel = format === 'hsl' ? `HSL: ${hue}°, ${saturation}%, ${lightness}%` : colorLabel;
3505
3411
  colorLabel = format === 'rgb' ? `RGB: ${r}, ${g}, ${b}` : colorLabel;
3506
- setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3412
+
3507
3413
  setAttribute(knob1, ariaValueText, `${lightness}% & ${saturation}%`);
3508
3414
  setAttribute(knob1, ariaValueNow, `${lightness}`);
3415
+ setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3509
3416
  setAttribute(knob2, ariaValueText, `${hue}°`);
3510
3417
  setAttribute(knob2, ariaValueNow, `${hue}`);
3511
3418
  }
@@ -3533,10 +3440,12 @@
3533
3440
  /** Updates the control knobs actual positions. */
3534
3441
  updateControls() {
3535
3442
  const { controlKnobs, controlPositions } = this;
3536
- const {
3443
+ let {
3537
3444
  c1x, c1y, c2y, c3y,
3538
3445
  } = controlPositions;
3539
3446
  const [control1, control2, control3] = controlKnobs;
3447
+ // round control positions
3448
+ [c1x, c1y, c2y, c3y] = [c1x, c1y, c2y, c3y].map(roundPart);
3540
3449
 
3541
3450
  setElementStyle(control1, { transform: `translate3d(${c1x - 4}px,${c1y - 4}px,0)` });
3542
3451
  setElementStyle(control2, { transform: `translate3d(0,${c2y - 4}px,0)` });
@@ -3579,7 +3488,8 @@
3579
3488
  i3.value = `${blackness}`;
3580
3489
  i4.value = `${alpha}`;
3581
3490
  } else if (format === 'rgb') {
3582
- const { r, g, b } = self.rgb;
3491
+ let { r, g, b } = self.rgb;
3492
+ [r, g, b] = [r, g, b].map(roundPart);
3583
3493
 
3584
3494
  newColor = self.color.toRgbString();
3585
3495
  i1.value = `${r}`;
@@ -3597,37 +3507,13 @@
3597
3507
  }
3598
3508
  }
3599
3509
 
3600
- /**
3601
- * The `Space` & `Enter` keys specific event listener.
3602
- * Toggle visibility of the `ColorPicker` / the presets menu, showing one will hide the other.
3603
- * @param {KeyboardEvent} e
3604
- * @this {ColorPicker}
3605
- */
3606
- keyToggle(e) {
3607
- const self = this;
3608
- const { menuToggle } = self;
3609
- const { activeElement } = getDocument(menuToggle);
3610
- const { code } = e;
3611
-
3612
- if ([keyEnter, keySpace].includes(code)) {
3613
- if ((menuToggle && activeElement === menuToggle) || !activeElement) {
3614
- e.preventDefault();
3615
- if (!activeElement) {
3616
- self.togglePicker(e);
3617
- } else {
3618
- self.toggleMenu();
3619
- }
3620
- }
3621
- }
3622
- }
3623
-
3624
3510
  /**
3625
3511
  * Toggle the `ColorPicker` dropdown visibility.
3626
- * @param {Event} e
3512
+ * @param {Event=} e
3627
3513
  * @this {ColorPicker}
3628
3514
  */
3629
3515
  togglePicker(e) {
3630
- e.preventDefault();
3516
+ if (e) e.preventDefault();
3631
3517
  const self = this;
3632
3518
  const { colorPicker } = self;
3633
3519
 
@@ -3648,8 +3534,13 @@
3648
3534
  }
3649
3535
  }
3650
3536
 
3651
- /** Toggles the visibility of the `ColorPicker` presets menu. */
3652
- toggleMenu() {
3537
+ /**
3538
+ * Toggles the visibility of the `ColorPicker` presets menu.
3539
+ * @param {Event=} e
3540
+ * @this {ColorPicker}
3541
+ */
3542
+ toggleMenu(e) {
3543
+ if (e) e.preventDefault();
3653
3544
  const self = this;
3654
3545
  const { colorMenu } = self;
3655
3546
 
@@ -3675,6 +3566,10 @@
3675
3566
  const relatedBtn = openPicker ? pickerToggle : menuToggle;
3676
3567
  const animationDuration = openDropdown && getElementTransitionDuration(openDropdown);
3677
3568
 
3569
+ // if (!self.isValid) {
3570
+ self.value = self.color.toString(true);
3571
+ // }
3572
+
3678
3573
  if (openDropdown) {
3679
3574
  removeClass(openDropdown, 'show');
3680
3575
  setAttribute(relatedBtn, ariaExpanded, 'false');
@@ -3688,9 +3583,6 @@
3688
3583
  }, animationDuration);
3689
3584
  }
3690
3585
 
3691
- if (!self.isValid) {
3692
- self.value = self.color.toString();
3693
- }
3694
3586
  if (!focusPrevented) {
3695
3587
  focus(pickerToggle);
3696
3588
  }
@@ -3735,4 +3627,4 @@
3735
3627
 
3736
3628
  return ColorPicker;
3737
3629
 
3738
- })));
3630
+ }));