@thednp/color-picker 0.0.1-alpha3 → 0.0.2-alpha2

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