@thednp/color-picker 0.0.1 → 0.0.2-alpha3

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.1 (http://thednp.github.io/color-picker)
2
+ * ColorPicker v0.0.2alpha3 (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
- // RGB values now are all in [0, 255] range
1718
- [r, g, b] = [r, g, b].map((n) => bound01(n, isPercentage(n) ? 100 : 255) * 255);
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));
1719
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,12 +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
1530
 
1531
+ [r, g, b] = [r, g, b].map((n) => roundPart(n * 255 * 100) / 100);
1532
+ a = roundPart(a * 100) / 100;
1875
1533
  return {
1876
- r, g, b, a: roundPart(a * 100) / 100,
1534
+ r, g, b, a,
1877
1535
  };
1878
1536
  }
1879
1537
 
@@ -1967,7 +1625,7 @@
1967
1625
  toHsv() {
1968
1626
  const {
1969
1627
  r, g, b, a,
1970
- } = this.toRgb();
1628
+ } = this;
1971
1629
  const { h, s, v } = rgbToHsv(r, g, b);
1972
1630
 
1973
1631
  return {
@@ -1982,7 +1640,7 @@
1982
1640
  toHsl() {
1983
1641
  const {
1984
1642
  r, g, b, a,
1985
- } = this.toRgb();
1643
+ } = this;
1986
1644
  const { h, s, l } = rgbToHsl(r, g, b);
1987
1645
 
1988
1646
  return {
@@ -2067,6 +1725,7 @@
2067
1725
  */
2068
1726
  setAlpha(alpha) {
2069
1727
  const self = this;
1728
+ if (typeof alpha !== 'number') return self;
2070
1729
  self.a = boundAlpha(alpha);
2071
1730
  return self;
2072
1731
  }
@@ -2181,6 +1840,7 @@
2181
1840
  isOnePointZero,
2182
1841
  isPercentage,
2183
1842
  isValidCSSUnit,
1843
+ isColorName,
2184
1844
  pad2,
2185
1845
  clamp01,
2186
1846
  bound01,
@@ -2198,10 +1858,11 @@
2198
1858
  hueToRgb,
2199
1859
  hwbToRgb,
2200
1860
  parseIntFromHex,
2201
- numberInputToObject,
2202
1861
  stringInputToObject,
2203
1862
  inputToRGB,
2204
1863
  roundPart,
1864
+ getElementStyle,
1865
+ setElementStyle,
2205
1866
  ObjectAssign,
2206
1867
  });
2207
1868
 
@@ -2210,7 +1871,7 @@
2210
1871
  * Returns a color palette with a given set of parameters.
2211
1872
  * @example
2212
1873
  * new ColorPalette(0, 12, 10);
2213
- * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: array }
1874
+ * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: Array<Color> }
2214
1875
  */
2215
1876
  class ColorPalette {
2216
1877
  /**
@@ -2230,48 +1891,352 @@
2230
1891
  [hue, hueSteps, lightSteps] = args;
2231
1892
  } else if (args.length === 2) {
2232
1893
  [hueSteps, lightSteps] = args;
2233
- } else {
2234
- 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
+ }
2235
1897
  }
2236
1898
 
2237
- /** @type {string[]} */
1899
+ /** @type {*} */
2238
1900
  const colors = [];
2239
-
2240
1901
  const hueStep = 360 / hueSteps;
2241
1902
  const half = roundPart((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
2242
- 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');
2243
2205
 
2244
- let lightStep = 0.25;
2245
- lightStep = [4, 5].includes(lightSteps) ? 0.2 : lightStep;
2246
- lightStep = [6, 7].includes(lightSteps) ? 0.15 : lightStep;
2247
- lightStep = [8, 9].includes(lightSteps) ? 0.11 : lightStep;
2248
- lightStep = [10, 11].includes(lightSteps) ? 0.09 : lightStep;
2249
- lightStep = [12, 13].includes(lightSteps) ? 0.075 : lightStep;
2250
- lightStep = lightSteps > 13 ? estimatedStep : lightStep;
2206
+ control.append(
2207
+ createElement({
2208
+ tagName: 'div',
2209
+ className: `visual-control visual-control${i}`,
2210
+ }),
2211
+ );
2251
2212
 
2252
- // light tints
2253
- for (let i = 1; i < half + 1; i += 1) {
2254
- lightnessArray = [...lightnessArray, (0.5 + lightStep * (i))];
2255
- }
2213
+ const knob = createElement({
2214
+ tagName: 'div',
2215
+ className: `${c} knob`,
2216
+ ariaLive: 'polite',
2217
+ });
2256
2218
 
2257
- // dark tints
2258
- for (let i = 1; i < lightSteps - half; i += 1) {
2259
- lightnessArray = [(0.5 - lightStep * (i)), ...lightnessArray];
2260
- }
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
+ });
2261
2227
 
2262
- // feed `colors` Array
2263
- for (let i = 0; i < hueSteps; i += 1) {
2264
- const currentHue = ((hue + i * hueStep) % 360) / 360;
2265
- lightnessArray.forEach((l) => {
2266
- colors.push(new Color({ h: currentHue, s: 1, l }).toHexString());
2267
- });
2268
- }
2228
+ return colorControls;
2229
+ }
2269
2230
 
