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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +32 -15
  2. package/dist/css/color-picker.css +38 -15
  3. package/dist/css/color-picker.min.css +2 -2
  4. package/dist/css/color-picker.rtl.css +38 -15
  5. package/dist/css/color-picker.rtl.min.css +2 -2
  6. package/dist/js/color-esm.js +1178 -0
  7. package/dist/js/color-esm.min.js +2 -0
  8. package/dist/js/color-palette-esm.js +1252 -0
  9. package/dist/js/color-palette-esm.min.js +2 -0
  10. package/dist/js/color-palette.js +1260 -0
  11. package/dist/js/color-palette.min.js +2 -0
  12. package/dist/js/color-picker-element-esm.js +433 -424
  13. package/dist/js/color-picker-element-esm.min.js +2 -2
  14. package/dist/js/color-picker-element.js +435 -426
  15. package/dist/js/color-picker-element.min.js +2 -2
  16. package/dist/js/color-picker-esm.js +745 -739
  17. package/dist/js/color-picker-esm.min.js +2 -2
  18. package/dist/js/color-picker.js +747 -741
  19. package/dist/js/color-picker.min.js +2 -2
  20. package/dist/js/color.js +1186 -0
  21. package/dist/js/color.min.js +2 -0
  22. package/package.json +19 -3
  23. package/src/js/color-palette.js +28 -12
  24. package/src/js/color-picker-element.js +8 -4
  25. package/src/js/color-picker.js +84 -172
  26. package/src/js/color.js +125 -131
  27. package/src/js/util/getColorControls.js +3 -3
  28. package/src/js/util/getColorForm.js +0 -1
  29. package/src/js/util/getColorMenu.js +31 -33
  30. package/src/js/util/roundPart.js +9 -0
  31. package/src/js/util/setCSSProperties.js +12 -0
  32. package/src/js/util/setMarkup.js +122 -0
  33. package/src/js/util/tabindex.js +3 -0
  34. package/src/js/util/version.js +6 -0
  35. package/src/scss/color-picker.scss +35 -16
  36. package/types/cp.d.ts +48 -20
  37. package/src/js/util/templates.js +0 -10
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * ColorPicker v0.0.1alpha2 (http://thednp.github.io/color-picker)
2
+ * ColorPicker v0.0.2alpha1 (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
  */
@@ -129,24 +129,6 @@ const ariaValueText = 'aria-valuetext';
129
129
  */
130
130
  const ariaValueNow = 'aria-valuenow';
131
131
 
132
- /**
133
- * A global namespace for aria-haspopup.
134
- * @type {string}
135
- */
136
- const ariaHasPopup = 'aria-haspopup';
137
-
138
- /**
139
- * A global namespace for aria-hidden.
140
- * @type {string}
141
- */
142
- const ariaHidden = 'aria-hidden';
143
-
144
- /**
145
- * A global namespace for aria-labelledby.
146
- * @type {string}
147
- */
148
- const ariaLabelledBy = 'aria-labelledby';
149
-
150
132
  /**
151
133
  * A global namespace for `ArrowDown` key.
152
134
  * @type {string} e.which = 40 equivalent
@@ -273,37 +255,6 @@ const resizeEvent = 'resize';
273
255
  */
274
256
  const focusoutEvent = 'focusout';
275
257
 
276
- // @ts-ignore
277
- const { userAgentData: uaDATA } = navigator;
278
-
279
- /**
280
- * A global namespace for `userAgentData` object.
281
- */
282
- const userAgentData = uaDATA;
283
-
284
- const { userAgent: userAgentString } = navigator;
285
-
286
- /**
287
- * A global namespace for `navigator.userAgent` string.
288
- */
289
- const userAgent = userAgentString;
290
-
291
- const mobileBrands = /iPhone|iPad|iPod|Android/i;
292
- let isMobileCheck = false;
293
-
294
- if (userAgentData) {
295
- isMobileCheck = userAgentData.brands
296
- .some((/** @type {Record<String, any>} */x) => mobileBrands.test(x.brand));
297
- } else {
298
- isMobileCheck = mobileBrands.test(userAgent);
299
- }
300
-
301
- /**
302
- * A global `boolean` for mobile detection.
303
- * @type {boolean}
304
- */
305
- const isMobile = isMobileCheck;
306
-
307
258
  /**
308
259
  * Returns the `document` or the `#document` element.
309
260
  * @see https://github.com/floating-ui/floating-ui
@@ -524,60 +475,6 @@ function getElementsByClassName(selector, parent) {
524
475
  return lookUp.getElementsByClassName(selector);
525
476
  }
526
477
 
527
- /**
528
- * Shortcut for `Object.assign()` static method.
529
- * @param {Record<string, any>} obj a target object
530
- * @param {Record<string, any>} source a source object
531
- */
532
- const ObjectAssign = (obj, source) => Object.assign(obj, source);
533
-
534
- /**
535
- * This is a shortie for `document.createElement` method
536
- * which allows you to create a new `HTMLElement` for a given `tagName`
537
- * or based on an object with specific non-readonly attributes:
538
- * `id`, `className`, `textContent`, `style`, etc.
539
- * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
540
- *
541
- * @param {Record<string, string> | string} param `tagName` or object
542
- * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
543
- */
544
- function createElement(param) {
545
- if (typeof param === 'string') {
546
- return getDocument().createElement(param);
547
- }
548
-
549
- const { tagName } = param;
550
- const attr = { ...param };
551
- const newElement = createElement(tagName);
552
- delete attr.tagName;
553
- ObjectAssign(newElement, attr);
554
- return newElement;
555
- }
556
-
557
- /**
558
- * This is a shortie for `document.createElementNS` method
559
- * which allows you to create a new `HTMLElement` for a given `tagName`
560
- * or based on an object with specific non-readonly attributes:
561
- * `id`, `className`, `textContent`, `style`, etc.
562
- * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
563
- *
564
- * @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
565
- * @param {Record<string, string> | string} param `tagName` or object
566
- * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
567
- */
568
- function createElementNS(namespace, param) {
569
- if (typeof param === 'string') {
570
- return getDocument().createElementNS(namespace, param);
571
- }
572
-
573
- const { tagName } = param;
574
- const attr = { ...param };
575
- const newElement = createElementNS(namespace, tagName);
576
- delete attr.tagName;
577
- ObjectAssign(newElement, attr);
578
- return newElement;
579
- }
580
-
581
478
  /**
582
479
  * Shortcut for the `Element.dispatchEvent(Event)` method.
583
480
  *
@@ -586,6 +483,13 @@ function createElementNS(namespace, param) {
586
483
  */
587
484
  const dispatchEvent = (element, event) => element.dispatchEvent(event);
588
485
 
486
+ /**
487
+ * Shortcut for `Object.assign()` static method.
488
+ * @param {Record<string, any>} obj a target object
489
+ * @param {Record<string, any>} source a source object
490
+ */
491
+ const ObjectAssign = (obj, source) => Object.assign(obj, source);
492
+
589
493
  /** @type {Map<string, Map<HTMLElement | Element, Record<string, any>>>} */
590
494
  const componentData = new Map();
591
495
  /**
@@ -837,32 +741,10 @@ const setAttribute = (element, attribute, value) => element.setAttribute(attribu
837
741
  */
838
742
  const removeAttribute = (element, attribute) => element.removeAttribute(attribute);
839
743
 
840
- /** @type {Record<string, string>} */
841
- const colorPickerLabels = {
842
- pickerLabel: 'Colour Picker',
843
- appearanceLabel: 'Colour Appearance',
844
- valueLabel: 'Colour Value',
845
- toggleLabel: 'Select Colour',
846
- presetsLabel: 'Colour Presets',
847
- defaultsLabel: 'Colour Defaults',
848
- formatLabel: 'Format',
849
- alphaLabel: 'Alpha',
850
- hexLabel: 'Hexadecimal',
851
- hueLabel: 'Hue',
852
- whitenessLabel: 'Whiteness',
853
- blacknessLabel: 'Blackness',
854
- saturationLabel: 'Saturation',
855
- lightnessLabel: 'Lightness',
856
- redLabel: 'Red',
857
- greenLabel: 'Green',
858
- blueLabel: 'Blue',
859
- };
860
-
861
744
  /**
862
- * A list of 17 color names used for WAI-ARIA compliance.
863
- * @type {string[]}
745
+ * A global namespace for `document.head`.
864
746
  */
865
- const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
747
+ const { head: documentHead } = document;
866
748
 
867
749
  /**
868
750
  * A list of explicit default non-color values.
@@ -870,284 +752,104 @@ const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold',
870
752
  const nonColors = ['transparent', 'currentColor', 'inherit', 'revert', 'initial'];
871
753
 
872
754
  /**
873
- * Shortcut for `String.toUpperCase()`.
874
- *
875
- * @param {string} source input string
876
- * @returns {string} uppercase output string
755
+ * Round colour components, for all formats except HEX.
756
+ * @param {number} v one of the colour components
757
+ * @returns {number} the rounded number
877
758
  */
878
- const toUpperCase = (source) => source.toUpperCase();
759
+ function roundPart(v) {
760
+ const floor = Math.floor(v);
761
+ return v - floor < 0.5 ? floor : Math.round(v);
762
+ }
879
763
 
880
- const vHidden = 'v-hidden';
764
+ // Color supported formats
765
+ const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsv', 'hwb'];
881
766
 
882
- /**
883
- * Returns the color form for `ColorPicker`.
884
- *
885
- * @param {CP.ColorPicker} self the `ColorPicker` instance
886
- * @returns {HTMLElement | Element} a new `<div>` element with color component `<input>`
887
- */
888
- function getColorForm(self) {
889
- const { format, id, componentLabels } = self;
890
- const colorForm = createElement({
891
- tagName: 'div',
892
- className: `color-form ${format}`,
893
- });
767
+ // Hue angles
768
+ const ANGLES = 'deg|rad|grad|turn';
894
769
 
895
- let components = ['hex'];
896
- if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
897
- else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
898
- else if (format === 'hwb') components = ['hue', 'whiteness', 'blackness', 'alpha'];
770
+ // <http://www.w3.org/TR/css3-values/#integers>
771
+ const CSS_INTEGER = '[-\\+]?\\d+%?';
899
772
 
900
- components.forEach((c) => {
901
- const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
902
- const cID = `color_${format}_${c}_${id}`;
903
- const formatLabel = componentLabels[`${c}Label`];
904
- const cInputLabel = createElement({ tagName: 'label' });
905
- setAttribute(cInputLabel, 'for', cID);
906
- cInputLabel.append(
907
- createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
908
- createElement({ tagName: 'span', className: vHidden, innerText: formatLabel }),
909
- );
910
- const cInput = createElement({
911
- tagName: 'input',
912
- id: cID,
913
- // name: cID, - prevent saving the value to a form
914
- type: format === 'hex' ? 'text' : 'number',
915
- value: c === 'alpha' ? '100' : '0',
916
- className: `color-input ${c}`,
917
- });
918
- setAttribute(cInput, 'autocomplete', 'off');
919
- setAttribute(cInput, 'spellcheck', 'false');
773
+ // Include CSS3 Module
774
+ // <http://www.w3.org/TR/css3-values/#number-value>
775
+ const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
920
776
 
921
- // alpha
922
- let max = '100';
923
- let step = '1';
924
- if (c !== 'alpha') {
925
- if (format === 'rgb') {
926
- max = '255'; step = '1';
927
- } else if (c === 'hue') {
928
- max = '360'; step = '1';
929
- }
930
- }
931
- ObjectAssign(cInput, {
932
- min: '0',
933
- max,
934
- step,
935
- });
936
- // }
937
- colorForm.append(cInputLabel, cInput);
938
- });
939
- return colorForm;
940
- }
777
+ // Include CSS4 Module Hue degrees unit
778
+ // <https://www.w3.org/TR/css3-values/#angle-value>
779
+ const CSS_ANGLE = `[-\\+]?\\d*\\.?\\d+(?:${ANGLES})?`;
780
+
781
+ // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
782
+ const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
783
+
784
+ // Add angles to the mix
785
+ const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
786
+
787
+ // Start & end
788
+ const START_MATCH = '(?:[\\s|\\(\\s|\\s\\(\\s]+)?';
789
+ const END_MATCH = '(?:[\\s|\\)\\s]+)?';
790
+ // Components separation
791
+ const SEP = '(?:[,|\\s]+)';
792
+ const SEP2 = '(?:[,|\\/\\s]*)?';
793
+
794
+ // Actual matching.
795
+ // Parentheses and commas are optional, but not required.
796
+ // Whitespace can take the place of commas or opening paren
797
+ const PERMISSIVE_MATCH = `${START_MATCH}(${CSS_UNIT2})${SEP}(${CSS_UNIT})${SEP}(${CSS_UNIT})${SEP2}(${CSS_UNIT})?${END_MATCH}`;
798
+
799
+ const matchers = {
800
+ CSS_UNIT: new RegExp(CSS_UNIT2),
801
+ hwb: new RegExp(`hwb${PERMISSIVE_MATCH}`),
802
+ rgb: new RegExp(`rgb(?:a)?${PERMISSIVE_MATCH}`),
803
+ hsl: new RegExp(`hsl(?:a)?${PERMISSIVE_MATCH}`),
804
+ hsv: new RegExp(`hsv(?:a)?${PERMISSIVE_MATCH}`),
805
+ hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
806
+ hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
807
+ hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
808
+ hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
809
+ };
941
810
 
942
811
  /**
943
- * A global namespace for aria-label.
944
- * @type {string}
812
+ * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
813
+ * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
814
+ * @param {string} n testing number
815
+ * @returns {boolean} the query result
945
816
  */
946
- const ariaLabel = 'aria-label';
817
+ function isOnePointZero(n) {
818
+ return `${n}`.includes('.') && parseFloat(n) === 1;
819
+ }
947
820
 
948
821
  /**
949
- * A global namespace for aria-valuemin.
950
- * @type {string}
822
+ * Check to see if string passed in is a percentage
823
+ * @param {string} n testing number
824
+ * @returns {boolean} the query result
951
825
  */
952
- const ariaValueMin = 'aria-valuemin';
826
+ function isPercentage(n) {
827
+ return `${n}`.includes('%');
828
+ }
953
829
 
954
830
  /**
955
- * A global namespace for aria-valuemax.
956
- * @type {string}
831
+ * Check to see if string passed is a web safe colour.
832
+ * @see https://stackoverflow.com/a/16994164
833
+ * @param {string} color a colour name, EG: *red*
834
+ * @returns {boolean} the query result
957
835
  */
958
- const ariaValueMax = 'aria-valuemax';
836
+ function isColorName(color) {
837
+ if (nonColors.includes(color)
838
+ || ['#', ...COLOR_FORMAT].some((f) => color.includes(f))) return false;
839
+
840
+ return ['rgb(255, 255, 255)', 'rgb(0, 0, 0)'].every((c) => {
841
+ setElementStyle(documentHead, { color });
842
+ const computedColor = getElementStyle(documentHead, 'color');
843
+ setElementStyle(documentHead, { color: '' });
844
+ return computedColor !== c;
845
+ });
846
+ }
959
847
 
960
848
  /**
961
- * Returns all color controls for `ColorPicker`.
962
- *
963
- * @param {CP.ColorPicker} self the `ColorPicker` instance
964
- * @returns {HTMLElement | Element} color controls
965
- */
966
- function getColorControls(self) {
967
- const { format, componentLabels } = self;
968
- const {
969
- hueLabel, alphaLabel, lightnessLabel, saturationLabel,
970
- whitenessLabel, blacknessLabel,
971
- } = componentLabels;
972
-
973
- const max1 = format === 'hsl' ? 360 : 100;
974
- const max2 = format === 'hsl' ? 100 : 360;
975
- const max3 = 100;
976
-
977
- let ctrl1Label = format === 'hsl'
978
- ? `${hueLabel} & ${lightnessLabel}`
979
- : `${lightnessLabel} & ${saturationLabel}`;
980
-
981
- ctrl1Label = format === 'hwb'
982
- ? `${whitenessLabel} & ${blacknessLabel}`
983
- : ctrl1Label;
984
-
985
- const ctrl2Label = format === 'hsl'
986
- ? `${saturationLabel}`
987
- : `${hueLabel}`;
988
-
989
- const colorControls = createElement({
990
- tagName: 'div',
991
- className: `color-controls ${format}`,
992
- });
993
-
994
- const colorPointer = 'color-pointer';
995
- const colorSlider = 'color-slider';
996
-
997
- const controls = [
998
- {
999
- i: 1,
1000
- c: colorPointer,
1001
- l: ctrl1Label,
1002
- min: 0,
1003
- max: max1,
1004
- },
1005
- {
1006
- i: 2,
1007
- c: colorSlider,
1008
- l: ctrl2Label,
1009
- min: 0,
1010
- max: max2,
1011
- },
1012
- {
1013
- i: 3,
1014
- c: colorSlider,
1015
- l: alphaLabel,
1016
- min: 0,
1017
- max: max3,
1018
- },
1019
- ];
1020
-
1021
- controls.forEach((template) => {
1022
- const {
1023
- i, c, l, min, max,
1024
- } = template;
1025
- // const hidden = i === 2 && format === 'hwb' ? ' v-hidden' : '';
1026
- const control = createElement({
1027
- tagName: 'div',
1028
- // className: `color-control${hidden}`,
1029
- className: 'color-control',
1030
- });
1031
- setAttribute(control, 'role', 'presentation');
1032
-
1033
- control.append(
1034
- createElement({
1035
- tagName: 'div',
1036
- className: `visual-control visual-control${i}`,
1037
- }),
1038
- );
1039
-
1040
- const knob = createElement({
1041
- tagName: 'div',
1042
- className: `${c} knob`,
1043
- ariaLive: 'polite',
1044
- });
1045
-
1046
- setAttribute(knob, ariaLabel, l);
1047
- setAttribute(knob, 'role', 'slider');
1048
- setAttribute(knob, 'tabindex', '0');
1049
- setAttribute(knob, ariaValueMin, `${min}`);
1050
- setAttribute(knob, ariaValueMax, `${max}`);
1051
- control.append(knob);
1052
- colorControls.append(control);
1053
- });
1054
-
1055
- return colorControls;
1056
- }
1057
-
1058
- /**
1059
- * Returns the `document.head` or the `<head>` element.
1060
- *
1061
- * @param {(Node | HTMLElement | Element | globalThis)=} node
1062
- * @returns {HTMLElement | HTMLHeadElement}
1063
- */
1064
- function getDocumentHead(node) {
1065
- return getDocument(node).head;
1066
- }
1067
-
1068
- // Color supported formats
1069
- const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsb', 'hwb'];
1070
-
1071
- // Hue angles
1072
- const ANGLES = 'deg|rad|grad|turn';
1073
-
1074
- // <http://www.w3.org/TR/css3-values/#integers>
1075
- const CSS_INTEGER = '[-\\+]?\\d+%?';
1076
-
1077
- // Include CSS3 Module
1078
- // <http://www.w3.org/TR/css3-values/#number-value>
1079
- const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
1080
-
1081
- // Include CSS4 Module Hue degrees unit
1082
- // <https://www.w3.org/TR/css3-values/#angle-value>
1083
- const CSS_ANGLE = `[-\\+]?\\d*\\.?\\d+(?:${ANGLES})?`;
1084
-
1085
- // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
1086
- const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
1087
-
1088
- // Add angles to the mix
1089
- const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
1090
-
1091
- // Actual matching.
1092
- // Parentheses and commas are optional, but not required.
1093
- // Whitespace can take the place of commas or opening paren
1094
- const PERMISSIVE_MATCH = `[\\s|\\(]+(${CSS_UNIT2})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s|\\/\\s]*(${CSS_UNIT})?\\s*\\)?`;
1095
-
1096
- const matchers = {
1097
- CSS_UNIT: new RegExp(CSS_UNIT2),
1098
- hwb: new RegExp(`hwb${PERMISSIVE_MATCH}`),
1099
- rgb: new RegExp(`rgb(?:a)?${PERMISSIVE_MATCH}`),
1100
- hsl: new RegExp(`hsl(?:a)?${PERMISSIVE_MATCH}`),
1101
- hsv: new RegExp(`hsv(?:a)?${PERMISSIVE_MATCH}`),
1102
- hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1103
- hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
1104
- hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1105
- hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
1106
- };
1107
-
1108
- /**
1109
- * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
1110
- * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
1111
- * @param {string} n testing number
1112
- * @returns {boolean} the query result
1113
- */
1114
- function isOnePointZero(n) {
1115
- return `${n}`.includes('.') && parseFloat(n) === 1;
1116
- }
1117
-
1118
- /**
1119
- * Check to see if string passed in is a percentage
1120
- * @param {string} n testing number
1121
- * @returns {boolean} the query result
1122
- */
1123
- function isPercentage(n) {
1124
- return `${n}`.includes('%');
1125
- }
1126
-
1127
- /**
1128
- * Check to see if string passed in is an angle
1129
- * @param {string} n testing string
1130
- * @returns {boolean} the query result
1131
- */
1132
- function isAngle(n) {
1133
- return ANGLES.split('|').some((a) => `${n}`.includes(a));
1134
- }
1135
-
1136
- /**
1137
- * Check to see if string passed is a web safe colour.
1138
- * @param {string} color a colour name, EG: *red*
1139
- * @returns {boolean} the query result
1140
- */
1141
- function isColorName(color) {
1142
- return !['#', ...COLOR_FORMAT].some((s) => color.includes(s))
1143
- && !/[0-9]/.test(color);
1144
- }
1145
-
1146
- /**
1147
- * Check to see if it looks like a CSS unit
1148
- * (see `matchers` above for definition).
1149
- * @param {string | number} color testing value
1150
- * @returns {boolean} the query result
849
+ * Check to see if it looks like a CSS unit
850
+ * (see `matchers` above for definition).
851
+ * @param {string | number} color testing value
852
+ * @returns {boolean} the query result
1151
853
  */
1152
854
  function isValidCSSUnit(color) {
1153
855
  return Boolean(matchers.CSS_UNIT.exec(String(color)));
@@ -1161,15 +863,15 @@ function isValidCSSUnit(color) {
1161
863
  */
1162
864
  function bound01(N, max) {
1163
865
  let n = N;
1164
- if (isOnePointZero(n)) n = '100%';
1165
-
1166
- n = max === 360 ? n : Math.min(max, Math.max(0, parseFloat(n)));
866
+ if (isOnePointZero(N)) n = '100%';
1167
867
 
1168
- // Handle hue angles
1169
- if (isAngle(N)) n = N.replace(new RegExp(ANGLES), '');
868
+ const processPercent = isPercentage(n);
869
+ n = max === 360
870
+ ? parseFloat(n)
871
+ : Math.min(max, Math.max(0, parseFloat(n)));
1170
872
 
1171
873
  // Automatically convert percentage into number
1172
- if (isPercentage(n)) n = parseInt(String(n * max), 10) / 100;
874
+ if (processPercent) n = (n * max) / 100;
1173
875
 
1174
876
  // Handle floating point rounding errors
1175
877
  if (Math.abs(n - max) < 0.000001) {
@@ -1180,11 +882,11 @@ function bound01(N, max) {
1180
882
  // If n is a hue given in degrees,
1181
883
  // wrap around out-of-range values into [0, 360] range
1182
884
  // then convert into [0, 1].
1183
- n = (n < 0 ? (n % max) + max : n % max) / parseFloat(String(max));
885
+ n = (n < 0 ? (n % max) + max : n % max) / max;
1184
886
  } else {
1185
887
  // If n not a hue given in degrees
1186
888
  // Convert into [0, 1] range if it isn't already.
1187
- n = (n % max) / parseFloat(String(max));
889
+ n = (n % max) / max;
1188
890
  }
1189
891
  return n;
1190
892
  }
@@ -1219,7 +921,6 @@ function clamp01(v) {
1219
921
  * @returns {string}
1220
922
  */
1221
923
  function getRGBFromName(name) {
1222
- const documentHead = getDocumentHead();
1223
924
  setElementStyle(documentHead, { color: name });
1224
925
  const colorName = getElementStyle(documentHead, 'color');
1225
926
  setElementStyle(documentHead, { color: '' });
@@ -1232,7 +933,7 @@ function getRGBFromName(name) {
1232
933
  * @returns {string} - the hexadecimal value
1233
934
  */
1234
935
  function convertDecimalToHex(d) {
1235
- return Math.round(d * 255).toString(16);
936
+ return roundPart(d * 255).toString(16);
1236
937
  }
1237
938
 
1238
939
  /**
@@ -1318,6 +1019,36 @@ function hueToRgb(p, q, t) {
1318
1019
  return p;
1319
1020
  }
1320
1021
 
1022
+ /**
1023
+ * Converts an HSL colour value to RGB.
1024
+ *
1025
+ * @param {number} h Hue Angle [0, 1]
1026
+ * @param {number} s Saturation [0, 1]
1027
+ * @param {number} l Lightness Angle [0, 1]
1028
+ * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1029
+ */
1030
+ function hslToRgb(h, s, l) {
1031
+ let r = 0;
1032
+ let g = 0;
1033
+ let b = 0;
1034
+
1035
+ if (s === 0) {
1036
+ // achromatic
1037
+ g = l;
1038
+ b = l;
1039
+ r = l;
1040
+ } else {
1041
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1042
+ const p = 2 * l - q;
1043
+ r = hueToRgb(p, q, h + 1 / 3);
1044
+ g = hueToRgb(p, q, h);
1045
+ b = hueToRgb(p, q, h - 1 / 3);
1046
+ }
1047
+ [r, g, b] = [r, g, b].map((x) => x * 255);
1048
+
1049
+ return { r, g, b };
1050
+ }
1051
+
1321
1052
  /**
1322
1053
  * Returns an HWB colour object from an RGB colour object.
1323
1054
  * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
@@ -1380,36 +1111,6 @@ function hwbToRgb(H, W, B) {
1380
1111
  return { r, g, b };
1381
1112
  }
1382
1113
 
1383
- /**
1384
- * Converts an HSL colour value to RGB.
1385
- *
1386
- * @param {number} h Hue Angle [0, 1]
1387
- * @param {number} s Saturation [0, 1]
1388
- * @param {number} l Lightness Angle [0, 1]
1389
- * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1390
- */
1391
- function hslToRgb(h, s, l) {
1392
- let r = 0;
1393
- let g = 0;
1394
- let b = 0;
1395
-
1396
- if (s === 0) {
1397
- // achromatic
1398
- g = l;
1399
- b = l;
1400
- r = l;
1401
- } else {
1402
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1403
- const p = 2 * l - q;
1404
- r = hueToRgb(p, q, h + 1 / 3);
1405
- g = hueToRgb(p, q, h);
1406
- b = hueToRgb(p, q, h - 1 / 3);
1407
- }
1408
- [r, g, b] = [r, g, b].map((x) => x * 255);
1409
-
1410
- return { r, g, b };
1411
- }
1412
-
1413
1114
  /**
1414
1115
  * Converts an RGB colour value to HSV.
1415
1116
  *
@@ -1465,10 +1166,11 @@ function hsvToRgb(H, S, V) {
1465
1166
  const q = v * (1 - f * s);
1466
1167
  const t = v * (1 - (1 - f) * s);
1467
1168
  const mod = i % 6;
1468
- const r = [v, q, p, p, t, v][mod];
1469
- const g = [t, v, v, q, p, p][mod];
1470
- const b = [p, p, t, v, v, q][mod];
1471
- return { r: r * 255, g: g * 255, b: b * 255 };
1169
+ let r = [v, q, p, p, t, v][mod];
1170
+ let g = [t, v, v, q, p, p][mod];
1171
+ let b = [p, p, t, v, v, q][mod];
1172
+ [r, g, b] = [r, g, b].map((n) => n * 255);
1173
+ return { r, g, b };
1472
1174
  }
1473
1175
 
1474
1176
  /**
@@ -1484,15 +1186,15 @@ function hsvToRgb(H, S, V) {
1484
1186
  */
1485
1187
  function rgbToHex(r, g, b, allow3Char) {
1486
1188
  const hex = [
1487
- pad2(Math.round(r).toString(16)),
1488
- pad2(Math.round(g).toString(16)),
1489
- pad2(Math.round(b).toString(16)),
1189
+ pad2(roundPart(r).toString(16)),
1190
+ pad2(roundPart(g).toString(16)),
1191
+ pad2(roundPart(b).toString(16)),
1490
1192
  ];
1491
1193
 
1492
1194
  // Return a 3 character hex if possible
1493
1195
  if (allow3Char && hex[0].charAt(0) === hex[0].charAt(1)
1494
1196
  && hex[1].charAt(0) === hex[1].charAt(1)
1495
- && hex[2].charAt(0) === hex[2].charAt(1)) {
1197
+ && hex[2].charAt(0) === hex[2].charAt(1)) {
1496
1198
  return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
1497
1199
  }
1498
1200
 
@@ -1511,48 +1213,33 @@ function rgbToHex(r, g, b, allow3Char) {
1511
1213
  */
1512
1214
  function rgbaToHex(r, g, b, a, allow4Char) {
1513
1215
  const hex = [
1514
- pad2(Math.round(r).toString(16)),
1515
- pad2(Math.round(g).toString(16)),
1516
- pad2(Math.round(b).toString(16)),
1216
+ pad2(roundPart(r).toString(16)),
1217
+ pad2(roundPart(g).toString(16)),
1218
+ pad2(roundPart(b).toString(16)),
1517
1219
  pad2(convertDecimalToHex(a)),
1518
1220
  ];
1519
1221
 
1520
1222
  // Return a 4 character hex if possible
1521
1223
  if (allow4Char && hex[0].charAt(0) === hex[0].charAt(1)
1522
1224
  && hex[1].charAt(0) === hex[1].charAt(1)
1523
- && hex[2].charAt(0) === hex[2].charAt(1)
1524
- && hex[3].charAt(0) === hex[3].charAt(1)) {
1225
+ && hex[2].charAt(0) === hex[2].charAt(1)
1226
+ && hex[3].charAt(0) === hex[3].charAt(1)) {
1525
1227
  return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
1526
1228
  }
1527
1229
  return hex.join('');
1528
1230
  }
1529
1231
 
1530
- /**
1531
- * Returns a colour object corresponding to a given number.
1532
- * @param {number} color input number
1533
- * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1534
- */
1535
- function numberInputToObject(color) {
1536
- /* eslint-disable no-bitwise */
1537
- return {
1538
- r: color >> 16,
1539
- g: (color & 0xff00) >> 8,
1540
- b: color & 0xff,
1541
- };
1542
- /* eslint-enable no-bitwise */
1543
- }
1544
-
1545
1232
  /**
1546
1233
  * Permissive string parsing. Take in a number of formats, and output an object
1547
1234
  * based on detected format. Returns {r,g,b} or {h,s,l} or {h,s,v}
1548
1235
  * @param {string} input colour value in any format
1549
- * @returns {Record<string, (number | string)> | false} an object matching the RegExp
1236
+ * @returns {Record<string, (number | string | boolean)> | false} an object matching the RegExp
1550
1237
  */
1551
1238
  function stringInputToObject(input) {
1552
- let color = input.trim().toLowerCase();
1239
+ let color = toLowerCase(input.trim());
1553
1240
  if (color.length === 0) {
1554
1241
  return {
1555
- r: 0, g: 0, b: 0, a: 0,
1242
+ r: 0, g: 0, b: 0, a: 1,
1556
1243
  };
1557
1244
  }
1558
1245
  let named = false;
@@ -1560,11 +1247,9 @@ function stringInputToObject(input) {
1560
1247
  color = getRGBFromName(color);
1561
1248
  named = true;
1562
1249
  } else if (nonColors.includes(color)) {
1563
- const isTransparent = color === 'transparent';
1564
- const rgb = isTransparent ? 0 : 255;
1565
- const a = isTransparent ? 0 : 1;
1250
+ const a = color === 'transparent' ? 0 : 1;
1566
1251
  return {
1567
- r: rgb, g: rgb, b: rgb, a, format: 'rgb',
1252
+ r: 0, g: 0, b: 0, a, format: 'rgb', ok: true,
1568
1253
  };
1569
1254
  }
1570
1255
 
@@ -1604,7 +1289,6 @@ function stringInputToObject(input) {
1604
1289
  g: parseIntFromHex(m2),
1605
1290
  b: parseIntFromHex(m3),
1606
1291
  a: convertHexToDecimal(m4),
1607
- // format: named ? 'rgb' : 'hex8',
1608
1292
  format: named ? 'rgb' : 'hex',
1609
1293
  };
1610
1294
  }
@@ -1668,6 +1352,7 @@ function stringInputToObject(input) {
1668
1352
  function inputToRGB(input) {
1669
1353
  let rgb = { r: 0, g: 0, b: 0 };
1670
1354
  let color = input;
1355
+ /** @type {string | number} */
1671
1356
  let a = 1;
1672
1357
  let s = null;
1673
1358
  let v = null;
@@ -1675,8 +1360,11 @@ function inputToRGB(input) {
1675
1360
  let w = null;
1676
1361
  let b = null;
1677
1362
  let h = null;
1363
+ let r = null;
1364
+ let g = null;
1678
1365
  let ok = false;
1679
- let format = 'hex';
1366
+ const inputFormat = typeof color === 'object' && color.format;
1367
+ let format = inputFormat && COLOR_FORMAT.includes(inputFormat) ? inputFormat : 'rgb';
1680
1368
 
1681
1369
  if (typeof input === 'string') {
1682
1370
  // @ts-ignore -- this now is converted to object
@@ -1685,7 +1373,10 @@ function inputToRGB(input) {
1685
1373
  }
1686
1374
  if (typeof color === 'object') {
1687
1375
  if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
1688
- rgb = { r: color.r, g: color.g, b: color.b }; // RGB values in [0, 255] range
1376
+ ({ r, g, b } = color);
1377
+ // RGB values now are all in [0, 255] range
1378
+ [r, g, b] = [r, g, b].map((n) => bound01(n, isPercentage(n) ? 100 : 255) * 255);
1379
+ rgb = { r, g, b };
1689
1380
  ok = true;
1690
1381
  format = 'rgb';
1691
1382
  } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
@@ -1714,14 +1405,17 @@ function inputToRGB(input) {
1714
1405
  format = 'hwb';
1715
1406
  }
1716
1407
  if (isValidCSSUnit(color.a)) {
1717
- a = color.a;
1718
- a = isPercentage(`${a}`) ? bound01(a, 100) : a;
1408
+ a = color.a; // @ts-ignore -- `parseFloat` works with numbers too
1409
+ a = isPercentage(`${a}`) || parseFloat(a) > 1 ? bound01(a, 100) : a;
1719
1410
  }
1720
1411
  }
1412
+ if (typeof color === 'undefined') {
1413
+ ok = true;
1414
+ }
1721
1415
 
1722
1416
  return {
1723
- ok, // @ts-ignore
1724
- format: color.format || format,
1417
+ ok,
1418
+ format,
1725
1419
  r: Math.min(255, Math.max(rgb.r, 0)),
1726
1420
  g: Math.min(255, Math.max(rgb.g, 0)),
1727
1421
  b: Math.min(255, Math.max(rgb.b, 0)),
@@ -1750,7 +1444,8 @@ class Color {
1750
1444
  color = inputToRGB(color);
1751
1445
  }
1752
1446
  if (typeof color === 'number') {
1753
- color = numberInputToObject(color);
1447
+ const len = `${color}`.length;
1448
+ color = `#${(len === 2 ? '0' : '00')}${color}`;
1754
1449
  }
1755
1450
  const {
1756
1451
  r, g, b, a, ok, format,
@@ -1760,7 +1455,7 @@ class Color {
1760
1455
  const self = this;
1761
1456
 
1762
1457
  /** @type {CP.ColorInput} */
1763
- self.originalInput = color;
1458
+ self.originalInput = input;
1764
1459
  /** @type {number} */
1765
1460
  self.r = r;
1766
1461
  /** @type {number} */
@@ -1773,14 +1468,6 @@ class Color {
1773
1468
  self.ok = ok;
1774
1469
  /** @type {CP.ColorFormats} */
1775
1470
  self.format = configFormat || format;
1776
-
1777
- // Don't let the range of [0,255] come back in [0,1].
1778
- // Potentially lose a little bit of precision here, but will fix issues where
1779
- // .5 gets interpreted as half of the total, instead of half of 1
1780
- // If it was supposed to be 128, this was already taken care of by `inputToRgb`
1781
- if (r < 1) self.r = Math.round(r);
1782
- if (g < 1) self.g = Math.round(g);
1783
- if (b < 1) self.b = Math.round(b);
1784
1471
  }
1785
1472
 
1786
1473
  /**
@@ -1796,7 +1483,7 @@ class Color {
1796
1483
  * @returns {boolean} the query result
1797
1484
  */
1798
1485
  get isDark() {
1799
- return this.brightness < 128;
1486
+ return this.brightness < 120;
1800
1487
  }
1801
1488
 
1802
1489
  /**
@@ -1848,13 +1535,9 @@ class Color {
1848
1535
  const {
1849
1536
  r, g, b, a,
1850
1537
  } = this;
1851
- const [R, G, B] = [r, g, b].map((x) => Math.round(x));
1852
1538
 
1853
1539
  return {
1854
- r: R,
1855
- g: G,
1856
- b: B,
1857
- a: Math.round(a * 100) / 100,
1540
+ r, g, b, a: roundPart(a * 100) / 100,
1858
1541
  };
1859
1542
  }
1860
1543
 
@@ -1868,10 +1551,11 @@ class Color {
1868
1551
  const {
1869
1552
  r, g, b, a,
1870
1553
  } = this.toRgb();
1554
+ const [R, G, B] = [r, g, b].map(roundPart);
1871
1555
 
1872
1556
  return a === 1
1873
- ? `rgb(${r}, ${g}, ${b})`
1874
- : `rgba(${r}, ${g}, ${b}, ${a})`;
1557
+ ? `rgb(${R}, ${G}, ${B})`
1558
+ : `rgba(${R}, ${G}, ${B}, ${a})`;
1875
1559
  }
1876
1560
 
1877
1561
  /**
@@ -1884,9 +1568,10 @@ class Color {
1884
1568
  const {
1885
1569
  r, g, b, a,
1886
1570
  } = this.toRgb();
1887
- const A = a === 1 ? '' : ` / ${Math.round(a * 100)}%`;
1571
+ const [R, G, B] = [r, g, b].map(roundPart);
1572
+ const A = a === 1 ? '' : ` / ${roundPart(a * 100)}%`;
1888
1573
 
1889
- return `rgb(${r} ${g} ${b}${A})`;
1574
+ return `rgb(${R} ${G} ${B}${A})`;
1890
1575
  }
1891
1576
 
1892
1577
  /**
@@ -1979,10 +1664,10 @@ class Color {
1979
1664
  let {
1980
1665
  h, s, l, a,
1981
1666
  } = this.toHsl();
1982
- h = Math.round(h * 360);
1983
- s = Math.round(s * 100);
1984
- l = Math.round(l * 100);
1985
- a = Math.round(a * 100) / 100;
1667
+ h = roundPart(h * 360);
1668
+ s = roundPart(s * 100);
1669
+ l = roundPart(l * 100);
1670
+ a = roundPart(a * 100) / 100;
1986
1671
 
1987
1672
  return a === 1
1988
1673
  ? `hsl(${h}, ${s}%, ${l}%)`
@@ -1999,11 +1684,11 @@ class Color {
1999
1684
  let {
2000
1685
  h, s, l, a,
2001
1686
  } = this.toHsl();
2002
- h = Math.round(h * 360);
2003
- s = Math.round(s * 100);
2004
- l = Math.round(l * 100);
2005
- a = Math.round(a * 100);
2006
- const A = a < 100 ? ` / ${Math.round(a)}%` : '';
1687
+ h = roundPart(h * 360);
1688
+ s = roundPart(s * 100);
1689
+ l = roundPart(l * 100);
1690
+ a = roundPart(a * 100);
1691
+ const A = a < 100 ? ` / ${roundPart(a)}%` : '';
2007
1692
 
2008
1693
  return `hsl(${h}deg ${s}% ${l}%${A})`;
2009
1694
  }
@@ -2030,11 +1715,11 @@ class Color {
2030
1715
  let {
2031
1716
  h, w, b, a,
2032
1717
  } = this.toHwb();
2033
- h = Math.round(h * 360);
2034
- w = Math.round(w * 100);
2035
- b = Math.round(b * 100);
2036
- a = Math.round(a * 100);
2037
- const A = a < 100 ? ` / ${Math.round(a)}%` : '';
1718
+ h = roundPart(h * 360);
1719
+ w = roundPart(w * 100);
1720
+ b = roundPart(b * 100);
1721
+ a = roundPart(a * 100);
1722
+ const A = a < 100 ? ` / ${roundPart(a)}%` : '';
2038
1723
 
2039
1724
  return `hwb(${h}deg ${w}% ${b}%${A})`;
2040
1725
  }
@@ -2140,108 +1825,426 @@ class Color {
2140
1825
  const self = this;
2141
1826
  const { format } = self;
2142
1827
 
2143
- if (format === 'hex') return self.toHexString(allowShort);
2144
- if (format === 'hsl') return self.toHslString();
2145
- if (format === 'hwb') return self.toHwbString();
1828
+ if (format === 'hex') return self.toHexString(allowShort);
1829
+ if (format === 'hsl') return self.toHslString();
1830
+ if (format === 'hwb') return self.toHwbString();
1831
+
1832
+ return self.toRgbString();
1833
+ }
1834
+ }
1835
+
1836
+ ObjectAssign(Color, {
1837
+ ANGLES,
1838
+ CSS_ANGLE,
1839
+ CSS_INTEGER,
1840
+ CSS_NUMBER,
1841
+ CSS_UNIT,
1842
+ CSS_UNIT2,
1843
+ PERMISSIVE_MATCH,
1844
+ matchers,
1845
+ isOnePointZero,
1846
+ isPercentage,
1847
+ isValidCSSUnit,
1848
+ isColorName,
1849
+ pad2,
1850
+ clamp01,
1851
+ bound01,
1852
+ boundAlpha,
1853
+ getRGBFromName,
1854
+ convertHexToDecimal,
1855
+ convertDecimalToHex,
1856
+ rgbToHsl,
1857
+ rgbToHex,
1858
+ rgbToHsv,
1859
+ rgbToHwb,
1860
+ rgbaToHex,
1861
+ hslToRgb,
1862
+ hsvToRgb,
1863
+ hueToRgb,
1864
+ hwbToRgb,
1865
+ parseIntFromHex,
1866
+ stringInputToObject,
1867
+ inputToRGB,
1868
+ roundPart,
1869
+ getElementStyle,
1870
+ setElementStyle,
1871
+ ObjectAssign,
1872
+ });
1873
+
1874
+ /**
1875
+ * @class
1876
+ * Returns a color palette with a given set of parameters.
1877
+ * @example
1878
+ * new ColorPalette(0, 12, 10);
1879
+ * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: Array<Color> }
1880
+ */
1881
+ class ColorPalette {
1882
+ /**
1883
+ * The `hue` parameter is optional, which would be set to 0.
1884
+ * @param {number[]} args represeinting hue, hueSteps, lightSteps
1885
+ * * `args.hue` the starting Hue [0, 360]
1886
+ * * `args.hueSteps` Hue Steps Count [5, 24]
1887
+ * * `args.lightSteps` Lightness Steps Count [5, 12]
1888
+ */
1889
+ constructor(...args) {
1890
+ let hue = 0;
1891
+ let hueSteps = 12;
1892
+ let lightSteps = 10;
1893
+ let lightnessArray = [0.5];
1894
+
1895
+ if (args.length === 3) {
1896
+ [hue, hueSteps, lightSteps] = args;
1897
+ } else if (args.length === 2) {
1898
+ [hueSteps, lightSteps] = args;
1899
+ if ([hueSteps, lightSteps].some((n) => n < 1)) {
1900
+ throw TypeError('ColorPalette: when 2 arguments used, both must be larger than 0.');
1901
+ }
1902
+ } else {
1903
+ throw TypeError('ColorPalette requires minimum 2 arguments');
1904
+ }
1905
+
1906
+ /** @type {Color[]} */
1907
+ const colors = [];
1908
+
1909
+ const hueStep = 360 / hueSteps;
1910
+ const half = roundPart((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
1911
+ const estimatedStep = 100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100;
1912
+
1913
+ let lightStep = 0.25;
1914
+ lightStep = [4, 5].includes(lightSteps) ? 0.2 : lightStep;
1915
+ lightStep = [6, 7].includes(lightSteps) ? 0.15 : lightStep;
1916
+ lightStep = [8, 9].includes(lightSteps) ? 0.11 : lightStep;
1917
+ lightStep = [10, 11].includes(lightSteps) ? 0.09 : lightStep;
1918
+ lightStep = [12, 13].includes(lightSteps) ? 0.075 : lightStep;
1919
+ lightStep = lightSteps > 13 ? estimatedStep : lightStep;
1920
+
1921
+ // light tints
1922
+ for (let i = 1; i < half + 1; i += 1) {
1923
+ lightnessArray = [...lightnessArray, (0.5 + lightStep * (i))];
1924
+ }
1925
+
1926
+ // dark tints
1927
+ for (let i = 1; i < lightSteps - half; i += 1) {
1928
+ lightnessArray = [(0.5 - lightStep * (i)), ...lightnessArray];
1929
+ }
1930
+
1931
+ // feed `colors` Array
1932
+ for (let i = 0; i < hueSteps; i += 1) {
1933
+ const currentHue = ((hue + i * hueStep) % 360) / 360;
1934
+ lightnessArray.forEach((l) => {
1935
+ colors.push(new Color({ h: currentHue, s: 1, l }));
1936
+ });
1937
+ }
1938
+
1939
+ this.hue = hue;
1940
+ this.hueSteps = hueSteps;
1941
+ this.lightSteps = lightSteps;
1942
+ this.colors = colors;
1943
+ }
1944
+ }
1945
+
1946
+ ObjectAssign(ColorPalette, { Color });
1947
+
1948
+ /** @type {Record<string, string>} */
1949
+ const colorPickerLabels = {
1950
+ pickerLabel: 'Colour Picker',
1951
+ appearanceLabel: 'Colour Appearance',
1952
+ valueLabel: 'Colour Value',
1953
+ toggleLabel: 'Select Colour',
1954
+ presetsLabel: 'Colour Presets',
1955
+ defaultsLabel: 'Colour Defaults',
1956
+ formatLabel: 'Format',
1957
+ alphaLabel: 'Alpha',
1958
+ hexLabel: 'Hexadecimal',
1959
+ hueLabel: 'Hue',
1960
+ whitenessLabel: 'Whiteness',
1961
+ blacknessLabel: 'Blackness',
1962
+ saturationLabel: 'Saturation',
1963
+ lightnessLabel: 'Lightness',
1964
+ redLabel: 'Red',
1965
+ greenLabel: 'Green',
1966
+ blueLabel: 'Blue',
1967
+ };
1968
+
1969
+ /**
1970
+ * A list of 17 color names used for WAI-ARIA compliance.
1971
+ * @type {string[]}
1972
+ */
1973
+ const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
1974
+
1975
+ const tabIndex = 'tabindex';
1976
+
1977
+ /**
1978
+ * Check if a string is valid JSON string.
1979
+ * @param {string} str the string input
1980
+ * @returns {boolean} the query result
1981
+ */
1982
+ function isValidJSON(str) {
1983
+ try {
1984
+ JSON.parse(str);
1985
+ } catch (e) {
1986
+ return false;
1987
+ }
1988
+ return true;
1989
+ }
1990
+
1991
+ /**
1992
+ * Shortcut for `String.toUpperCase()`.
1993
+ *
1994
+ * @param {string} source input string
1995
+ * @returns {string} uppercase output string
1996
+ */
1997
+ const toUpperCase = (source) => source.toUpperCase();
1998
+
1999
+ /**
2000
+ * A global namespace for aria-haspopup.
2001
+ * @type {string}
2002
+ */
2003
+ const ariaHasPopup = 'aria-haspopup';
2004
+
2005
+ /**
2006
+ * A global namespace for aria-hidden.
2007
+ * @type {string}
2008
+ */
2009
+ const ariaHidden = 'aria-hidden';
2010
+
2011
+ /**
2012
+ * A global namespace for aria-labelledby.
2013
+ * @type {string}
2014
+ */
2015
+ const ariaLabelledBy = 'aria-labelledby';
2016
+
2017
+ /**
2018
+ * This is a shortie for `document.createElement` method
2019
+ * which allows you to create a new `HTMLElement` for a given `tagName`
2020
+ * or based on an object with specific non-readonly attributes:
2021
+ * `id`, `className`, `textContent`, `style`, etc.
2022
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
2023
+ *
2024
+ * @param {Record<string, string> | string} param `tagName` or object
2025
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
2026
+ */
2027
+ function createElement(param) {
2028
+ if (typeof param === 'string') {
2029
+ return getDocument().createElement(param);
2030
+ }
2031
+
2032
+ const { tagName } = param;
2033
+ const attr = { ...param };
2034
+ const newElement = createElement(tagName);
2035
+ delete attr.tagName;
2036
+ ObjectAssign(newElement, attr);
2037
+ return newElement;
2038
+ }
2039
+
2040
+ /**
2041
+ * This is a shortie for `document.createElementNS` method
2042
+ * which allows you to create a new `HTMLElement` for a given `tagName`
2043
+ * or based on an object with specific non-readonly attributes:
2044
+ * `id`, `className`, `textContent`, `style`, etc.
2045
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
2046
+ *
2047
+ * @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
2048
+ * @param {Record<string, string> | string} param `tagName` or object
2049
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
2050
+ */
2051
+ function createElementNS(namespace, param) {
2052
+ if (typeof param === 'string') {
2053
+ return getDocument().createElementNS(namespace, param);
2054
+ }
2055
+
2056
+ const { tagName } = param;
2057
+ const attr = { ...param };
2058
+ const newElement = createElementNS(namespace, tagName);
2059
+ delete attr.tagName;
2060
+ ObjectAssign(newElement, attr);
2061
+ return newElement;
2062
+ }
2063
+
2064
+ const vHidden = 'v-hidden';
2065
+
2066
+ /**
2067
+ * Returns the color form for `ColorPicker`.
2068
+ *
2069
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
2070
+ * @returns {HTMLElement | Element} a new `<div>` element with color component `<input>`
2071
+ */
2072
+ function getColorForm(self) {
2073
+ const { format, id, componentLabels } = self;
2074
+ const colorForm = createElement({
2075
+ tagName: 'div',
2076
+ className: `color-form ${format}`,
2077
+ });
2078
+
2079
+ let components = ['hex'];
2080
+ if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
2081
+ else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
2082
+ else if (format === 'hwb') components = ['hue', 'whiteness', 'blackness', 'alpha'];
2083
+
2084
+ components.forEach((c) => {
2085
+ const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
2086
+ const cID = `color_${format}_${c}_${id}`;
2087
+ const formatLabel = componentLabels[`${c}Label`];
2088
+ const cInputLabel = createElement({ tagName: 'label' });
2089
+ setAttribute(cInputLabel, 'for', cID);
2090
+ cInputLabel.append(
2091
+ createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
2092
+ createElement({ tagName: 'span', className: vHidden, innerText: formatLabel }),
2093
+ );
2094
+ const cInput = createElement({
2095
+ tagName: 'input',
2096
+ id: cID,
2097
+ // name: cID, - prevent saving the value to a form
2098
+ type: format === 'hex' ? 'text' : 'number',
2099
+ value: c === 'alpha' ? '100' : '0',
2100
+ className: `color-input ${c}`,
2101
+ });
2102
+ setAttribute(cInput, 'autocomplete', 'off');
2103
+ setAttribute(cInput, 'spellcheck', 'false');
2104
+
2105
+ // alpha
2106
+ let max = '100';
2107
+ let step = '1';
2108
+ if (c !== 'alpha') {
2109
+ if (format === 'rgb') {
2110
+ max = '255'; step = '1';
2111
+ } else if (c === 'hue') {
2112
+ max = '360'; step = '1';
2113
+ }
2114
+ }
2115
+ ObjectAssign(cInput, {
2116
+ min: '0',
2117
+ max,
2118
+ step,
2119
+ });
2120
+ colorForm.append(cInputLabel, cInput);
2121
+ });
2122
+ return colorForm;
2123
+ }
2124
+
2125
+ /**
2126
+ * A global namespace for aria-label.
2127
+ * @type {string}
2128
+ */
2129
+ const ariaLabel = 'aria-label';
2130
+
2131
+ /**
2132
+ * A global namespace for aria-valuemin.
2133
+ * @type {string}
2134
+ */
2135
+ const ariaValueMin = 'aria-valuemin';
2136
+
2137
+ /**
2138
+ * A global namespace for aria-valuemax.
2139
+ * @type {string}
2140
+ */
2141
+ const ariaValueMax = 'aria-valuemax';
2142
+
2143
+ /**
2144
+ * Returns all color controls for `ColorPicker`.
2145
+ *
2146
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
2147
+ * @returns {HTMLElement | Element} color controls
2148
+ */
2149
+ function getColorControls(self) {
2150
+ const { format, componentLabels } = self;
2151
+ const {
2152
+ hueLabel, alphaLabel, lightnessLabel, saturationLabel,
2153
+ whitenessLabel, blacknessLabel,
2154
+ } = componentLabels;
2155
+
2156
+ const max1 = format === 'hsl' ? 360 : 100;
2157
+ const max2 = format === 'hsl' ? 100 : 360;
2158
+ const max3 = 100;
2159
+
2160
+ let ctrl1Label = format === 'hsl'
2161
+ ? `${hueLabel} & ${lightnessLabel}`
2162
+ : `${lightnessLabel} & ${saturationLabel}`;
2163
+
2164
+ ctrl1Label = format === 'hwb'
2165
+ ? `${whitenessLabel} & ${blacknessLabel}`
2166
+ : ctrl1Label;
2146
2167
 
2147
- return self.toRgbString();
2148
- }
2149
- }
2168
+ const ctrl2Label = format === 'hsl'
2169
+ ? `${saturationLabel}`
2170
+ : `${hueLabel}`;
2150
2171
 
2151
- ObjectAssign(Color, {
2152
- ANGLES,
2153
- CSS_ANGLE,
2154
- CSS_INTEGER,
2155
- CSS_NUMBER,
2156
- CSS_UNIT,
2157
- CSS_UNIT2,
2158
- PERMISSIVE_MATCH,
2159
- matchers,
2160
- isOnePointZero,
2161
- isPercentage,
2162
- isValidCSSUnit,
2163
- pad2,
2164
- clamp01,
2165
- bound01,
2166
- boundAlpha,
2167
- getRGBFromName,
2168
- convertHexToDecimal,
2169
- convertDecimalToHex,
2170
- rgbToHsl,
2171
- rgbToHex,
2172
- rgbToHsv,
2173
- rgbToHwb,
2174
- rgbaToHex,
2175
- hslToRgb,
2176
- hsvToRgb,
2177
- hueToRgb,
2178
- hwbToRgb,
2179
- parseIntFromHex,
2180
- numberInputToObject,
2181
- stringInputToObject,
2182
- inputToRGB,
2183
- ObjectAssign,
2184
- });
2172
+ const colorControls = createElement({
2173
+ tagName: 'div',
2174
+ className: `color-controls ${format}`,
2175
+ });
2185
2176
 
2186
- /**
2187
- * @class
2188
- * Returns a color palette with a given set of parameters.
2189
- * @example
2190
- * new ColorPalette(0, 12, 10);
2191
- * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: array }
2192
- */
2193
- class ColorPalette {
2194
- /**
2195
- * The `hue` parameter is optional, which would be set to 0.
2196
- * @param {number[]} args represeinting hue, hueSteps, lightSteps
2197
- * * `args.hue` the starting Hue [0, 360]
2198
- * * `args.hueSteps` Hue Steps Count [5, 13]
2199
- * * `args.lightSteps` Lightness Steps Count [8, 10]
2200
- */
2201
- constructor(...args) {
2202
- let hue = 0;
2203
- let hueSteps = 12;
2204
- let lightSteps = 10;
2205
- let lightnessArray = [0.5];
2177
+ const colorPointer = 'color-pointer';
2178
+ const colorSlider = 'color-slider';
2206
2179
 
2207
- if (args.length === 3) {
2208
- [hue, hueSteps, lightSteps] = args;
2209
- } else if (args.length === 2) {
2210
- [hueSteps, lightSteps] = args;
2211
- } else {
2212
- throw TypeError('The ColorPalette requires minimum 2 arguments');
2213
- }
2180
+ const controls = [
2181
+ {
2182
+ i: 1,
2183
+ c: colorPointer,
2184
+ l: ctrl1Label,
2185
+ min: 0,
2186
+ max: max1,
2187
+ },
2188
+ {
2189
+ i: 2,
2190
+ c: colorSlider,
2191
+ l: ctrl2Label,
2192
+ min: 0,
2193
+ max: max2,
2194
+ },
2195
+ {
2196
+ i: 3,
2197
+ c: colorSlider,
2198
+ l: alphaLabel,
2199
+ min: 0,
2200
+ max: max3,
2201
+ },
2202
+ ];
2214
2203
 
2215
- /** @type {string[]} */
2216
- const colors = [];
2204
+ controls.forEach((template) => {
2205
+ const {
2206
+ i, c, l, min, max,
2207
+ } = template;
2208
+ const control = createElement({
2209
+ tagName: 'div',
2210
+ className: 'color-control',
2211
+ });
2212
+ setAttribute(control, 'role', 'presentation');
2217
2213
 
2218
- const hueStep = 360 / hueSteps;
2219
- const lightStep = 100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100;
2220
- const half = Math.round((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
2214
+ control.append(
2215
+ createElement({
2216
+ tagName: 'div',
2217
+ className: `visual-control visual-control${i}`,
2218
+ }),
2219
+ );
2221
2220
 
2222
- // light tints
2223
- for (let i = 0; i < half; i += 1) {
2224
- lightnessArray = [...lightnessArray, (0.5 + lightStep * (i + 1))];
2225
- }
2221
+ const knob = createElement({
2222
+ tagName: 'div',
2223
+ className: `${c} knob`,
2224
+ ariaLive: 'polite',
2225
+ });
2226
2226
 
2227
- // dark tints
2228
- for (let i = 0; i < lightSteps - half - 1; i += 1) {
2229
- lightnessArray = [(0.5 - lightStep * (i + 1)), ...lightnessArray];
2230
- }
2227
+ setAttribute(knob, ariaLabel, l);
2228
+ setAttribute(knob, 'role', 'slider');
2229
+ setAttribute(knob, tabIndex, '0');
2230
+ setAttribute(knob, ariaValueMin, `${min}`);
2231
+ setAttribute(knob, ariaValueMax, `${max}`);
2232
+ control.append(knob);
2233
+ colorControls.append(control);
2234
+ });
2231
2235
 
2232
- // feed `colors` Array
2233
- for (let i = 0; i < hueSteps; i += 1) {
2234
- const currentHue = ((hue + i * hueStep) % 360) / 360;
2235
- lightnessArray.forEach((l) => {
2236
- colors.push(new Color({ h: currentHue, s: 1, l }).toHexString());
2237
- });
2238
- }
2236
+ return colorControls;
2237
+ }
2239
2238
 
2240
- this.hue = hue;
2241
- this.hueSteps = hueSteps;
2242
- this.lightSteps = lightSteps;
2243
- this.colors = colors;
2244
- }
2239
+ /**
2240
+ * Helps setting CSS variables to the color-menu.
2241
+ * @param {HTMLElement} element
2242
+ * @param {Record<string,any>} props
2243
+ */
2244
+ function setCSSProperties(element, props) {
2245
+ ObjectKeys(props).forEach((prop) => {
2246
+ element.style.setProperty(prop, props[prop]);
2247
+ });
2245
2248
  }
2246
2249
 
2247
2250
  /**
@@ -2261,68 +2264,64 @@ function getColorMenu(self, colorsSource, menuClass) {
2261
2264
  colorsArray = colorsArray instanceof Array ? colorsArray : [];
2262
2265
  const colorsCount = colorsArray.length;
2263
2266
  const { lightSteps } = isPalette ? colorsSource : { lightSteps: null };
2264
- let fit = lightSteps
2265
- || Math.max(...[5, 6, 7, 8, 9, 10].filter((x) => colorsCount > (x * 2) && !(colorsCount % x)));
2266
- fit = Number.isFinite(fit) ? fit : 5;
2267
+ const fit = lightSteps || [9, 10].find((x) => colorsCount > x * 2 && !(colorsCount % x)) || 5;
2267
2268
  const isMultiLine = isOptionsMenu && colorsCount > fit;
2268
- let rowCountHover = 1;
2269
- rowCountHover = isMultiLine && colorsCount < 27 ? 2 : rowCountHover;
2270
- rowCountHover = colorsCount >= 27 ? 3 : rowCountHover;
2271
- rowCountHover = colorsCount >= 36 ? 4 : rowCountHover;
2272
- rowCountHover = colorsCount >= 45 ? 5 : rowCountHover;
2273
- const rowCount = rowCountHover - (colorsCount < 27 ? 1 : 2);
2274
- const isScrollable = isMultiLine && colorsCount > rowCountHover * fit;
2269
+ let rowCountHover = 2;
2270
+ rowCountHover = isMultiLine && colorsCount >= fit * 2 ? 3 : rowCountHover;
2271
+ rowCountHover = colorsCount >= fit * 3 ? 4 : rowCountHover;
2272
+ rowCountHover = colorsCount >= fit * 4 ? 5 : rowCountHover;
2273
+ const rowCount = rowCountHover - (colorsCount < fit * 3 ? 1 : 2);
2274
+ const isScrollable = isMultiLine && colorsCount > rowCount * fit;
2275
2275
  let finalClass = menuClass;
2276
2276
  finalClass += isScrollable ? ' scrollable' : '';
2277
2277
  finalClass += isMultiLine ? ' multiline' : '';
2278
2278
  const gap = isMultiLine ? '1px' : '0.25rem';
2279
2279
  let optionSize = isMultiLine ? 1.75 : 2;
2280
- optionSize = !(colorsCount % 10) && isMultiLine ? 1.5 : optionSize;
2280
+ optionSize = fit > 5 && isMultiLine ? 1.5 : optionSize;
2281
2281
  const menuHeight = `${(rowCount || 1) * optionSize}rem`;
2282
2282
  const menuHeightHover = `calc(${rowCountHover} * ${optionSize}rem + ${rowCountHover - 1} * ${gap})`;
2283
- const gridTemplateColumns = `repeat(${fit}, ${optionSize}rem)`;
2284
- const gridTemplateRows = `repeat(auto-fill, ${optionSize}rem)`;
2285
-
2283
+ /** @type {HTMLUListElement} */
2284
+ // @ts-ignore -- <UL> is an `HTMLElement`
2286
2285
  const menu = createElement({
2287
2286
  tagName: 'ul',
2288
2287
  className: finalClass,
2289
2288
  });
2290
2289
  setAttribute(menu, 'role', 'listbox');
2291
- setAttribute(menu, ariaLabel, `${menuLabel}`);
2292
-
2293
- if (isOptionsMenu) {
2294
- if (isScrollable) {
2295
- const styleText = 'this.style.height=';
2296
- setAttribute(menu, 'onmouseout', `${styleText}'${menuHeight}'`);
2297
- setAttribute(menu, 'onmouseover', `${styleText}'${menuHeightHover}'`);
2298
- }
2299
- const menuStyle = {
2300
- height: isScrollable ? menuHeight : '', gridTemplateColumns, gridTemplateRows, gap,
2301
- };
2302
- setElementStyle(menu, menuStyle);
2290
+ setAttribute(menu, ariaLabel, menuLabel);
2291
+
2292
+ if (isScrollable) {
2293
+ setCSSProperties(menu, {
2294
+ '--grid-item-size': `${optionSize}rem`,
2295
+ '--grid-fit': fit,
2296
+ '--grid-gap': gap,
2297
+ '--grid-height': menuHeight,
2298
+ '--grid-hover-height': menuHeightHover,
2299
+ });
2303
2300
  }
2304
2301
 
2305
2302
  colorsArray.forEach((x) => {
2306
- const [value, label] = x.trim().split(':');
2307
- const xRealColor = new Color(value, format).toString();
2308
- const isActive = xRealColor === getAttribute(input, 'value');
2303
+ let [value, label] = typeof x === 'string' ? x.trim().split(':') : [];
2304
+ if (x instanceof Color) {
2305
+ value = x.toHexString();
2306
+ label = value;
2307
+ }
2308
+ const color = new Color(x instanceof Color ? x : value, format);
2309
+ const isActive = color.toString() === getAttribute(input, 'value');
2309
2310
  const active = isActive ? ' active' : '';
2310
2311
 
2311
2312
  const option = createElement({
2312
2313
  tagName: 'li',
2313
2314
  className: `color-option${active}`,
2314
- innerText: `${label || x}`,
2315
+ innerText: `${label || value}`,
2315
2316
  });
2316
2317
 
2317
- setAttribute(option, 'tabindex', '0');
2318
+ setAttribute(option, tabIndex, '0');
2318
2319
  setAttribute(option, 'data-value', `${value}`);
2319
2320
  setAttribute(option, 'role', 'option');
2320
2321
  setAttribute(option, ariaSelected, isActive ? 'true' : 'false');
2321
2322
 
2322
2323
  if (isOptionsMenu) {
2323
- setElementStyle(option, {
2324
- width: `${optionSize}rem`, height: `${optionSize}rem`, backgroundColor: x,
2325
- });
2324
+ setElementStyle(option, { backgroundColor: value });
2326
2325
  }
2327
2326
 
2328
2327
  menu.append(option);
@@ -2331,55 +2330,10 @@ function getColorMenu(self, colorsSource, menuClass) {
2331
2330
  }
2332
2331
 
2333
2332
  /**
2334
- * Check if a string is valid JSON string.
2335
- * @param {string} str the string input
2336
- * @returns {boolean} the query result
2337
- */
2338
- function isValidJSON(str) {
2339
- try {
2340
- JSON.parse(str);
2341
- } catch (e) {
2342
- return false;
2343
- }
2344
- return true;
2345
- }
2346
-
2347
- var version = "0.0.1alpha2";
2348
-
2349
- // @ts-ignore
2350
-
2351
- const Version = version;
2352
-
2353
- // ColorPicker GC
2354
- // ==============
2355
- const colorPickerString = 'color-picker';
2356
- const colorPickerSelector = `[data-function="${colorPickerString}"]`;
2357
- const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2358
- const colorPickerDefaults = {
2359
- componentLabels: colorPickerLabels,
2360
- colorLabels: colorNames,
2361
- format: 'rgb',
2362
- colorPresets: undefined,
2363
- colorKeywords: nonColors,
2364
- };
2365
-
2366
- // ColorPicker Static Methods
2367
- // ==========================
2368
-
2369
- /** @type {CP.GetInstance<ColorPicker>} */
2370
- const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
2371
-
2372
- /** @type {CP.InitCallback<ColorPicker>} */
2373
- const initColorPicker = (element) => new ColorPicker(element);
2374
-
2375
- // ColorPicker Private Methods
2376
- // ===========================
2377
-
2378
- /**
2379
- * Generate HTML markup and update instance properties.
2380
- * @param {ColorPicker} self
2381
- */
2382
- function initCallback(self) {
2333
+ * Generate HTML markup and update instance properties.
2334
+ * @param {CP.ColorPicker} self
2335
+ */
2336
+ function setMarkup(self) {
2383
2337
  const {
2384
2338
  input, parent, format, id, componentLabels, colorKeywords, colorPresets,
2385
2339
  } = self;
@@ -2394,9 +2348,7 @@ function initCallback(self) {
2394
2348
  self.color = new Color(color, format);
2395
2349
 
2396
2350
  // set initial controls dimensions
2397
- // make the controls smaller on mobile
2398
- const dropClass = isMobile ? ' mobile' : '';
2399
- const formatString = format === 'hex' ? hexLabel : format.toUpperCase();
2351
+ const formatString = format === 'hex' ? hexLabel : toUpperCase(format);
2400
2352
 
2401
2353
  const pickerBtn = createElement({
2402
2354
  id: `picker-btn-${id}`,
@@ -2413,7 +2365,7 @@ function initCallback(self) {
2413
2365
 
2414
2366
  const pickerDropdown = createElement({
2415
2367
  tagName: 'div',
2416
- className: `color-dropdown picker${dropClass}`,
2368
+ className: 'color-dropdown picker',
2417
2369
  });
2418
2370
  setAttribute(pickerDropdown, ariaLabelledBy, `picker-btn-${id}`);
2419
2371
  setAttribute(pickerDropdown, 'role', 'group');
@@ -2429,7 +2381,7 @@ function initCallback(self) {
2429
2381
  if (colorKeywords || colorPresets) {
2430
2382
  const presetsDropdown = createElement({
2431
2383
  tagName: 'div',
2432
- className: `color-dropdown scrollable menu${dropClass}`,
2384
+ className: 'color-dropdown scrollable menu',
2433
2385
  });
2434
2386
 
2435
2387
  // color presets
@@ -2449,7 +2401,7 @@ function initCallback(self) {
2449
2401
  tagName: 'button',
2450
2402
  className: 'menu-toggle btn-appearance',
2451
2403
  });
2452
- setAttribute(presetsBtn, 'tabindex', '-1');
2404
+ setAttribute(presetsBtn, tabIndex, '-1');
2453
2405
  setAttribute(presetsBtn, ariaExpanded, 'false');
2454
2406
  setAttribute(presetsBtn, ariaHasPopup, 'true');
2455
2407
 
@@ -2476,9 +2428,40 @@ function initCallback(self) {
2476
2428
  if (colorKeywords && nonColors.includes(colorValue)) {
2477
2429
  self.value = colorValue;
2478
2430
  }
2479
- setAttribute(input, 'tabindex', '-1');
2431
+ setAttribute(input, tabIndex, '-1');
2480
2432
  }
2481
2433
 
2434
+ var version = "0.0.2alpha1";
2435
+
2436
+ // @ts-ignore
2437
+
2438
+ const Version = version;
2439
+
2440
+ // ColorPicker GC
2441
+ // ==============
2442
+ const colorPickerString = 'color-picker';
2443
+ const colorPickerSelector = `[data-function="${colorPickerString}"]`;
2444
+ const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2445
+ const colorPickerDefaults = {
2446
+ componentLabels: colorPickerLabels,
2447
+ colorLabels: colorNames,
2448
+ format: 'rgb',
2449
+ colorPresets: false,
2450
+ colorKeywords: false,
2451
+ };
2452
+
2453
+ // ColorPicker Static Methods
2454
+ // ==========================
2455
+
2456
+ /** @type {CP.GetInstance<ColorPicker>} */
2457
+ const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
2458
+
2459
+ /** @type {CP.InitCallback<ColorPicker>} */
2460
+ const initColorPicker = (element) => new ColorPicker(element);
2461
+
2462
+ // ColorPicker Private Methods
2463
+ // ===========================
2464
+
2482
2465
  /**
2483
2466
  * Add / remove `ColorPicker` main event listeners.
2484
2467
  * @param {ColorPicker} self
@@ -2577,8 +2560,19 @@ function showDropdown(self, dropdown) {
2577
2560
  addClass(dropdown, 'bottom');
2578
2561
  reflow(dropdown);
2579
2562
  addClass(dropdown, 'show');
2563
+
2580
2564
  if (isPicker) self.update();
2581
- self.show();
2565
+
2566
+ if (!self.isOpen) {
2567
+ toggleEventsOnShown(self, true);
2568
+ self.updateDropdownPosition();
2569
+ self.isOpen = true;
2570
+ setAttribute(self.input, tabIndex, '0');
2571
+ if (menuToggle) {
2572
+ setAttribute(menuToggle, tabIndex, '0');
2573
+ }
2574
+ }
2575
+
2582
2576
  setAttribute(nextBtn, ariaExpanded, 'true');
2583
2577
  if (activeBtn) {
2584
2578
  setAttribute(activeBtn, ariaExpanded, 'false');
@@ -2701,7 +2695,7 @@ class ColorPicker {
2701
2695
  self.handleKnobs = self.handleKnobs.bind(self);
2702
2696
 
2703
2697
  // generate markup
2704
- initCallback(self);
2698
+ setMarkup(self);
2705
2699
 
2706
2700
  const [colorPicker, colorMenu] = getElementsByClassName('color-dropdown', parent);
2707
2701
  // set main elements
@@ -2748,7 +2742,7 @@ class ColorPicker {
2748
2742
  set value(v) { this.input.value = v; }
2749
2743
 
2750
2744
  /** Check if the colour presets include any non-colour. */
2751
- get includeNonColor() {
2745
+ get hasNonColor() {
2752
2746
  return this.colorKeywords instanceof Array
2753
2747
  && this.colorKeywords.some((x) => nonColors.includes(x));
2754
2748
  }
@@ -2804,7 +2798,7 @@ class ColorPicker {
2804
2798
  const { r, g, b } = Color.hslToRgb(hue, 1, 0.5);
2805
2799
  const whiteGrad = 'linear-gradient(rgb(255,255,255) 0%, rgb(255,255,255) 100%)';
2806
2800
  const alpha = 1 - controlPositions.c3y / offsetHeight;
2807
- const roundA = Math.round((alpha * 100)) / 100;
2801
+ const roundA = roundPart((alpha * 100)) / 100;
2808
2802
 
2809
2803
  if (format !== 'hsl') {
2810
2804
  const fill = new Color({
@@ -2822,7 +2816,7 @@ class ColorPicker {
2822
2816
  });
2823
2817
  setElementStyle(v2, { background: hueGradient });
2824
2818
  } else {
2825
- const saturation = Math.round((controlPositions.c2y / offsetHeight) * 100);
2819
+ const saturation = roundPart((controlPositions.c2y / offsetHeight) * 100);
2826
2820
  const fill0 = new Color({
2827
2821
  r: 255, g: 0, b: 0, a: alpha,
2828
2822
  }).saturate(-saturation).toRgbString();
@@ -2897,7 +2891,7 @@ class ColorPicker {
2897
2891
  const self = this;
2898
2892
  const { activeElement } = getDocument(self.input);
2899
2893
 
2900
- if ((isMobile && self.dragElement)
2894
+ if ((e.type === touchmoveEvent && self.dragElement)
2901
2895
  || (activeElement && self.controlKnobs.includes(activeElement))) {
2902
2896
  e.stopPropagation();
2903
2897
  e.preventDefault();
@@ -2977,12 +2971,12 @@ class ColorPicker {
2977
2971
 
2978
2972
  self.update();
2979
2973
 
2980
- if (currentActive) {
2981
- removeClass(currentActive, 'active');
2982
- removeAttribute(currentActive, ariaSelected);
2983
- }
2984
-
2985
2974
  if (currentActive !== target) {
2975
+ if (currentActive) {
2976
+ removeClass(currentActive, 'active');
2977
+ removeAttribute(currentActive, ariaSelected);
2978
+ }
2979
+
2986
2980
  addClass(target, 'active');
2987
2981
  setAttribute(target, ariaSelected, 'true');
2988
2982
 
@@ -3104,30 +3098,41 @@ class ColorPicker {
3104
3098
  if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
3105
3099
  e.preventDefault();
3106
3100
 
3107
- const { controlKnobs } = self;
3101
+ const { format, controlKnobs, visuals } = self;
3102
+ const { offsetWidth, offsetHeight } = visuals[0];
3108
3103
  const [c1, c2, c3] = controlKnobs;
3109
3104
  const { activeElement } = getDocument(c1);
3110
3105
  const currentKnob = controlKnobs.find((x) => x === activeElement);
3106
+ const yRatio = offsetHeight / (format === 'hsl' ? 100 : 360);
3111
3107
 
3112
3108
  if (currentKnob) {
3113
3109
  let offsetX = 0;
3114
3110
  let offsetY = 0;
3111
+
3115
3112
  if (target === c1) {
3113
+ const xRatio = offsetWidth / (format === 'hsl' ? 360 : 100);
3114
+
3116
3115
  if ([keyArrowLeft, keyArrowRight].includes(code)) {
3117
- self.controlPositions.c1x += code === keyArrowRight ? +1 : -1;
3116
+ self.controlPositions.c1x += code === keyArrowRight ? xRatio : -xRatio;
3118
3117
  } else if ([keyArrowUp, keyArrowDown].includes(code)) {
3119
- self.controlPositions.c1y += code === keyArrowDown ? +1 : -1;
3118
+ self.controlPositions.c1y += code === keyArrowDown ? yRatio : -yRatio;
3120
3119
  }
3121
3120
 
3122
3121
  offsetX = self.controlPositions.c1x;
3123
3122
  offsetY = self.controlPositions.c1y;
3124
3123
  self.changeControl1(offsetX, offsetY);
3125
3124
  } else if (target === c2) {
3126
- self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3125
+ self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code)
3126
+ ? yRatio
3127
+ : -yRatio;
3128
+
3127
3129
  offsetY = self.controlPositions.c2y;
3128
3130
  self.changeControl2(offsetY);
3129
3131
  } else if (target === c3) {
3130
- self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3132
+ self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code)
3133
+ ? yRatio
3134
+ : -yRatio;
3135
+
3131
3136
  offsetY = self.controlPositions.c3y;
3132
3137
  self.changeAlpha(offsetY);
3133
3138
  }
@@ -3149,7 +3154,7 @@ class ColorPicker {
3149
3154
  const [v1, v2, v3, v4] = format === 'rgb'
3150
3155
  ? inputs.map((i) => parseFloat(i.value) / (i === i4 ? 100 : 1))
3151
3156
  : inputs.map((i) => parseFloat(i.value) / (i !== i1 ? 100 : 360));
3152
- const isNonColorValue = self.includeNonColor && nonColors.includes(currentValue);
3157
+ const isNonColorValue = self.hasNonColor && nonColors.includes(currentValue);
3153
3158
  const alpha = i4 ? v4 : (1 - controlPositions.c3y / offsetHeight);
3154
3159
 
3155
3160
  if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
@@ -3408,11 +3413,11 @@ class ColorPicker {
3408
3413
  } = componentLabels;
3409
3414
  const { r, g, b } = color.toRgb();
3410
3415
  const [knob1, knob2, knob3] = controlKnobs;
3411
- const hue = Math.round(hsl.h * 360);
3416
+ const hue = roundPart(hsl.h * 360);
3412
3417
  const alpha = color.a;
3413
3418
  const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
3414
- const saturation = Math.round(saturationSource * 100);
3415
- const lightness = Math.round(hsl.l * 100);
3419
+ const saturation = roundPart(saturationSource * 100);
3420
+ const lightness = roundPart(hsl.l * 100);
3416
3421
  const hsvl = hsv.v * 100;
3417
3422
  let colorName;
3418
3423
 
@@ -3459,8 +3464,8 @@ class ColorPicker {
3459
3464
  setAttribute(knob2, ariaValueNow, `${saturation}`);
3460
3465
  } else if (format === 'hwb') {
3461
3466
  const { hwb } = self;
3462
- const whiteness = Math.round(hwb.w * 100);
3463
- const blackness = Math.round(hwb.b * 100);
3467
+ const whiteness = roundPart(hwb.w * 100);
3468
+ const blackness = roundPart(hwb.b * 100);
3464
3469
  colorLabel = `HWB: ${hue}°, ${whiteness}%, ${blackness}%`;
3465
3470
  setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3466
3471
  setAttribute(knob1, ariaValueText, `${whiteness}% & ${blackness}%`);
@@ -3476,7 +3481,7 @@ class ColorPicker {
3476
3481
  setAttribute(knob2, ariaValueNow, `${hue}`);
3477
3482
  }
3478
3483
 
3479
- const alphaValue = Math.round(alpha * 100);
3484
+ const alphaValue = roundPart(alpha * 100);
3480
3485
  setAttribute(knob3, ariaValueText, `${alphaValue}%`);
3481
3486
  setAttribute(knob3, ariaValueNow, `${alphaValue}`);
3482
3487
 
@@ -3499,10 +3504,16 @@ class ColorPicker {
3499
3504
  /** Updates the control knobs actual positions. */
3500
3505
  updateControls() {
3501
3506
  const { controlKnobs, controlPositions } = this;
3507
+ let {
3508
+ c1x, c1y, c2y, c3y,
3509
+ } = controlPositions;
3502
3510
  const [control1, control2, control3] = controlKnobs;
3503
- setElementStyle(control1, { transform: `translate3d(${controlPositions.c1x - 4}px,${controlPositions.c1y - 4}px,0)` });
3504
- setElementStyle(control2, { transform: `translate3d(0,${controlPositions.c2y - 4}px,0)` });
3505
- setElementStyle(control3, { transform: `translate3d(0,${controlPositions.c3y - 4}px,0)` });
3511
+ // round control positions
3512
+ [c1x, c1y, c2y, c3y] = [c1x, c1y, c2y, c3y].map(roundPart);
3513
+
3514
+ setElementStyle(control1, { transform: `translate3d(${c1x - 4}px,${c1y - 4}px,0)` });
3515
+ setElementStyle(control2, { transform: `translate3d(0,${c2y - 4}px,0)` });
3516
+ setElementStyle(control3, { transform: `translate3d(0,${c3y - 4}px,0)` });
3506
3517
  }
3507
3518
 
3508
3519
  /**
@@ -3515,16 +3526,16 @@ class ColorPicker {
3515
3526
  value: oldColor, format, inputs, color, hsl,
3516
3527
  } = self;
3517
3528
  const [i1, i2, i3, i4] = inputs;
3518
- const alpha = Math.round(color.a * 100);
3519
- const hue = Math.round(hsl.h * 360);
3529
+ const alpha = roundPart(color.a * 100);
3530
+ const hue = roundPart(hsl.h * 360);
3520
3531
  let newColor;
3521
3532
 
3522
3533
  if (format === 'hex') {
3523
3534
  newColor = self.color.toHexString(true);
3524
3535
  i1.value = self.hex;
3525
3536
  } else if (format === 'hsl') {
3526
- const lightness = Math.round(hsl.l * 100);
3527
- const saturation = Math.round(hsl.s * 100);
3537
+ const lightness = roundPart(hsl.l * 100);
3538
+ const saturation = roundPart(hsl.s * 100);
3528
3539
  newColor = self.color.toHslString();
3529
3540
  i1.value = `${hue}`;
3530
3541
  i2.value = `${saturation}`;
@@ -3532,8 +3543,8 @@ class ColorPicker {
3532
3543
  i4.value = `${alpha}`;
3533
3544
  } else if (format === 'hwb') {
3534
3545
  const { w, b } = self.hwb;
3535
- const whiteness = Math.round(w * 100);
3536
- const blackness = Math.round(b * 100);
3546
+ const whiteness = roundPart(w * 100);
3547
+ const blackness = roundPart(b * 100);
3537
3548
 
3538
3549
  newColor = self.color.toHwbString();
3539
3550
  i1.value = `${hue}`;
@@ -3541,7 +3552,8 @@ class ColorPicker {
3541
3552
  i3.value = `${blackness}`;
3542
3553
  i4.value = `${alpha}`;
3543
3554
  } else if (format === 'rgb') {
3544
- const { r, g, b } = self.rgb;
3555
+ let { r, g, b } = self.rgb;
3556
+ [r, g, b] = [r, g, b].map(roundPart);
3545
3557
 
3546
3558
  newColor = self.color.toRgbString();
3547
3559
  i1.value = `${r}`;
@@ -3605,7 +3617,7 @@ class ColorPicker {
3605
3617
  const self = this;
3606
3618
  const { colorPicker } = self;
3607
3619
 
3608
- if (!hasClass(colorPicker, 'show')) {
3620
+ if (!['top', 'bottom'].some((c) => hasClass(colorPicker, c))) {
3609
3621
  showDropdown(self, colorPicker);
3610
3622
  }
3611
3623
  }
@@ -3622,21 +3634,6 @@ class ColorPicker {
3622
3634
  }
3623
3635
  }
3624
3636
 
3625
- /** Shows the `ColorPicker` dropdown or the presets menu. */
3626
- show() {
3627
- const self = this;
3628
- const { menuToggle } = self;
3629
- if (!self.isOpen) {
3630
- toggleEventsOnShown(self, true);
3631
- self.updateDropdownPosition();
3632
- self.isOpen = true;
3633
- setAttribute(self.input, 'tabindex', '0');
3634
- if (menuToggle) {
3635
- setAttribute(menuToggle, 'tabindex', '0');
3636
- }
3637
- }
3638
- }
3639
-
3640
3637
  /**
3641
3638
  * Hides the currently open `ColorPicker` dropdown.
3642
3639
  * @param {boolean=} focusPrevented
@@ -3671,9 +3668,9 @@ class ColorPicker {
3671
3668
  if (!focusPrevented) {
3672
3669
  focus(pickerToggle);
3673
3670
  }
3674
- setAttribute(input, 'tabindex', '-1');
3671
+ setAttribute(input, tabIndex, '-1');
3675
3672
  if (menuToggle) {
3676
- setAttribute(menuToggle, 'tabindex', '-1');
3673
+ setAttribute(menuToggle, tabIndex, '-1');
3677
3674
  }
3678
3675
  }
3679
3676
  }
@@ -3687,7 +3684,10 @@ class ColorPicker {
3687
3684
  [...parent.children].forEach((el) => {
3688
3685
  if (el !== input) el.remove();
3689
3686
  });
3687
+
3688
+ removeAttribute(input, tabIndex);
3690
3689
  setElementStyle(input, { backgroundColor: '' });
3690
+
3691
3691
  ['txt-light', 'txt-dark'].forEach((c) => removeClass(parent, c));
3692
3692
  Data.remove(input, colorPickerString);
3693
3693
  }
@@ -3695,10 +3695,16 @@ class ColorPicker {
3695
3695
 
3696
3696
  ObjectAssign(ColorPicker, {
3697
3697
  Color,
3698
+ ColorPalette,
3698
3699
  Version,
3699
3700
  getInstance: getColorPickerInstance,
3700
3701
  init: initColorPicker,
3701
3702
  selector: colorPickerSelector,
3703
+ // utils important for render
3704
+ roundPart,
3705
+ setElementStyle,
3706
+ setAttribute,
3707
+ getBoundingClientRect,
3702
3708
  });
3703
3709
 
3704
- export default ColorPicker;
3710
+ export { ColorPicker as default };