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

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