2270
- this.hue = hue;
2271
- this.hueSteps = hueSteps;
2272
- this.lightSteps = lightSteps;
2273
- this.colors = colors;
2274
- }
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
+ });
2275
2240
  }
2276
2241
 
2277
2242
  /**
@@ -2307,7 +2272,8 @@
2307
2272
  optionSize = fit > 5 && isMultiLine ? 1.5 : optionSize;
2308
2273
  const menuHeight = `${(rowCount || 1) * optionSize}rem`;
2309
2274
  const menuHeightHover = `calc(${rowCountHover} * ${optionSize}rem + ${rowCountHover - 1} * ${gap})`;
2310
-
2275
+ /** @type {HTMLUListElement} */
2276
+ // @ts-ignore -- <UL> is an `HTMLElement`
2311
2277
  const menu = createElement({
2312
2278
  tagName: 'ul',
2313
2279
  className: finalClass,
@@ -2315,7 +2281,7 @@
2315
2281
  setAttribute(menu, 'role', 'listbox');
2316
2282
  setAttribute(menu, ariaLabel, menuLabel);
2317
2283
 
2318
- if (isScrollable) { // @ts-ignore
2284
+ if (isScrollable) {
2319
2285
  setCSSProperties(menu, {
2320
2286
  '--grid-item-size': `${optionSize}rem`,
2321
2287
  '--grid-fit': fit,
@@ -2326,15 +2292,19 @@
2326
2292
  }
2327
2293
 
2328
2294
  colorsArray.forEach((x) => {
2329
- const [value, label] = x.trim().split(':');
2330
- const xRealColor = new Color(value, format).toString();
2331
- const isActive = xRealColor === getAttribute(input, 'value');
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');
2332
2302
  const active = isActive ? ' active' : '';
2333
2303
 
2334
2304
  const option = createElement({
2335
2305
  tagName: 'li',
2336
2306
  className: `color-option${active}`,
2337
- innerText: `${label || x}`,
2307
+ innerText: `${label || value}`,
2338
2308
  });
2339
2309
 
2340
2310
  setAttribute(option, tabIndex, '0');
@@ -2343,7 +2313,7 @@
2343
2313
  setAttribute(option, ariaSelected, isActive ? 'true' : 'false');
2344
2314
 
2345
2315
  if (isOptionsMenu) {
2346
- setElementStyle(option, { backgroundColor: x });
2316
+ setElementStyle(option, { backgroundColor: value });
2347
2317
  }
2348
2318
 
2349
2319
  menu.append(option);
@@ -2352,55 +2322,10 @@
2352
2322
  }
2353
2323
 
2354
2324
  /**
2355
- * Check if a string is valid JSON string.
2356
- * @param {string} str the string input
2357
- * @returns {boolean} the query result
2358
- */
2359
- function isValidJSON(str) {
2360
- try {
2361
- JSON.parse(str);
2362
- } catch (e) {
2363
- return false;
2364
- }
2365
- return true;
2366
- }
2367
-
2368
- var version = "0.0.1";
2369
-
2370
- // @ts-ignore
2371
-
2372
- const Version = version;
2373
-
2374
- // ColorPicker GC
2375
- // ==============
2376
- const colorPickerString = 'color-picker';
2377
- const colorPickerSelector = `[data-function="${colorPickerString}"]`;
2378
- const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2379
- const colorPickerDefaults = {
2380
- componentLabels: colorPickerLabels,
2381
- colorLabels: colorNames,
2382
- format: 'rgb',
2383
- colorPresets: false,
2384
- colorKeywords: false,
2385
- };
2386
-
2387
- // ColorPicker Static Methods
2388
- // ==========================
2389
-
2390
- /** @type {CP.GetInstance<ColorPicker>} */
2391
- const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
2392
-
2393
- /** @type {CP.InitCallback<ColorPicker>} */
2394
- const initColorPicker = (element) => new ColorPicker(element);
2395
-
2396
- // ColorPicker Private Methods
2397
- // ===========================
2398
-
2399
- /**
2400
- * Generate HTML markup and update instance properties.
2401
- * @param {ColorPicker} self
2402
- */
2403
- function initCallback(self) {
2325
+ * Generate HTML markup and update instance properties.
2326
+ * @param {CP.ColorPicker} self
2327
+ */
2328
+ function setMarkup(self) {
2404
2329
  const {
2405
2330
  input, parent, format, id, componentLabels, colorKeywords, colorPresets,
2406
2331
  } = self;
@@ -2415,9 +2340,7 @@
2415
2340
  self.color = new Color(color, format);
2416
2341
 
2417
2342
  // set initial controls dimensions
2418
- // make the controls smaller on mobile
2419
- const dropClass = isMobile ? ' mobile' : '';
2420
- const formatString = format === 'hex' ? hexLabel : format.toUpperCase();
2343
+ const formatString = format === 'hex' ? hexLabel : toUpperCase(format);
2421
2344
 
2422
2345
  const pickerBtn = createElement({
2423
2346
  id: `picker-btn-${id}`,
@@ -2434,7 +2357,7 @@
2434
2357
 
2435
2358
  const pickerDropdown = createElement({
2436
2359
  tagName: 'div',
2437
- className: `color-dropdown picker${dropClass}`,
2360
+ className: 'color-dropdown picker',
2438
2361
  });
2439
2362
  setAttribute(pickerDropdown, ariaLabelledBy, `picker-btn-${id}`);
2440
2363
  setAttribute(pickerDropdown, 'role', 'group');
@@ -2450,7 +2373,7 @@
2450
2373
  if (colorKeywords || colorPresets) {
2451
2374
  const presetsDropdown = createElement({
2452
2375
  tagName: 'div',
2453
- className: `color-dropdown scrollable menu${dropClass}`,
2376
+ className: 'color-dropdown scrollable menu',
2454
2377
  });
2455
2378
 
2456
2379
  // color presets
@@ -2500,6 +2423,37 @@
2500
2423
  setAttribute(input, tabIndex, '-1');
2501
2424
  }
2502
2425
 
2426
+ var version = "0.0.2alpha3";
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
+
2503
2457
  /**
2504
2458
  * Add / remove `ColorPicker` main event listeners.
2505
2459
  * @param {ColorPicker} self
@@ -2512,8 +2466,6 @@
2512
2466
  fn(input, focusinEvent, self.showPicker);
2513
2467
  fn(pickerToggle, mouseclickEvent, self.togglePicker);
2514
2468
 
2515
- fn(input, keydownEvent, self.keyToggle);
2516
-
2517
2469
  if (menuToggle) {
2518
2470
  fn(menuToggle, mouseclickEvent, self.toggleMenu);
2519
2471
  }
@@ -2551,8 +2503,7 @@
2551
2503
  fn(doc, pointerEvents.move, self.pointerMove);
2552
2504
  fn(doc, pointerEvents.up, self.pointerUp);
2553
2505
  fn(parent, focusoutEvent, self.handleFocusOut);
2554
- // @ts-ignore -- this is `Window`
2555
- fn(win, keyupEvent, self.handleDismiss);
2506
+ fn(doc, keyupEvent, self.handleDismiss);
2556
2507
  }
2557
2508
 
2558
2509
  /**
@@ -2636,7 +2587,7 @@
2636
2587
  const input = querySelector(target);
2637
2588
 
2638
2589
  // invalidate
2639
- if (!input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
2590
+ if (!input) throw new TypeError(`ColorPicker target "${target}" cannot be found.`);
2640
2591
  self.input = input;
2641
2592
 
2642
2593
  const parent = closest(input, colorPickerParentSelector);
@@ -2683,15 +2634,14 @@
2683
2634
  });
2684
2635
 
2685
2636
  // update and expose component labels
2686
- const tempLabels = ObjectAssign({}, colorPickerLabels);
2687
- const jsonLabels = componentLabels && isValidJSON(componentLabels)
2688
- ? JSON.parse(componentLabels) : componentLabels || {};
2637
+ const tempComponentLabels = componentLabels && isValidJSON(componentLabels)
2638
+ ? JSON.parse(componentLabels) : componentLabels;
2689
2639
 
2690
2640
  /** @type {Record<string, string>} */
2691
- self.componentLabels = ObjectAssign(tempLabels, jsonLabels);
2641
+ self.componentLabels = ObjectAssign(colorPickerLabels, tempComponentLabels);
2692
2642
 
2693
2643
  /** @type {Color} */
2694
- self.color = new Color('white', format);
2644
+ self.color = new Color(input.value || '#fff', format);
2695
2645
 
2696
2646
  /** @type {CP.ColorFormats} */
2697
2647
  self.format = format;
@@ -2700,7 +2650,7 @@
2700
2650
  if (colorKeywords instanceof Array) {
2701
2651
  self.colorKeywords = colorKeywords;
2702
2652
  } else if (typeof colorKeywords === 'string' && colorKeywords.length) {
2703
- self.colorKeywords = colorKeywords.split(',');
2653
+ self.colorKeywords = colorKeywords.split(',').map((x) => x.trim());
2704
2654
  }
2705
2655
 
2706
2656
  // set colour presets
@@ -2729,11 +2679,10 @@
2729
2679
  self.handleFocusOut = self.handleFocusOut.bind(self);
2730
2680
  self.changeHandler = self.changeHandler.bind(self);
2731
2681
  self.handleDismiss = self.handleDismiss.bind(self);
2732
- self.keyToggle = self.keyToggle.bind(self);
2733
2682
  self.handleKnobs = self.handleKnobs.bind(self);
2734
2683
 
2735
2684
  // generate markup
2736
- initCallback(self);
2685
+ setMarkup(self);
2737
2686
 
2738
2687
  const [colorPicker, colorMenu] = getElementsByClassName('color-dropdown', parent);
2739
2688
  // set main elements
@@ -2821,76 +2770,83 @@
2821
2770
  return inputValue !== '' && new Color(inputValue).isValid;
2822
2771
  }
2823
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
+
2824
2821
  /** Updates `ColorPicker` visuals. */
2825
2822
  updateVisuals() {
2826
2823
  const self = this;
2827
2824
  const {
2828
- format, controlPositions, visuals,
2825
+ controlPositions, visuals,
2829
2826
  } = self;
2830
2827
  const [v1, v2, v3] = visuals;
2831
- const { offsetWidth, offsetHeight } = v1;
2832
- const hue = format === 'hsl'
2833
- ? controlPositions.c1x / offsetWidth
2834
- : controlPositions.c2y / offsetHeight;
2835
- // @ts-ignore - `hslToRgb` is assigned to `Color` as static method
2836
- const { r, g, b } = Color.hslToRgb(hue, 1, 0.5);
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();
2837
2831
  const whiteGrad = 'linear-gradient(rgb(255,255,255) 0%, rgb(255,255,255) 100%)';
2838
2832
  const alpha = 1 - controlPositions.c3y / offsetHeight;
2839
2833
  const roundA = roundPart((alpha * 100)) / 100;
2840
2834
 
2841
- if (format !== 'hsl') {
2842
- const fill = new Color({
2843
- h: hue, s: 1, l: 0.5, a: alpha,
2844
- }).toRgbString();
2845
- const hueGradient = `linear-gradient(
2846
- rgb(255,0,0) 0%, rgb(255,255,0) 16.67%,
2847
- rgb(0,255,0) 33.33%, rgb(0,255,255) 50%,
2848
- rgb(0,0,255) 66.67%, rgb(255,0,255) 83.33%,
2849
- rgb(255,0,0) 100%)`;
2850
- setElementStyle(v1, {
2851
- background: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,${roundA}) 100%),
2852
- linear-gradient(to right, rgba(255,255,255,${roundA}) 0%, ${fill} 100%),
2853
- ${whiteGrad}`,
2854
- });
2855
- setElementStyle(v2, { background: hueGradient });
2856
- } else {
2857
- const saturation = roundPart((controlPositions.c2y / offsetHeight) * 100);
2858
- const fill0 = new Color({
2859
- r: 255, g: 0, b: 0, a: alpha,
2860
- }).saturate(-saturation).toRgbString();
2861
- const fill1 = new Color({
2862
- r: 255, g: 255, b: 0, a: alpha,
2863
- }).saturate(-saturation).toRgbString();
2864
- const fill2 = new Color({
2865
- r: 0, g: 255, b: 0, a: alpha,
2866
- }).saturate(-saturation).toRgbString();
2867
- const fill3 = new Color({
2868
- r: 0, g: 255, b: 255, a: alpha,
2869
- }).saturate(-saturation).toRgbString();
2870
- const fill4 = new Color({
2871
- r: 0, g: 0, b: 255, a: alpha,
2872
- }).saturate(-saturation).toRgbString();
2873
- const fill5 = new Color({
2874
- r: 255, g: 0, b: 255, a: alpha,
2875
- }).saturate(-saturation).toRgbString();
2876
- const fill6 = new Color({
2877
- r: 255, g: 0, b: 0, a: alpha,
2878
- }).saturate(-saturation).toRgbString();
2879
- const fillGradient = `linear-gradient(to right,
2880
- ${fill0} 0%, ${fill1} 16.67%, ${fill2} 33.33%, ${fill3} 50%,
2881
- ${fill4} 66.67%, ${fill5} 83.33%, ${fill6} 100%)`;
2882
- const lightGrad = `linear-gradient(rgba(255,255,255,${roundA}) 0%, rgba(255,255,255,0) 50%),
2883
- linear-gradient(rgba(0,0,0,0) 50%, rgba(0,0,0,${roundA}) 100%)`;
2884
-
2885
- setElementStyle(v1, { background: `${lightGrad},${fillGradient},${whiteGrad}` });
2886
- const {
2887
- r: gr, g: gg, b: gb,
2888
- } = new Color({ r, g, b }).greyscale().toRgb();
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 });
2889
2849
 
2890
- setElementStyle(v2, {
2891
- background: `linear-gradient(rgb(${r},${g},${b}) 0%, rgb(${gr},${gg},${gb}) 100%)`,
2892
- });
2893
- }
2894
2850
  setElementStyle(v3, {
2895
2851
  background: `linear-gradient(rgba(${r},${g},${b},1) 0%,rgba(${r},${g},${b},0) 100%)`,
2896
2852
  });
@@ -2929,7 +2885,7 @@
2929
2885
  const self = this;
2930
2886
  const { activeElement } = getDocument(self.input);
2931
2887
 
2932
- if ((isMobile && self.dragElement)
2888
+ if ((e.type === touchmoveEvent && self.dragElement)
2933
2889
  || (activeElement && self.controlKnobs.includes(activeElement))) {
2934
2890
  e.stopPropagation();
2935
2891
  e.preventDefault();
@@ -3040,13 +2996,13 @@
3040
2996
  const [v1, v2, v3] = visuals;
3041
2997
  const [c1, c2, c3] = controlKnobs;
3042
2998
  /** @type {HTMLElement} */
3043
- const visual = hasClass(target, 'visual-control')
3044
- ? target : querySelector('.visual-control', target.parentElement);
2999
+ const visual = controlKnobs.includes(target) ? target.previousElementSibling : target;
3045
3000
  const visualRect = getBoundingClientRect(visual);
3001
+ const html = getDocumentElement(v1);
3046
3002
  const X = type === 'touchstart' ? touches[0].pageX : pageX;
3047
3003
  const Y = type === 'touchstart' ? touches[0].pageY : pageY;
3048
- const offsetX = X - window.pageXOffset - visualRect.left;
3049
- const offsetY = Y - window.pageYOffset - visualRect.top;
3004
+ const offsetX = X - html.scrollLeft - visualRect.left;
3005
+ const offsetY = Y - html.scrollTop - visualRect.top;
3050
3006
 
3051
3007
  if (target === v1 || target === c1) {
3052
3008
  self.dragElement = visual;
@@ -3106,10 +3062,11 @@
3106
3062
  if (!dragElement) return;
3107
3063
 
3108
3064
  const controlRect = getBoundingClientRect(dragElement);
3109
- const X = type === 'touchmove' ? touches[0].pageX : pageX;
3110
- const Y = type === 'touchmove' ? touches[0].pageY : pageY;
3111
- const offsetX = X - window.pageXOffset - controlRect.left;
3112
- 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;
3113
3070
 
3114
3071
  if (dragElement === v1) {
3115
3072
  self.changeControl1(offsetX, offsetY);
@@ -3136,19 +3093,19 @@
3136
3093
  if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
3137
3094
  e.preventDefault();
3138
3095
 
3139
- const { format, controlKnobs, visuals } = self;
3096
+ const { controlKnobs, visuals } = self;
3140
3097
  const { offsetWidth, offsetHeight } = visuals[0];
3141
3098
  const [c1, c2, c3] = controlKnobs;
3142
3099
  const { activeElement } = getDocument(c1);
3143
3100
  const currentKnob = controlKnobs.find((x) => x === activeElement);
3144
- const yRatio = offsetHeight / (format === 'hsl' ? 100 : 360);
3101
+ const yRatio = offsetHeight / 360;
3145
3102
 
3146
3103
  if (currentKnob) {
3147
3104
  let offsetX = 0;
3148
3105
  let offsetY = 0;
3149
3106
 
3150
3107
  if (target === c1) {
3151
- const xRatio = offsetWidth / (format === 'hsl' ? 360 : 100);
3108
+ const xRatio = offsetWidth / 100;
3152
3109
 
3153
3110
  if ([keyArrowLeft, keyArrowRight].includes(code)) {
3154
3111
  self.controlPositions.c1x += code === keyArrowRight ? xRatio : -xRatio;
@@ -3198,7 +3155,7 @@
3198
3155
  if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
3199
3156
  if (activeElement === input) {
3200
3157
  if (isNonColorValue) {
3201
- colorSource = 'white';
3158
+ colorSource = currentValue === 'transparent' ? 'rgba(0,0,0,0)' : 'rgb(0,0,0)';
3202
3159
  } else {
3203
3160
  colorSource = currentValue;
3204
3161
  }
@@ -3249,9 +3206,7 @@
3249
3206
  changeControl1(X, Y) {
3250
3207
  const self = this;
3251
3208
  let [offsetX, offsetY] = [0, 0];
3252
- const {
3253
- format, controlPositions, visuals,
3254
- } = self;
3209
+ const { controlPositions, visuals } = self;
3255
3210
  const { offsetHeight, offsetWidth } = visuals[0];
3256
3211
 
3257
3212
  if (X > offsetWidth) offsetX = offsetWidth;
@@ -3260,29 +3215,19 @@
3260
3215
  if (Y > offsetHeight) offsetY = offsetHeight;
3261
3216
  else if (Y >= 0) offsetY = Y;
3262
3217
 
3263
- const hue = format === 'hsl'
3264
- ? offsetX / offsetWidth
3265
- : controlPositions.c2y / offsetHeight;
3218
+ const hue = controlPositions.c2y / offsetHeight;
3266
3219
 
3267
- const saturation = format === 'hsl'
3268
- ? 1 - controlPositions.c2y / offsetHeight
3269
- : offsetX / offsetWidth;
3220
+ const saturation = offsetX / offsetWidth;
3270
3221
 
3271
3222
  const lightness = 1 - offsetY / offsetHeight;
3272
3223
  const alpha = 1 - controlPositions.c3y / offsetHeight;
3273
3224
 
3274
- const colorObject = format === 'hsl'
3275
- ? {
3276
- h: hue, s: saturation, l: lightness, a: alpha,
3277
- }
3278
- : {
3279
- h: hue, s: saturation, v: lightness, a: alpha,
3280
- };
3281
-
3282
3225
  // new color
3283
3226
  const {
3284
3227
  r, g, b, a,
3285
- } = new Color(colorObject);
3228
+ } = new Color({
3229
+ h: hue, s: saturation, v: lightness, a: alpha,
3230
+ });
3286
3231
 
3287
3232
  ObjectAssign(self.color, {
3288
3233
  r, g, b, a,
@@ -3309,7 +3254,7 @@
3309
3254
  changeControl2(Y) {
3310
3255
  const self = this;
3311
3256
  const {
3312
- format, controlPositions, visuals,
3257
+ controlPositions, visuals,
3313
3258
  } = self;
3314
3259
  const { offsetHeight, offsetWidth } = visuals[0];
3315
3260
 
@@ -3318,26 +3263,17 @@
3318
3263
  if (Y > offsetHeight) offsetY = offsetHeight;
3319
3264
  else if (Y >= 0) offsetY = Y;
3320
3265
 
3321
- const hue = format === 'hsl'
3322
- ? controlPositions.c1x / offsetWidth
3323
- : offsetY / offsetHeight;
3324
- const saturation = format === 'hsl'
3325
- ? 1 - offsetY / offsetHeight
3326
- : controlPositions.c1x / offsetWidth;
3266
+ const hue = offsetY / offsetHeight;
3267
+ const saturation = controlPositions.c1x / offsetWidth;
3327
3268
  const lightness = 1 - controlPositions.c1y / offsetHeight;
3328
3269
  const alpha = 1 - controlPositions.c3y / offsetHeight;
3329
- const colorObject = format === 'hsl'
3330
- ? {
3331
- h: hue, s: saturation, l: lightness, a: alpha,
3332
- }
3333
- : {
3334
- h: hue, s: saturation, v: lightness, a: alpha,
3335
- };
3336
3270
 
3337
3271
  // new color
3338
3272
  const {
3339
3273
  r, g, b, a,
3340
- } = new Color(colorObject);
3274
+ } = new Color({
3275
+ h: hue, s: saturation, v: lightness, a: alpha,
3276
+ });
3341
3277
 
3342
3278
  ObjectAssign(self.color, {
3343
3279
  r, g, b, a,
@@ -3424,18 +3360,18 @@
3424
3360
  setControlPositions() {
3425
3361
  const self = this;
3426
3362
  const {
3427
- format, visuals, color, hsl, hsv,
3363
+ visuals, color, hsv,
3428
3364
  } = self;
3429
3365
  const { offsetHeight, offsetWidth } = visuals[0];
3430
3366
  const alpha = color.a;
3431
- const hue = hsl.h;
3367
+ const hue = hsv.h;
3432
3368
 
3433
- const saturation = format !== 'hsl' ? hsv.s : hsl.s;
3434
- const lightness = format !== 'hsl' ? hsv.v : hsl.l;
3369
+ const saturation = hsv.s;
3370
+ const lightness = hsv.v;
3435
3371
 
3436
- self.controlPositions.c1x = format !== 'hsl' ? saturation * offsetWidth : hue * offsetWidth;
3372
+ self.controlPositions.c1x = saturation * offsetWidth;
3437
3373
  self.controlPositions.c1y = (1 - lightness) * offsetHeight;
3438
- self.controlPositions.c2y = format !== 'hsl' ? hue * offsetHeight : (1 - saturation) * offsetHeight;
3374
+ self.controlPositions.c2y = hue * offsetHeight;
3439
3375
  self.controlPositions.c3y = (1 - alpha) * offsetHeight;
3440
3376
  }
3441
3377
 
@@ -3443,78 +3379,40 @@
3443
3379
  updateAppearance() {
3444
3380
  const self = this;
3445
3381
  const {
3446
- componentLabels, colorLabels, color, parent,
3447
- hsl, hsv, hex, format, controlKnobs,
3382
+ componentLabels, color, parent,
3383
+ hsv, hex, format, controlKnobs,
3448
3384
  } = self;
3449
3385
  const {
3450
3386
  appearanceLabel, hexLabel, valueLabel,
3451
3387
  } = componentLabels;
3452
- const { r, g, b } = color.toRgb();
3388
+ let { r, g, b } = color.toRgb();
3453
3389
  const [knob1, knob2, knob3] = controlKnobs;
3454
- const hue = roundPart(hsl.h * 360);
3390
+ const hue = roundPart(hsv.h * 360);
3455
3391
  const alpha = color.a;
3456
- const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
3457
- const saturation = roundPart(saturationSource * 100);
3458
- const lightness = roundPart(hsl.l * 100);
3459
- const hsvl = hsv.v * 100;
3460
- let colorName;
3461
-
3462
- // determine color appearance
3463
- if (lightness === 100 && saturation === 0) {
3464
- colorName = colorLabels.white;
3465
- } else if (lightness === 0) {
3466
- colorName = colorLabels.black;
3467
- } else if (saturation === 0) {
3468
- colorName = colorLabels.grey;
3469
- } else if (hue < 15 || hue >= 345) {
3470
- colorName = colorLabels.red;
3471
- } else if (hue >= 15 && hue < 45) {
3472
- colorName = hsvl > 80 && saturation > 80 ? colorLabels.orange : colorLabels.brown;
3473
- } else if (hue >= 45 && hue < 75) {
3474
- const isGold = hue > 46 && hue < 54 && hsvl < 80 && saturation > 90;
3475
- const isOlive = hue >= 54 && hue < 75 && hsvl < 80;
3476
- colorName = isGold ? colorLabels.gold : colorLabels.yellow;
3477
- colorName = isOlive ? colorLabels.olive : colorName;
3478
- } else if (hue >= 75 && hue < 155) {
3479
- colorName = hsvl < 68 ? colorLabels.green : colorLabels.lime;
3480
- } else if (hue >= 155 && hue < 175) {
3481
- colorName = colorLabels.teal;
3482
- } else if (hue >= 175 && hue < 195) {
3483
- colorName = colorLabels.cyan;
3484
- } else if (hue >= 195 && hue < 255) {
3485
- colorName = colorLabels.blue;
3486
- } else if (hue >= 255 && hue < 270) {
3487
- colorName = colorLabels.violet;
3488
- } else if (hue >= 270 && hue < 295) {
3489
- colorName = colorLabels.magenta;
3490
- } else if (hue >= 295 && hue < 345) {
3491
- colorName = colorLabels.pink;
3492
- }
3392
+ const saturation = roundPart(hsv.s * 100);
3393
+ const lightness = roundPart(hsv.v * 100);
3394
+ const colorName = self.appearance;
3493
3395
 
3494
3396
  let colorLabel = `${hexLabel} ${hex.split('').join(' ')}`;
3495
3397
 
3496
- if (format === 'hsl') {
3497
- colorLabel = `HSL: ${hue}°, ${saturation}%, ${lightness}%`;
3498
- setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3499
- setAttribute(knob1, ariaValueText, `${hue}° & ${lightness}%`);
3500
- setAttribute(knob1, ariaValueNow, `${hue}`);
3501
- setAttribute(knob2, ariaValueText, `${saturation}%`);
3502
- setAttribute(knob2, ariaValueNow, `${saturation}`);
3503
- } else if (format === 'hwb') {
3398
+ if (format === 'hwb') {
3504
3399
  const { hwb } = self;
3505
3400
  const whiteness = roundPart(hwb.w * 100);
3506
3401
  const blackness = roundPart(hwb.b * 100);
3507
3402
  colorLabel = `HWB: ${hue}°, ${whiteness}%, ${blackness}%`;
3508
- setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3509
3403
  setAttribute(knob1, ariaValueText, `${whiteness}% & ${blackness}%`);
3510
3404
  setAttribute(knob1, ariaValueNow, `${whiteness}`);
3405
+ setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3511
3406
  setAttribute(knob2, ariaValueText, `${hue}%`);
3512
3407
  setAttribute(knob2, ariaValueNow, `${hue}`);
3513
3408
  } else {
3409
+ [r, g, b] = [r, g, b].map(roundPart);
3410
+ colorLabel = format === 'hsl' ? `HSL: ${hue}°, ${saturation}%, ${lightness}%` : colorLabel;
3514
3411
  colorLabel = format === 'rgb' ? `RGB: ${r}, ${g}, ${b}` : colorLabel;
3515
- setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3412
+
3516
3413
  setAttribute(knob1, ariaValueText, `${lightness}% & ${saturation}%`);
3517
3414
  setAttribute(knob1, ariaValueNow, `${lightness}`);
3415
+ setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3518
3416
  setAttribute(knob2, ariaValueText, `${hue}°`);
3519
3417
  setAttribute(knob2, ariaValueNow, `${hue}`);
3520
3418
  }
@@ -3609,37 +3507,13 @@
3609
3507
  }
3610
3508
  }
3611
3509
 
3612
- /**
3613
- * The `Space` & `Enter` keys specific event listener.
3614
- * Toggle visibility of the `ColorPicker` / the presets menu, showing one will hide the other.
3615
- * @param {KeyboardEvent} e
3616
- * @this {ColorPicker}
3617
- */
3618
- keyToggle(e) {
3619
- const self = this;
3620
- const { menuToggle } = self;
3621
- const { activeElement } = getDocument(menuToggle);
3622
- const { code } = e;
3623
-
3624
- if ([keyEnter, keySpace].includes(code)) {
3625
- if ((menuToggle && activeElement === menuToggle) || !activeElement) {
3626
- e.preventDefault();
3627
- if (!activeElement) {
3628
- self.togglePicker(e);
3629
- } else {
3630
- self.toggleMenu();
3631
- }
3632
- }
3633
- }
3634
- }
3635
-
3636
3510
  /**
3637
3511
  * Toggle the `ColorPicker` dropdown visibility.
3638
- * @param {Event} e
3512
+ * @param {Event=} e
3639
3513
  * @this {ColorPicker}
3640
3514
  */
3641
3515
  togglePicker(e) {
3642
- e.preventDefault();
3516
+ if (e) e.preventDefault();
3643
3517
  const self = this;
3644
3518
  const { colorPicker } = self;
3645
3519
 
@@ -3660,8 +3534,13 @@
3660
3534
  }
3661
3535
  }
3662
3536
 
3663
- /** Toggles the visibility of the `ColorPicker` presets menu. */
3664
- 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();
3665
3544
  const self = this;
3666
3545
  const { colorMenu } = self;
3667
3546
 
@@ -3687,6 +3566,10 @@
3687
3566
  const relatedBtn = openPicker ? pickerToggle : menuToggle;
3688
3567
  const animationDuration = openDropdown && getElementTransitionDuration(openDropdown);
3689
3568
 
3569
+ // if (!self.isValid) {
3570
+ self.value = self.color.toString(true);
3571
+ // }
3572
+
3690
3573
  if (openDropdown) {
3691
3574
  removeClass(openDropdown, 'show');
3692
3575
  setAttribute(relatedBtn, ariaExpanded, 'false');
@@ -3700,9 +3583,6 @@
3700
3583
  }, animationDuration);
3701
3584
  }
3702
3585
 
3703
- if (!self.isValid) {
3704
- self.value = self.color.toString();
3705
- }
3706
3586
  if (!focusPrevented) {
3707
3587
  focus(pickerToggle);
3708
3588
  }
@@ -3747,4 +3627,4 @@
3747
3627
 
3748
3628
  return ColorPicker;
3749
3629
 
3750
- })));
3630
+ }));