@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,13 +1,13 @@
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
  */
6
6
  (function (global, factory) {
7
7
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
8
8
  typeof define === 'function' && define.amd ? define(factory) :
9
- (global = global || self, global.ColorPicker = factory());
10
- }(this, (function () { 'use strict';
9
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ColorPicker = factory());
10
+ })(this, (function () { 'use strict';
11
11
 
12
12
  /** @type {Record<string, any>} */
13
13
  const EventRegistry = {};
@@ -135,24 +135,6 @@
135
135
  */
136
136
  const ariaValueNow = 'aria-valuenow';
137
137
 
138
- /**
139
- * A global namespace for aria-haspopup.
140
- * @type {string}
141
- */
142
- const ariaHasPopup = 'aria-haspopup';
143
-
144
- /**
145
- * A global namespace for aria-hidden.
146
- * @type {string}
147
- */
148
- const ariaHidden = 'aria-hidden';
149
-
150
- /**
151
- * A global namespace for aria-labelledby.
152
- * @type {string}
153
- */
154
- const ariaLabelledBy = 'aria-labelledby';
155
-
156
138
  /**
157
139
  * A global namespace for `ArrowDown` key.
158
140
  * @type {string} e.which = 40 equivalent
@@ -279,37 +261,6 @@
279
261
  */
280
262
  const focusoutEvent = 'focusout';
281
263
 
282
- // @ts-ignore
283
- const { userAgentData: uaDATA } = navigator;
284
-
285
- /**
286
- * A global namespace for `userAgentData` object.
287
- */
288
- const userAgentData = uaDATA;
289
-
290
- const { userAgent: userAgentString } = navigator;
291
-
292
- /**
293
- * A global namespace for `navigator.userAgent` string.
294
- */
295
- const userAgent = userAgentString;
296
-
297
- const mobileBrands = /iPhone|iPad|iPod|Android/i;
298
- let isMobileCheck = false;
299
-
300
- if (userAgentData) {
301
- isMobileCheck = userAgentData.brands
302
- .some((/** @type {Record<String, any>} */x) => mobileBrands.test(x.brand));
303
- } else {
304
- isMobileCheck = mobileBrands.test(userAgent);
305
- }
306
-
307
- /**
308
- * A global `boolean` for mobile detection.
309
- * @type {boolean}
310
- */
311
- const isMobile = isMobileCheck;
312
-
313
264
  /**
314
265
  * Returns the `document` or the `#document` element.
315
266
  * @see https://github.com/floating-ui/floating-ui
@@ -530,60 +481,6 @@
530
481
  return lookUp.getElementsByClassName(selector);
531
482
  }
532
483
 
533
- /**
534
- * Shortcut for `Object.assign()` static method.
535
- * @param {Record<string, any>} obj a target object
536
- * @param {Record<string, any>} source a source object
537
- */
538
- const ObjectAssign = (obj, source) => Object.assign(obj, source);
539
-
540
- /**
541
- * This is a shortie for `document.createElement` method
542
- * which allows you to create a new `HTMLElement` for a given `tagName`
543
- * or based on an object with specific non-readonly attributes:
544
- * `id`, `className`, `textContent`, `style`, etc.
545
- * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
546
- *
547
- * @param {Record<string, string> | string} param `tagName` or object
548
- * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
549
- */
550
- function createElement(param) {
551
- if (typeof param === 'string') {
552
- return getDocument().createElement(param);
553
- }
554
-
555
- const { tagName } = param;
556
- const attr = { ...param };
557
- const newElement = createElement(tagName);
558
- delete attr.tagName;
559
- ObjectAssign(newElement, attr);
560
- return newElement;
561
- }
562
-
563
- /**
564
- * This is a shortie for `document.createElementNS` method
565
- * which allows you to create a new `HTMLElement` for a given `tagName`
566
- * or based on an object with specific non-readonly attributes:
567
- * `id`, `className`, `textContent`, `style`, etc.
568
- * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
569
- *
570
- * @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
571
- * @param {Record<string, string> | string} param `tagName` or object
572
- * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
573
- */
574
- function createElementNS(namespace, param) {
575
- if (typeof param === 'string') {
576
- return getDocument().createElementNS(namespace, param);
577
- }
578
-
579
- const { tagName } = param;
580
- const attr = { ...param };
581
- const newElement = createElementNS(namespace, tagName);
582
- delete attr.tagName;
583
- ObjectAssign(newElement, attr);
584
- return newElement;
585
- }
586
-
587
484
  /**
588
485
  * Shortcut for the `Element.dispatchEvent(Event)` method.
589
486
  *
@@ -592,6 +489,13 @@
592
489
  */
593
490
  const dispatchEvent = (element, event) => element.dispatchEvent(event);
594
491
 
492
+ /**
493
+ * Shortcut for `Object.assign()` static method.
494
+ * @param {Record<string, any>} obj a target object
495
+ * @param {Record<string, any>} source a source object
496
+ */
497
+ const ObjectAssign = (obj, source) => Object.assign(obj, source);
498
+
595
499
  /** @type {Map<string, Map<HTMLElement | Element, Record<string, any>>>} */
596
500
  const componentData = new Map();
597
501
  /**
@@ -843,32 +747,10 @@
843
747
  */
844
748
  const removeAttribute = (element, attribute) => element.removeAttribute(attribute);
845
749
 
846
- /** @type {Record<string, string>} */
847
- const colorPickerLabels = {
848
- pickerLabel: 'Colour Picker',
849
- appearanceLabel: 'Colour Appearance',
850
- valueLabel: 'Colour Value',
851
- toggleLabel: 'Select Colour',
852
- presetsLabel: 'Colour Presets',
853
- defaultsLabel: 'Colour Defaults',
854
- formatLabel: 'Format',
855
- alphaLabel: 'Alpha',
856
- hexLabel: 'Hexadecimal',
857
- hueLabel: 'Hue',
858
- whitenessLabel: 'Whiteness',
859
- blacknessLabel: 'Blackness',
860
- saturationLabel: 'Saturation',
861
- lightnessLabel: 'Lightness',
862
- redLabel: 'Red',
863
- greenLabel: 'Green',
864
- blueLabel: 'Blue',
865
- };
866
-
867
750
  /**
868
- * A list of 17 color names used for WAI-ARIA compliance.
869
- * @type {string[]}
751
+ * A global namespace for `document.head`.
870
752
  */
871
- const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
753
+ const { head: documentHead } = document;
872
754
 
873
755
  /**
874
756
  * A list of explicit default non-color values.
@@ -876,284 +758,104 @@
876
758
  const nonColors = ['transparent', 'currentColor', 'inherit', 'revert', 'initial'];
877
759
 
878
760
  /**
879
- * Shortcut for `String.toUpperCase()`.
880
- *
881
- * @param {string} source input string
882
- * @returns {string} uppercase output string
761
+ * Round colour components, for all formats except HEX.
762
+ * @param {number} v one of the colour components
763
+ * @returns {number} the rounded number
883
764
  */
884
- const toUpperCase = (source) => source.toUpperCase();
765
+ function roundPart(v) {
766
+ const floor = Math.floor(v);
767
+ return v - floor < 0.5 ? floor : Math.round(v);
768
+ }
885
769
 
886
- const vHidden = 'v-hidden';
770
+ // Color supported formats
771
+ const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsv', 'hwb'];
887
772
 
888
- /**
889
- * Returns the color form for `ColorPicker`.
890
- *
891
- * @param {CP.ColorPicker} self the `ColorPicker` instance
892
- * @returns {HTMLElement | Element} a new `<div>` element with color component `<input>`
893
- */
894
- function getColorForm(self) {
895
- const { format, id, componentLabels } = self;
896
- const colorForm = createElement({
897
- tagName: 'div',
898
- className: `color-form ${format}`,
899
- });
773
+ // Hue angles
774
+ const ANGLES = 'deg|rad|grad|turn';
900
775
 
901
- let components = ['hex'];
902
- if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
903
- else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
904
- else if (format === 'hwb') components = ['hue', 'whiteness', 'blackness', 'alpha'];
776
+ // <http://www.w3.org/TR/css3-values/#integers>
777
+ const CSS_INTEGER = '[-\\+]?\\d+%?';
905
778
 
906
- components.forEach((c) => {
907
- const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
908
- const cID = `color_${format}_${c}_${id}`;
909
- const formatLabel = componentLabels[`${c}Label`];
910
- const cInputLabel = createElement({ tagName: 'label' });
911
- setAttribute(cInputLabel, 'for', cID);
912
- cInputLabel.append(
913
- createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
914
- createElement({ tagName: 'span', className: vHidden, innerText: formatLabel }),
915
- );
916
- const cInput = createElement({
917
- tagName: 'input',
918
- id: cID,
919
- // name: cID, - prevent saving the value to a form
920
- type: format === 'hex' ? 'text' : 'number',
921
- value: c === 'alpha' ? '100' : '0',
922
- className: `color-input ${c}`,
923
- });
924
- setAttribute(cInput, 'autocomplete', 'off');
925
- setAttribute(cInput, 'spellcheck', 'false');
779
+ // Include CSS3 Module
780
+ // <http://www.w3.org/TR/css3-values/#number-value>
781
+ const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
926
782
 
927
- // alpha
928
- let max = '100';
929
- let step = '1';
930
- if (c !== 'alpha') {
931
- if (format === 'rgb') {
932
- max = '255'; step = '1';
933
- } else if (c === 'hue') {
934
- max = '360'; step = '1';
935
- }
936
- }
937
- ObjectAssign(cInput, {
938
- min: '0',
939
- max,
940
- step,
941
- });
942
- // }
943
- colorForm.append(cInputLabel, cInput);
944
- });
945
- return colorForm;
946
- }
783
+ // Include CSS4 Module Hue degrees unit
784
+ // <https://www.w3.org/TR/css3-values/#angle-value>
785
+ const CSS_ANGLE = `[-\\+]?\\d*\\.?\\d+(?:${ANGLES})?`;
786
+
787
+ // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
788
+ const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
789
+
790
+ // Add angles to the mix
791
+ const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
792
+
793
+ // Start & end
794
+ const START_MATCH = '(?:[\\s|\\(\\s|\\s\\(\\s]+)?';
795
+ const END_MATCH = '(?:[\\s|\\)\\s]+)?';
796
+ // Components separation
797
+ const SEP = '(?:[,|\\s]+)';
798
+ const SEP2 = '(?:[,|\\/\\s]*)?';
799
+
800
+ // Actual matching.
801
+ // Parentheses and commas are optional, but not required.
802
+ // Whitespace can take the place of commas or opening paren
803
+ const PERMISSIVE_MATCH = `${START_MATCH}(${CSS_UNIT2})${SEP}(${CSS_UNIT})${SEP}(${CSS_UNIT})${SEP2}(${CSS_UNIT})?${END_MATCH}`;
804
+
805
+ const matchers = {
806
+ CSS_UNIT: new RegExp(CSS_UNIT2),
807
+ hwb: new RegExp(`hwb${PERMISSIVE_MATCH}`),
808
+ rgb: new RegExp(`rgb(?:a)?${PERMISSIVE_MATCH}`),
809
+ hsl: new RegExp(`hsl(?:a)?${PERMISSIVE_MATCH}`),
810
+ hsv: new RegExp(`hsv(?:a)?${PERMISSIVE_MATCH}`),
811
+ hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
812
+ hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
813
+ hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
814
+ hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
815
+ };
947
816
 
948
817
  /**
949
- * A global namespace for aria-label.
950
- * @type {string}
818
+ * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
819
+ * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
820
+ * @param {string} n testing number
821
+ * @returns {boolean} the query result
951
822
  */
952
- const ariaLabel = 'aria-label';
823
+ function isOnePointZero(n) {
824
+ return `${n}`.includes('.') && parseFloat(n) === 1;
825
+ }
953
826
 
954
827
  /**
955
- * A global namespace for aria-valuemin.
956
- * @type {string}
828
+ * Check to see if string passed in is a percentage
829
+ * @param {string} n testing number
830
+ * @returns {boolean} the query result
957
831
  */
958
- const ariaValueMin = 'aria-valuemin';
832
+ function isPercentage(n) {
833
+ return `${n}`.includes('%');
834
+ }
959
835
 
960
836
  /**
961
- * A global namespace for aria-valuemax.
962
- * @type {string}
837
+ * Check to see if string passed is a web safe colour.
838
+ * @see https://stackoverflow.com/a/16994164
839
+ * @param {string} color a colour name, EG: *red*
840
+ * @returns {boolean} the query result
963
841
  */
964
- const ariaValueMax = 'aria-valuemax';
842
+ function isColorName(color) {
843
+ if (nonColors.includes(color)
844
+ || ['#', ...COLOR_FORMAT].some((f) => color.includes(f))) return false;
845
+
846
+ return ['rgb(255, 255, 255)', 'rgb(0, 0, 0)'].every((c) => {
847
+ setElementStyle(documentHead, { color });
848
+ const computedColor = getElementStyle(documentHead, 'color');
849
+ setElementStyle(documentHead, { color: '' });
850
+ return computedColor !== c;
851
+ });
852
+ }
965
853
 
966
854
  /**
967
- * Returns all color controls for `ColorPicker`.
968
- *
969
- * @param {CP.ColorPicker} self the `ColorPicker` instance
970
- * @returns {HTMLElement | Element} color controls
971
- */
972
- function getColorControls(self) {
973
- const { format, componentLabels } = self;
974
- const {
975
- hueLabel, alphaLabel, lightnessLabel, saturationLabel,
976
- whitenessLabel, blacknessLabel,
977
- } = componentLabels;
978
-
979
- const max1 = format === 'hsl' ? 360 : 100;
980
- const max2 = format === 'hsl' ? 100 : 360;
981
- const max3 = 100;
982
-
983
- let ctrl1Label = format === 'hsl'
984
- ? `${hueLabel} & ${lightnessLabel}`
985
- : `${lightnessLabel} & ${saturationLabel}`;
986
-
987
- ctrl1Label = format === 'hwb'
988
- ? `${whitenessLabel} & ${blacknessLabel}`
989
- : ctrl1Label;
990
-
991
- const ctrl2Label = format === 'hsl'
992
- ? `${saturationLabel}`
993
- : `${hueLabel}`;
994
-
995
- const colorControls = createElement({
996
- tagName: 'div',
997
- className: `color-controls ${format}`,
998
- });
999
-
1000
- const colorPointer = 'color-pointer';
1001
- const colorSlider = 'color-slider';
1002
-
1003
- const controls = [
1004
- {
1005
- i: 1,
1006
- c: colorPointer,
1007
- l: ctrl1Label,
1008
- min: 0,
1009
- max: max1,
1010
- },
1011
- {
1012
- i: 2,
1013
- c: colorSlider,
1014
- l: ctrl2Label,
1015
- min: 0,
1016
- max: max2,
1017
- },
1018
- {
1019
- i: 3,
1020
- c: colorSlider,
1021
- l: alphaLabel,
1022
- min: 0,
1023
- max: max3,
1024
- },
1025
- ];
1026
-
1027
- controls.forEach((template) => {
1028
- const {
1029
- i, c, l, min, max,
1030
- } = template;
1031
- // const hidden = i === 2 && format === 'hwb' ? ' v-hidden' : '';
1032
- const control = createElement({
1033
- tagName: 'div',
1034
- // className: `color-control${hidden}`,
1035
- className: 'color-control',
1036
- });
1037
- setAttribute(control, 'role', 'presentation');
1038
-
1039
- control.append(
1040
- createElement({
1041
- tagName: 'div',
1042
- className: `visual-control visual-control${i}`,
1043
- }),
1044
- );
1045
-
1046
- const knob = createElement({
1047
- tagName: 'div',
1048
- className: `${c} knob`,
1049
- ariaLive: 'polite',
1050
- });
1051
-
1052
- setAttribute(knob, ariaLabel, l);
1053
- setAttribute(knob, 'role', 'slider');
1054
- setAttribute(knob, 'tabindex', '0');
1055
- setAttribute(knob, ariaValueMin, `${min}`);
1056
- setAttribute(knob, ariaValueMax, `${max}`);
1057
- control.append(knob);
1058
- colorControls.append(control);
1059
- });
1060
-
1061
- return colorControls;
1062
- }
1063
-
1064
- /**
1065
- * Returns the `document.head` or the `<head>` element.
1066
- *
1067
- * @param {(Node | HTMLElement | Element | globalThis)=} node
1068
- * @returns {HTMLElement | HTMLHeadElement}
1069
- */
1070
- function getDocumentHead(node) {
1071
- return getDocument(node).head;
1072
- }
1073
-
1074
- // Color supported formats
1075
- const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsb', 'hwb'];
1076
-
1077
- // Hue angles
1078
- const ANGLES = 'deg|rad|grad|turn';
1079
-
1080
- // <http://www.w3.org/TR/css3-values/#integers>
1081
- const CSS_INTEGER = '[-\\+]?\\d+%?';
1082
-
1083
- // Include CSS3 Module
1084
- // <http://www.w3.org/TR/css3-values/#number-value>
1085
- const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
1086
-
1087
- // Include CSS4 Module Hue degrees unit
1088
- // <https://www.w3.org/TR/css3-values/#angle-value>
1089
- const CSS_ANGLE = `[-\\+]?\\d*\\.?\\d+(?:${ANGLES})?`;
1090
-
1091
- // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
1092
- const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
1093
-
1094
- // Add angles to the mix
1095
- const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
1096
-
1097
- // Actual matching.
1098
- // Parentheses and commas are optional, but not required.
1099
- // Whitespace can take the place of commas or opening paren
1100
- const PERMISSIVE_MATCH = `[\\s|\\(]+(${CSS_UNIT2})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s|\\/\\s]*(${CSS_UNIT})?\\s*\\)?`;
1101
-
1102
- const matchers = {
1103
- CSS_UNIT: new RegExp(CSS_UNIT2),
1104
- hwb: new RegExp(`hwb${PERMISSIVE_MATCH}`),
1105
- rgb: new RegExp(`rgb(?:a)?${PERMISSIVE_MATCH}`),
1106
- hsl: new RegExp(`hsl(?:a)?${PERMISSIVE_MATCH}`),
1107
- hsv: new RegExp(`hsv(?:a)?${PERMISSIVE_MATCH}`),
1108
- hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1109
- hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
1110
- hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1111
- hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
1112
- };
1113
-
1114
- /**
1115
- * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
1116
- * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
1117
- * @param {string} n testing number
1118
- * @returns {boolean} the query result
1119
- */
1120
- function isOnePointZero(n) {
1121
- return `${n}`.includes('.') && parseFloat(n) === 1;
1122
- }
1123
-
1124
- /**
1125
- * Check to see if string passed in is a percentage
1126
- * @param {string} n testing number
1127
- * @returns {boolean} the query result
1128
- */
1129
- function isPercentage(n) {
1130
- return `${n}`.includes('%');
1131
- }
1132
-
1133
- /**
1134
- * Check to see if string passed in is an angle
1135
- * @param {string} n testing string
1136
- * @returns {boolean} the query result
1137
- */
1138
- function isAngle(n) {
1139
- return ANGLES.split('|').some((a) => `${n}`.includes(a));
1140
- }
1141
-
1142
- /**
1143
- * Check to see if string passed is a web safe colour.
1144
- * @param {string} color a colour name, EG: *red*
1145
- * @returns {boolean} the query result
1146
- */
1147
- function isColorName(color) {
1148
- return !['#', ...COLOR_FORMAT].some((s) => color.includes(s))
1149
- && !/[0-9]/.test(color);
1150
- }
1151
-
1152
- /**
1153
- * Check to see if it looks like a CSS unit
1154
- * (see `matchers` above for definition).
1155
- * @param {string | number} color testing value
1156
- * @returns {boolean} the query result
855
+ * Check to see if it looks like a CSS unit
856
+ * (see `matchers` above for definition).
857
+ * @param {string | number} color testing value
858
+ * @returns {boolean} the query result
1157
859
  */
1158
860
  function isValidCSSUnit(color) {
1159
861
  return Boolean(matchers.CSS_UNIT.exec(String(color)));
@@ -1167,15 +869,15 @@
1167
869
  */
1168
870
  function bound01(N, max) {
1169
871
  let n = N;
1170
- if (isOnePointZero(n)) n = '100%';
1171
-
1172
- n = max === 360 ? n : Math.min(max, Math.max(0, parseFloat(n)));
872
+ if (isOnePointZero(N)) n = '100%';
1173
873
 
1174
- // Handle hue angles
1175
- if (isAngle(N)) n = N.replace(new RegExp(ANGLES), '');
874
+ const processPercent = isPercentage(n);
875
+ n = max === 360
876
+ ? parseFloat(n)
877
+ : Math.min(max, Math.max(0, parseFloat(n)));
1176
878
 
1177
879
  // Automatically convert percentage into number
1178
- if (isPercentage(n)) n = parseInt(String(n * max), 10) / 100;
880
+ if (processPercent) n = (n * max) / 100;
1179
881
 
1180
882
  // Handle floating point rounding errors
1181
883
  if (Math.abs(n - max) < 0.000001) {
@@ -1186,11 +888,11 @@
1186
888
  // If n is a hue given in degrees,
1187
889
  // wrap around out-of-range values into [0, 360] range
1188
890
  // then convert into [0, 1].
1189
- n = (n < 0 ? (n % max) + max : n % max) / parseFloat(String(max));
891
+ n = (n < 0 ? (n % max) + max : n % max) / max;
1190
892
  } else {
1191
893
  // If n not a hue given in degrees
1192
894
  // Convert into [0, 1] range if it isn't already.
1193
- n = (n % max) / parseFloat(String(max));
895
+ n = (n % max) / max;
1194
896
  }
1195
897
  return n;
1196
898
  }
@@ -1225,7 +927,6 @@
1225
927
  * @returns {string}
1226
928
  */
1227
929
  function getRGBFromName(name) {
1228
- const documentHead = getDocumentHead();
1229
930
  setElementStyle(documentHead, { color: name });
1230
931
  const colorName = getElementStyle(documentHead, 'color');
1231
932
  setElementStyle(documentHead, { color: '' });
@@ -1238,7 +939,7 @@
1238
939
  * @returns {string} - the hexadecimal value
1239
940
  */
1240
941
  function convertDecimalToHex(d) {
1241
- return Math.round(d * 255).toString(16);
942
+ return roundPart(d * 255).toString(16);
1242
943
  }
1243
944
 
1244
945
  /**
@@ -1324,6 +1025,36 @@
1324
1025
  return p;
1325
1026
  }
1326
1027
 
1028
+ /**
1029
+ * Converts an HSL colour value to RGB.
1030
+ *
1031
+ * @param {number} h Hue Angle [0, 1]
1032
+ * @param {number} s Saturation [0, 1]
1033
+ * @param {number} l Lightness Angle [0, 1]
1034
+ * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1035
+ */
1036
+ function hslToRgb(h, s, l) {
1037
+ let r = 0;
1038
+ let g = 0;
1039
+ let b = 0;
1040
+
1041
+ if (s === 0) {
1042
+ // achromatic
1043
+ g = l;
1044
+ b = l;
1045
+ r = l;
1046
+ } else {
1047
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1048
+ const p = 2 * l - q;
1049
+ r = hueToRgb(p, q, h + 1 / 3);
1050
+ g = hueToRgb(p, q, h);
1051
+ b = hueToRgb(p, q, h - 1 / 3);
1052
+ }
1053
+ [r, g, b] = [r, g, b].map((x) => x * 255);
1054
+
1055
+ return { r, g, b };
1056
+ }
1057
+
1327
1058
  /**
1328
1059
  * Returns an HWB colour object from an RGB colour object.
1329
1060
  * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
@@ -1386,36 +1117,6 @@
1386
1117
  return { r, g, b };
1387
1118
  }
1388
1119
 
1389
- /**
1390
- * Converts an HSL colour value to RGB.
1391
- *
1392
- * @param {number} h Hue Angle [0, 1]
1393
- * @param {number} s Saturation [0, 1]
1394
- * @param {number} l Lightness Angle [0, 1]
1395
- * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1396
- */
1397
- function hslToRgb(h, s, l) {
1398
- let r = 0;
1399
- let g = 0;
1400
- let b = 0;
1401
-
1402
- if (s === 0) {
1403
- // achromatic
1404
- g = l;
1405
- b = l;
1406
- r = l;
1407
- } else {
1408
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1409
- const p = 2 * l - q;
1410
- r = hueToRgb(p, q, h + 1 / 3);
1411
- g = hueToRgb(p, q, h);
1412
- b = hueToRgb(p, q, h - 1 / 3);
1413
- }
1414
- [r, g, b] = [r, g, b].map((x) => x * 255);
1415
-
1416
- return { r, g, b };
1417
- }
1418
-
1419
1120
  /**
1420
1121
  * Converts an RGB colour value to HSV.
1421
1122
  *
@@ -1471,10 +1172,11 @@
1471
1172
  const q = v * (1 - f * s);
1472
1173
  const t = v * (1 - (1 - f) * s);
1473
1174
  const mod = i % 6;
1474
- const r = [v, q, p, p, t, v][mod];
1475
- const g = [t, v, v, q, p, p][mod];
1476
- const b = [p, p, t, v, v, q][mod];
1477
- return { r: r * 255, g: g * 255, b: b * 255 };
1175
+ let r = [v, q, p, p, t, v][mod];
1176
+ let g = [t, v, v, q, p, p][mod];
1177
+ let b = [p, p, t, v, v, q][mod];
1178
+ [r, g, b] = [r, g, b].map((n) => n * 255);
1179
+ return { r, g, b };
1478
1180
  }
1479
1181
 
1480
1182
  /**
@@ -1490,15 +1192,15 @@
1490
1192
  */
1491
1193
  function rgbToHex(r, g, b, allow3Char) {
1492
1194
  const hex = [
1493
- pad2(Math.round(r).toString(16)),
1494
- pad2(Math.round(g).toString(16)),
1495
- pad2(Math.round(b).toString(16)),
1195
+ pad2(roundPart(r).toString(16)),
1196
+ pad2(roundPart(g).toString(16)),
1197
+ pad2(roundPart(b).toString(16)),
1496
1198
  ];
1497
1199
 
1498
1200
  // Return a 3 character hex if possible
1499
1201
  if (allow3Char && hex[0].charAt(0) === hex[0].charAt(1)
1500
1202
  && hex[1].charAt(0) === hex[1].charAt(1)
1501
- && hex[2].charAt(0) === hex[2].charAt(1)) {
1203
+ && hex[2].charAt(0) === hex[2].charAt(1)) {
1502
1204
  return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
1503
1205
  }
1504
1206
 
@@ -1517,48 +1219,33 @@
1517
1219
  */
1518
1220
  function rgbaToHex(r, g, b, a, allow4Char) {
1519
1221
  const hex = [
1520
- pad2(Math.round(r).toString(16)),
1521
- pad2(Math.round(g).toString(16)),
1522
- pad2(Math.round(b).toString(16)),
1222
+ pad2(roundPart(r).toString(16)),
1223
+ pad2(roundPart(g).toString(16)),
1224
+ pad2(roundPart(b).toString(16)),
1523
1225
  pad2(convertDecimalToHex(a)),
1524
1226
  ];
1525
1227
 
1526
1228
  // Return a 4 character hex if possible
1527
1229
  if (allow4Char && hex[0].charAt(0) === hex[0].charAt(1)
1528
1230
  && hex[1].charAt(0) === hex[1].charAt(1)
1529
- && hex[2].charAt(0) === hex[2].charAt(1)
1530
- && hex[3].charAt(0) === hex[3].charAt(1)) {
1231
+ && hex[2].charAt(0) === hex[2].charAt(1)
1232
+ && hex[3].charAt(0) === hex[3].charAt(1)) {
1531
1233
  return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
1532
1234
  }
1533
1235
  return hex.join('');
1534
1236
  }
1535
1237
 
1536
- /**
1537
- * Returns a colour object corresponding to a given number.
1538
- * @param {number} color input number
1539
- * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1540
- */
1541
- function numberInputToObject(color) {
1542
- /* eslint-disable no-bitwise */
1543
- return {
1544
- r: color >> 16,
1545
- g: (color & 0xff00) >> 8,
1546
- b: color & 0xff,
1547
- };
1548
- /* eslint-enable no-bitwise */
1549
- }
1550
-
1551
1238
  /**
1552
1239
  * Permissive string parsing. Take in a number of formats, and output an object
1553
1240
  * based on detected format. Returns {r,g,b} or {h,s,l} or {h,s,v}
1554
1241
  * @param {string} input colour value in any format
1555
- * @returns {Record<string, (number | string)> | false} an object matching the RegExp
1242
+ * @returns {Record<string, (number | string | boolean)> | false} an object matching the RegExp
1556
1243
  */
1557
1244
  function stringInputToObject(input) {
1558
- let color = input.trim().toLowerCase();
1245
+ let color = toLowerCase(input.trim());
1559
1246
  if (color.length === 0) {
1560
1247
  return {
1561
- r: 0, g: 0, b: 0, a: 0,
1248
+ r: 0, g: 0, b: 0, a: 1,
1562
1249
  };
1563
1250
  }
1564
1251
  let named = false;
@@ -1566,11 +1253,9 @@
1566
1253
  color = getRGBFromName(color);
1567
1254
  named = true;
1568
1255
  } else if (nonColors.includes(color)) {
1569
- const isTransparent = color === 'transparent';
1570
- const rgb = isTransparent ? 0 : 255;
1571
- const a = isTransparent ? 0 : 1;
1256
+ const a = color === 'transparent' ? 0 : 1;
1572
1257
  return {
1573
- r: rgb, g: rgb, b: rgb, a, format: 'rgb',
1258
+ r: 0, g: 0, b: 0, a, format: 'rgb', ok: true,
1574
1259
  };
1575
1260
  }
1576
1261
 
@@ -1610,7 +1295,6 @@
1610
1295
  g: parseIntFromHex(m2),
1611
1296
  b: parseIntFromHex(m3),
1612
1297
  a: convertHexToDecimal(m4),
1613
- // format: named ? 'rgb' : 'hex8',
1614
1298
  format: named ? 'rgb' : 'hex',
1615
1299
  };
1616
1300
  }
@@ -1674,6 +1358,7 @@
1674
1358
  function inputToRGB(input) {
1675
1359
  let rgb = { r: 0, g: 0, b: 0 };
1676
1360
  let color = input;
1361
+ /** @type {string | number} */
1677
1362
  let a = 1;
1678
1363
  let s = null;
1679
1364
  let v = null;
@@ -1681,8 +1366,11 @@
1681
1366
  let w = null;
1682
1367
  let b = null;
1683
1368
  let h = null;
1369
+ let r = null;
1370
+ let g = null;
1684
1371
  let ok = false;
1685
- let format = 'hex';
1372
+ const inputFormat = typeof color === 'object' && color.format;
1373
+ let format = inputFormat && COLOR_FORMAT.includes(inputFormat) ? inputFormat : 'rgb';
1686
1374
 
1687
1375
  if (typeof input === 'string') {
1688
1376
  // @ts-ignore -- this now is converted to object
@@ -1691,7 +1379,10 @@
1691
1379
  }
1692
1380
  if (typeof color === 'object') {
1693
1381
  if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
1694
- rgb = { r: color.r, g: color.g, b: color.b }; // RGB values in [0, 255] range
1382
+ ({ r, g, b } = color);
1383
+ // RGB values now are all in [0, 255] range
1384
+ [r, g, b] = [r, g, b].map((n) => bound01(n, isPercentage(n) ? 100 : 255) * 255);
1385
+ rgb = { r, g, b };
1695
1386
  ok = true;
1696
1387
  format = 'rgb';
1697
1388
  } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
@@ -1720,14 +1411,17 @@
1720
1411
  format = 'hwb';
1721
1412
  }
1722
1413
  if (isValidCSSUnit(color.a)) {
1723
- a = color.a;
1724
- a = isPercentage(`${a}`) ? bound01(a, 100) : a;
1414
+ a = color.a; // @ts-ignore -- `parseFloat` works with numbers too
1415
+ a = isPercentage(`${a}`) || parseFloat(a) > 1 ? bound01(a, 100) : a;
1725
1416
  }
1726
1417
  }
1418
+ if (typeof color === 'undefined') {
1419
+ ok = true;
1420
+ }
1727
1421
 
1728
1422
  return {
1729
- ok, // @ts-ignore
1730
- format: color.format || format,
1423
+ ok,
1424
+ format,
1731
1425
  r: Math.min(255, Math.max(rgb.r, 0)),
1732
1426
  g: Math.min(255, Math.max(rgb.g, 0)),
1733
1427
  b: Math.min(255, Math.max(rgb.b, 0)),
@@ -1756,7 +1450,8 @@
1756
1450
  color = inputToRGB(color);
1757
1451
  }
1758
1452
  if (typeof color === 'number') {
1759
- color = numberInputToObject(color);
1453
+ const len = `${color}`.length;
1454
+ color = `#${(len === 2 ? '0' : '00')}${color}`;
1760
1455
  }
1761
1456
  const {
1762
1457
  r, g, b, a, ok, format,
@@ -1766,7 +1461,7 @@
1766
1461
  const self = this;
1767
1462
 
1768
1463
  /** @type {CP.ColorInput} */
1769
- self.originalInput = color;
1464
+ self.originalInput = input;
1770
1465
  /** @type {number} */
1771
1466
  self.r = r;
1772
1467
  /** @type {number} */
@@ -1779,14 +1474,6 @@
1779
1474
  self.ok = ok;
1780
1475
  /** @type {CP.ColorFormats} */
1781
1476
  self.format = configFormat || format;
1782
-
1783
- // Don't let the range of [0,255] come back in [0,1].
1784
- // Potentially lose a little bit of precision here, but will fix issues where
1785
- // .5 gets interpreted as half of the total, instead of half of 1
1786
- // If it was supposed to be 128, this was already taken care of by `inputToRgb`
1787
- if (r < 1) self.r = Math.round(r);
1788
- if (g < 1) self.g = Math.round(g);
1789
- if (b < 1) self.b = Math.round(b);
1790
1477
  }
1791
1478
 
1792
1479
  /**
@@ -1802,7 +1489,7 @@
1802
1489
  * @returns {boolean} the query result
1803
1490
  */
1804
1491
  get isDark() {
1805
- return this.brightness < 128;
1492
+ return this.brightness < 120;
1806
1493
  }
1807
1494
 
1808
1495
  /**
@@ -1854,13 +1541,9 @@
1854
1541
  const {
1855
1542
  r, g, b, a,
1856
1543
  } = this;
1857
- const [R, G, B] = [r, g, b].map((x) => Math.round(x));
1858
1544
 
1859
1545
  return {
1860
- r: R,
1861
- g: G,
1862
- b: B,
1863
- a: Math.round(a * 100) / 100,
1546
+ r, g, b, a: roundPart(a * 100) / 100,
1864
1547
  };
1865
1548
  }
1866
1549
 
@@ -1874,10 +1557,11 @@
1874
1557
  const {
1875
1558
  r, g, b, a,
1876
1559
  } = this.toRgb();
1560
+ const [R, G, B] = [r, g, b].map(roundPart);
1877
1561
 
1878
1562
  return a === 1
1879
- ? `rgb(${r}, ${g}, ${b})`
1880
- : `rgba(${r}, ${g}, ${b}, ${a})`;
1563
+ ? `rgb(${R}, ${G}, ${B})`
1564
+ : `rgba(${R}, ${G}, ${B}, ${a})`;
1881
1565
  }
1882
1566
 
1883
1567
  /**
@@ -1890,9 +1574,10 @@
1890
1574
  const {
1891
1575
  r, g, b, a,
1892
1576
  } = this.toRgb();
1893
- const A = a === 1 ? '' : ` / ${Math.round(a * 100)}%`;
1577
+ const [R, G, B] = [r, g, b].map(roundPart);
1578
+ const A = a === 1 ? '' : ` / ${roundPart(a * 100)}%`;
1894
1579
 
1895
- return `rgb(${r} ${g} ${b}${A})`;
1580
+ return `rgb(${R} ${G} ${B}${A})`;
1896
1581
  }
1897
1582
 
1898
1583
  /**
@@ -1985,10 +1670,10 @@
1985
1670
  let {
1986
1671
  h, s, l, a,
1987
1672
  } = this.toHsl();
1988
- h = Math.round(h * 360);
1989
- s = Math.round(s * 100);
1990
- l = Math.round(l * 100);
1991
- a = Math.round(a * 100) / 100;
1673
+ h = roundPart(h * 360);
1674
+ s = roundPart(s * 100);
1675
+ l = roundPart(l * 100);
1676
+ a = roundPart(a * 100) / 100;
1992
1677
 
1993
1678
  return a === 1
1994
1679
  ? `hsl(${h}, ${s}%, ${l}%)`
@@ -2005,11 +1690,11 @@
2005
1690
  let {
2006
1691
  h, s, l, a,
2007
1692
  } = this.toHsl();
2008
- h = Math.round(h * 360);
2009
- s = Math.round(s * 100);
2010
- l = Math.round(l * 100);
2011
- a = Math.round(a * 100);
2012
- const A = a < 100 ? ` / ${Math.round(a)}%` : '';
1693
+ h = roundPart(h * 360);
1694
+ s = roundPart(s * 100);
1695
+ l = roundPart(l * 100);
1696
+ a = roundPart(a * 100);
1697
+ const A = a < 100 ? ` / ${roundPart(a)}%` : '';
2013
1698
 
2014
1699
  return `hsl(${h}deg ${s}% ${l}%${A})`;
2015
1700
  }
@@ -2036,11 +1721,11 @@
2036
1721
  let {
2037
1722
  h, w, b, a,
2038
1723
  } = this.toHwb();
2039
- h = Math.round(h * 360);
2040
- w = Math.round(w * 100);
2041
- b = Math.round(b * 100);
2042
- a = Math.round(a * 100);
2043
- const A = a < 100 ? ` / ${Math.round(a)}%` : '';
1724
+ h = roundPart(h * 360);
1725
+ w = roundPart(w * 100);
1726
+ b = roundPart(b * 100);
1727
+ a = roundPart(a * 100);
1728
+ const A = a < 100 ? ` / ${roundPart(a)}%` : '';
2044
1729
 
2045
1730
  return `hwb(${h}deg ${w}% ${b}%${A})`;
2046
1731
  }
@@ -2146,108 +1831,426 @@
2146
1831
  const self = this;
2147
1832
  const { format } = self;
2148
1833
 
2149
- if (format === 'hex') return self.toHexString(allowShort);
2150
- if (format === 'hsl') return self.toHslString();
2151
- if (format === 'hwb') return self.toHwbString();
1834
+ if (format === 'hex') return self.toHexString(allowShort);
1835
+ if (format === 'hsl') return self.toHslString();
1836
+ if (format === 'hwb') return self.toHwbString();
1837
+
1838
+ return self.toRgbString();
1839
+ }
1840
+ }
1841
+
1842
+ ObjectAssign(Color, {
1843
+ ANGLES,
1844
+ CSS_ANGLE,
1845
+ CSS_INTEGER,
1846
+ CSS_NUMBER,
1847
+ CSS_UNIT,
1848
+ CSS_UNIT2,
1849
+ PERMISSIVE_MATCH,
1850
+ matchers,
1851
+ isOnePointZero,
1852
+ isPercentage,
1853
+ isValidCSSUnit,
1854
+ isColorName,
1855
+ pad2,
1856
+ clamp01,
1857
+ bound01,
1858
+ boundAlpha,
1859
+ getRGBFromName,
1860
+ convertHexToDecimal,
1861
+ convertDecimalToHex,
1862
+ rgbToHsl,
1863
+ rgbToHex,
1864
+ rgbToHsv,
1865
+ rgbToHwb,
1866
+ rgbaToHex,
1867
+ hslToRgb,
1868
+ hsvToRgb,
1869
+ hueToRgb,
1870
+ hwbToRgb,
1871
+ parseIntFromHex,
1872
+ stringInputToObject,
1873
+ inputToRGB,
1874
+ roundPart,
1875
+ getElementStyle,
1876
+ setElementStyle,
1877
+ ObjectAssign,
1878
+ });
1879
+
1880
+ /**
1881
+ * @class
1882
+ * Returns a color palette with a given set of parameters.
1883
+ * @example
1884
+ * new ColorPalette(0, 12, 10);
1885
+ * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: Array<Color> }
1886
+ */
1887
+ class ColorPalette {
1888
+ /**
1889
+ * The `hue` parameter is optional, which would be set to 0.
1890
+ * @param {number[]} args represeinting hue, hueSteps, lightSteps
1891
+ * * `args.hue` the starting Hue [0, 360]
1892
+ * * `args.hueSteps` Hue Steps Count [5, 24]
1893
+ * * `args.lightSteps` Lightness Steps Count [5, 12]
1894
+ */
1895
+ constructor(...args) {
1896
+ let hue = 0;
1897
+ let hueSteps = 12;
1898
+ let lightSteps = 10;
1899
+ let lightnessArray = [0.5];
1900
+
1901
+ if (args.length === 3) {
1902
+ [hue, hueSteps, lightSteps] = args;
1903
+ } else if (args.length === 2) {
1904
+ [hueSteps, lightSteps] = args;
1905
+ if ([hueSteps, lightSteps].some((n) => n < 1)) {
1906
+ throw TypeError('ColorPalette: when 2 arguments used, both must be larger than 0.');
1907
+ }
1908
+ } else {
1909
+ throw TypeError('ColorPalette requires minimum 2 arguments');
1910
+ }
1911
+
1912
+ /** @type {Color[]} */
1913
+ const colors = [];
1914
+
1915
+ const hueStep = 360 / hueSteps;
1916
+ const half = roundPart((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
1917
+ const estimatedStep = 100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100;
1918
+
1919
+ let lightStep = 0.25;
1920
+ lightStep = [4, 5].includes(lightSteps) ? 0.2 : lightStep;
1921
+ lightStep = [6, 7].includes(lightSteps) ? 0.15 : lightStep;
1922
+ lightStep = [8, 9].includes(lightSteps) ? 0.11 : lightStep;
1923
+ lightStep = [10, 11].includes(lightSteps) ? 0.09 : lightStep;
1924
+ lightStep = [12, 13].includes(lightSteps) ? 0.075 : lightStep;
1925
+ lightStep = lightSteps > 13 ? estimatedStep : lightStep;
1926
+
1927
+ // light tints
1928
+ for (let i = 1; i < half + 1; i += 1) {
1929
+ lightnessArray = [...lightnessArray, (0.5 + lightStep * (i))];
1930
+ }
1931
+
1932
+ // dark tints
1933
+ for (let i = 1; i < lightSteps - half; i += 1) {
1934
+ lightnessArray = [(0.5 - lightStep * (i)), ...lightnessArray];
1935
+ }
1936
+
1937
+ // feed `colors` Array
1938
+ for (let i = 0; i < hueSteps; i += 1) {
1939
+ const currentHue = ((hue + i * hueStep) % 360) / 360;
1940
+ lightnessArray.forEach((l) => {
1941
+ colors.push(new Color({ h: currentHue, s: 1, l }));
1942
+ });
1943
+ }
1944
+
1945
+ this.hue = hue;
1946
+ this.hueSteps = hueSteps;
1947
+ this.lightSteps = lightSteps;
1948
+ this.colors = colors;
1949
+ }
1950
+ }
1951
+
1952
+ ObjectAssign(ColorPalette, { Color });
1953
+
1954
+ /** @type {Record<string, string>} */
1955
+ const colorPickerLabels = {
1956
+ pickerLabel: 'Colour Picker',
1957
+ appearanceLabel: 'Colour Appearance',
1958
+ valueLabel: 'Colour Value',
1959
+ toggleLabel: 'Select Colour',
1960
+ presetsLabel: 'Colour Presets',
1961
+ defaultsLabel: 'Colour Defaults',
1962
+ formatLabel: 'Format',
1963
+ alphaLabel: 'Alpha',
1964
+ hexLabel: 'Hexadecimal',
1965
+ hueLabel: 'Hue',
1966
+ whitenessLabel: 'Whiteness',
1967
+ blacknessLabel: 'Blackness',
1968
+ saturationLabel: 'Saturation',
1969
+ lightnessLabel: 'Lightness',
1970
+ redLabel: 'Red',
1971
+ greenLabel: 'Green',
1972
+ blueLabel: 'Blue',
1973
+ };
1974
+
1975
+ /**
1976
+ * A list of 17 color names used for WAI-ARIA compliance.
1977
+ * @type {string[]}
1978
+ */
1979
+ const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
1980
+
1981
+ const tabIndex = 'tabindex';
1982
+
1983
+ /**
1984
+ * Check if a string is valid JSON string.
1985
+ * @param {string} str the string input
1986
+ * @returns {boolean} the query result
1987
+ */
1988
+ function isValidJSON(str) {
1989
+ try {
1990
+ JSON.parse(str);
1991
+ } catch (e) {
1992
+ return false;
1993
+ }
1994
+ return true;
1995
+ }
1996
+
1997
+ /**
1998
+ * Shortcut for `String.toUpperCase()`.
1999
+ *
2000
+ * @param {string} source input string
2001
+ * @returns {string} uppercase output string
2002
+ */
2003
+ const toUpperCase = (source) => source.toUpperCase();
2004
+
2005
+ /**
2006
+ * A global namespace for aria-haspopup.
2007
+ * @type {string}
2008
+ */
2009
+ const ariaHasPopup = 'aria-haspopup';
2010
+
2011
+ /**
2012
+ * A global namespace for aria-hidden.
2013
+ * @type {string}
2014
+ */
2015
+ const ariaHidden = 'aria-hidden';
2016
+
2017
+ /**
2018
+ * A global namespace for aria-labelledby.
2019
+ * @type {string}
2020
+ */
2021
+ const ariaLabelledBy = 'aria-labelledby';
2022
+
2023
+ /**
2024
+ * This is a shortie for `document.createElement` method
2025
+ * which allows you to create a new `HTMLElement` for a given `tagName`
2026
+ * or based on an object with specific non-readonly attributes:
2027
+ * `id`, `className`, `textContent`, `style`, etc.
2028
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
2029
+ *
2030
+ * @param {Record<string, string> | string} param `tagName` or object
2031
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
2032
+ */
2033
+ function createElement(param) {
2034
+ if (typeof param === 'string') {
2035
+ return getDocument().createElement(param);
2036
+ }
2037
+
2038
+ const { tagName } = param;
2039
+ const attr = { ...param };
2040
+ const newElement = createElement(tagName);
2041
+ delete attr.tagName;
2042
+ ObjectAssign(newElement, attr);
2043
+ return newElement;
2044
+ }
2045
+
2046
+ /**
2047
+ * This is a shortie for `document.createElementNS` method
2048
+ * which allows you to create a new `HTMLElement` for a given `tagName`
2049
+ * or based on an object with specific non-readonly attributes:
2050
+ * `id`, `className`, `textContent`, `style`, etc.
2051
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
2052
+ *
2053
+ * @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
2054
+ * @param {Record<string, string> | string} param `tagName` or object
2055
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
2056
+ */
2057
+ function createElementNS(namespace, param) {
2058
+ if (typeof param === 'string') {
2059
+ return getDocument().createElementNS(namespace, param);
2060
+ }
2061
+
2062
+ const { tagName } = param;
2063
+ const attr = { ...param };
2064
+ const newElement = createElementNS(namespace, tagName);
2065
+ delete attr.tagName;
2066
+ ObjectAssign(newElement, attr);
2067
+ return newElement;
2068
+ }
2069
+
2070
+ const vHidden = 'v-hidden';
2071
+
2072
+ /**
2073
+ * Returns the color form for `ColorPicker`.
2074
+ *
2075
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
2076
+ * @returns {HTMLElement | Element} a new `<div>` element with color component `<input>`
2077
+ */
2078
+ function getColorForm(self) {
2079
+ const { format, id, componentLabels } = self;
2080
+ const colorForm = createElement({
2081
+ tagName: 'div',
2082
+ className: `color-form ${format}`,
2083
+ });
2084
+
2085
+ let components = ['hex'];
2086
+ if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
2087
+ else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
2088
+ else if (format === 'hwb') components = ['hue', 'whiteness', 'blackness', 'alpha'];
2089
+
2090
+ components.forEach((c) => {
2091
+ const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
2092
+ const cID = `color_${format}_${c}_${id}`;
2093
+ const formatLabel = componentLabels[`${c}Label`];
2094
+ const cInputLabel = createElement({ tagName: 'label' });
2095
+ setAttribute(cInputLabel, 'for', cID);
2096
+ cInputLabel.append(
2097
+ createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
2098
+ createElement({ tagName: 'span', className: vHidden, innerText: formatLabel }),
2099
+ );
2100
+ const cInput = createElement({
2101
+ tagName: 'input',
2102
+ id: cID,
2103
+ // name: cID, - prevent saving the value to a form
2104
+ type: format === 'hex' ? 'text' : 'number',
2105
+ value: c === 'alpha' ? '100' : '0',
2106
+ className: `color-input ${c}`,
2107
+ });
2108
+ setAttribute(cInput, 'autocomplete', 'off');
2109
+ setAttribute(cInput, 'spellcheck', 'false');
2110
+
2111
+ // alpha
2112
+ let max = '100';
2113
+ let step = '1';
2114
+ if (c !== 'alpha') {
2115
+ if (format === 'rgb') {
2116
+ max = '255'; step = '1';
2117
+ } else if (c === 'hue') {
2118
+ max = '360'; step = '1';
2119
+ }
2120
+ }
2121
+ ObjectAssign(cInput, {
2122
+ min: '0',
2123
+ max,
2124
+ step,
2125
+ });
2126
+ colorForm.append(cInputLabel, cInput);
2127
+ });
2128
+ return colorForm;
2129
+ }
2130
+
2131
+ /**
2132
+ * A global namespace for aria-label.
2133
+ * @type {string}
2134
+ */
2135
+ const ariaLabel = 'aria-label';
2136
+
2137
+ /**
2138
+ * A global namespace for aria-valuemin.
2139
+ * @type {string}
2140
+ */
2141
+ const ariaValueMin = 'aria-valuemin';
2142
+
2143
+ /**
2144
+ * A global namespace for aria-valuemax.
2145
+ * @type {string}
2146
+ */
2147
+ const ariaValueMax = 'aria-valuemax';
2148
+
2149
+ /**
2150
+ * Returns all color controls for `ColorPicker`.
2151
+ *
2152
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
2153
+ * @returns {HTMLElement | Element} color controls
2154
+ */
2155
+ function getColorControls(self) {
2156
+ const { format, componentLabels } = self;
2157
+ const {
2158
+ hueLabel, alphaLabel, lightnessLabel, saturationLabel,
2159
+ whitenessLabel, blacknessLabel,
2160
+ } = componentLabels;
2161
+
2162
+ const max1 = format === 'hsl' ? 360 : 100;
2163
+ const max2 = format === 'hsl' ? 100 : 360;
2164
+ const max3 = 100;
2165
+
2166
+ let ctrl1Label = format === 'hsl'
2167
+ ? `${hueLabel} & ${lightnessLabel}`
2168
+ : `${lightnessLabel} & ${saturationLabel}`;
2169
+
2170
+ ctrl1Label = format === 'hwb'
2171
+ ? `${whitenessLabel} & ${blacknessLabel}`
2172
+ : ctrl1Label;
2152
2173
 
2153
- return self.toRgbString();
2154
- }
2155
- }
2174
+ const ctrl2Label = format === 'hsl'
2175
+ ? `${saturationLabel}`
2176
+ : `${hueLabel}`;
2156
2177
 
2157
- ObjectAssign(Color, {
2158
- ANGLES,
2159
- CSS_ANGLE,
2160
- CSS_INTEGER,
2161
- CSS_NUMBER,
2162
- CSS_UNIT,
2163
- CSS_UNIT2,
2164
- PERMISSIVE_MATCH,
2165
- matchers,
2166
- isOnePointZero,
2167
- isPercentage,
2168
- isValidCSSUnit,
2169
- pad2,
2170
- clamp01,
2171
- bound01,
2172
- boundAlpha,
2173
- getRGBFromName,
2174
- convertHexToDecimal,
2175
- convertDecimalToHex,
2176
- rgbToHsl,
2177
- rgbToHex,
2178
- rgbToHsv,
2179
- rgbToHwb,
2180
- rgbaToHex,
2181
- hslToRgb,
2182
- hsvToRgb,
2183
- hueToRgb,
2184
- hwbToRgb,
2185
- parseIntFromHex,
2186
- numberInputToObject,
2187
- stringInputToObject,
2188
- inputToRGB,
2189
- ObjectAssign,
2190
- });
2178
+ const colorControls = createElement({
2179
+ tagName: 'div',
2180
+ className: `color-controls ${format}`,
2181
+ });
2191
2182
 
2192
- /**
2193
- * @class
2194
- * Returns a color palette with a given set of parameters.
2195
- * @example
2196
- * new ColorPalette(0, 12, 10);
2197
- * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: array }
2198
- */
2199
- class ColorPalette {
2200
- /**
2201
- * The `hue` parameter is optional, which would be set to 0.
2202
- * @param {number[]} args represeinting hue, hueSteps, lightSteps
2203
- * * `args.hue` the starting Hue [0, 360]
2204
- * * `args.hueSteps` Hue Steps Count [5, 13]
2205
- * * `args.lightSteps` Lightness Steps Count [8, 10]
2206
- */
2207
- constructor(...args) {
2208
- let hue = 0;
2209
- let hueSteps = 12;
2210
- let lightSteps = 10;
2211
- let lightnessArray = [0.5];
2183
+ const colorPointer = 'color-pointer';
2184
+ const colorSlider = 'color-slider';
2212
2185
 
2213
- if (args.length === 3) {
2214
- [hue, hueSteps, lightSteps] = args;
2215
- } else if (args.length === 2) {
2216
- [hueSteps, lightSteps] = args;
2217
- } else {
2218
- throw TypeError('The ColorPalette requires minimum 2 arguments');
2219
- }
2186
+ const controls = [
2187
+ {
2188
+ i: 1,
2189
+ c: colorPointer,
2190
+ l: ctrl1Label,
2191
+ min: 0,
2192
+ max: max1,
2193
+ },
2194
+ {
2195
+ i: 2,
2196
+ c: colorSlider,
2197
+ l: ctrl2Label,
2198
+ min: 0,
2199
+ max: max2,
2200
+ },
2201
+ {
2202
+ i: 3,
2203
+ c: colorSlider,
2204
+ l: alphaLabel,
2205
+ min: 0,
2206
+ max: max3,
2207
+ },
2208
+ ];
2220
2209
 
2221
- /** @type {string[]} */
2222
- const colors = [];
2210
+ controls.forEach((template) => {
2211
+ const {
2212
+ i, c, l, min, max,
2213
+ } = template;
2214
+ const control = createElement({
2215
+ tagName: 'div',
2216
+ className: 'color-control',
2217
+ });
2218
+ setAttribute(control, 'role', 'presentation');
2223
2219
 
2224
- const hueStep = 360 / hueSteps;
2225
- const lightStep = 100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100;
2226
- const half = Math.round((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
2220
+ control.append(
2221
+ createElement({
2222
+ tagName: 'div',
2223
+ className: `visual-control visual-control${i}`,
2224
+ }),
2225
+ );
2227
2226
 
2228
- // light tints
2229
- for (let i = 0; i < half; i += 1) {
2230
- lightnessArray = [...lightnessArray, (0.5 + lightStep * (i + 1))];
2231
- }
2227
+ const knob = createElement({
2228
+ tagName: 'div',
2229
+ className: `${c} knob`,
2230
+ ariaLive: 'polite',
2231
+ });
2232
2232
 
2233
- // dark tints
2234
- for (let i = 0; i < lightSteps - half - 1; i += 1) {
2235
- lightnessArray = [(0.5 - lightStep * (i + 1)), ...lightnessArray];
2236
- }
2233
+ setAttribute(knob, ariaLabel, l);
2234
+ setAttribute(knob, 'role', 'slider');
2235
+ setAttribute(knob, tabIndex, '0');
2236
+ setAttribute(knob, ariaValueMin, `${min}`);
2237
+ setAttribute(knob, ariaValueMax, `${max}`);
2238
+ control.append(knob);
2239
+ colorControls.append(control);
2240
+ });
2237
2241
 
2238
- // feed `colors` Array
2239
- for (let i = 0; i < hueSteps; i += 1) {
2240
- const currentHue = ((hue + i * hueStep) % 360) / 360;
2241
- lightnessArray.forEach((l) => {
2242
- colors.push(new Color({ h: currentHue, s: 1, l }).toHexString());
2243
- });
2244
- }
2242
+ return colorControls;
2243
+ }
2245
2244
 
2246
- this.hue = hue;
2247
- this.hueSteps = hueSteps;
2248
- this.lightSteps = lightSteps;
2249
- this.colors = colors;
2250
- }
2245
+ /**
2246
+ * Helps setting CSS variables to the color-menu.
2247
+ * @param {HTMLElement} element
2248
+ * @param {Record<string,any>} props
2249
+ */
2250
+ function setCSSProperties(element, props) {
2251
+ ObjectKeys(props).forEach((prop) => {
2252
+ element.style.setProperty(prop, props[prop]);
2253
+ });
2251
2254
  }
2252
2255
 
2253
2256
  /**
@@ -2267,68 +2270,64 @@
2267
2270
  colorsArray = colorsArray instanceof Array ? colorsArray : [];
2268
2271
  const colorsCount = colorsArray.length;
2269
2272
  const { lightSteps } = isPalette ? colorsSource : { lightSteps: null };
2270
- let fit = lightSteps
2271
- || Math.max(...[5, 6, 7, 8, 9, 10].filter((x) => colorsCount > (x * 2) && !(colorsCount % x)));
2272
- fit = Number.isFinite(fit) ? fit : 5;
2273
+ const fit = lightSteps || [9, 10].find((x) => colorsCount > x * 2 && !(colorsCount % x)) || 5;
2273
2274
  const isMultiLine = isOptionsMenu && colorsCount > fit;
2274
- let rowCountHover = 1;
2275
- rowCountHover = isMultiLine && colorsCount < 27 ? 2 : rowCountHover;
2276
- rowCountHover = colorsCount >= 27 ? 3 : rowCountHover;
2277
- rowCountHover = colorsCount >= 36 ? 4 : rowCountHover;
2278
- rowCountHover = colorsCount >= 45 ? 5 : rowCountHover;
2279
- const rowCount = rowCountHover - (colorsCount < 27 ? 1 : 2);
2280
- const isScrollable = isMultiLine && colorsCount > rowCountHover * fit;
2275
+ let rowCountHover = 2;
2276
+ rowCountHover = isMultiLine && colorsCount >= fit * 2 ? 3 : rowCountHover;
2277
+ rowCountHover = colorsCount >= fit * 3 ? 4 : rowCountHover;
2278
+ rowCountHover = colorsCount >= fit * 4 ? 5 : rowCountHover;
2279
+ const rowCount = rowCountHover - (colorsCount < fit * 3 ? 1 : 2);
2280
+ const isScrollable = isMultiLine && colorsCount > rowCount * fit;
2281
2281
  let finalClass = menuClass;
2282
2282
  finalClass += isScrollable ? ' scrollable' : '';
2283
2283
  finalClass += isMultiLine ? ' multiline' : '';
2284
2284
  const gap = isMultiLine ? '1px' : '0.25rem';
2285
2285
  let optionSize = isMultiLine ? 1.75 : 2;
2286
- optionSize = !(colorsCount % 10) && isMultiLine ? 1.5 : optionSize;
2286
+ optionSize = fit > 5 && isMultiLine ? 1.5 : optionSize;
2287
2287
  const menuHeight = `${(rowCount || 1) * optionSize}rem`;
2288
2288
  const menuHeightHover = `calc(${rowCountHover} * ${optionSize}rem + ${rowCountHover - 1} * ${gap})`;
2289
- const gridTemplateColumns = `repeat(${fit}, ${optionSize}rem)`;
2290
- const gridTemplateRows = `repeat(auto-fill, ${optionSize}rem)`;
2291
-
2289
+ /** @type {HTMLUListElement} */
2290
+ // @ts-ignore -- <UL> is an `HTMLElement`
2292
2291
  const menu = createElement({
2293
2292
  tagName: 'ul',
2294
2293
  className: finalClass,
2295
2294
  });
2296
2295
  setAttribute(menu, 'role', 'listbox');
2297
- setAttribute(menu, ariaLabel, `${menuLabel}`);
2298
-
2299
- if (isOptionsMenu) {
2300
- if (isScrollable) {
2301
- const styleText = 'this.style.height=';
2302
- setAttribute(menu, 'onmouseout', `${styleText}'${menuHeight}'`);
2303
- setAttribute(menu, 'onmouseover', `${styleText}'${menuHeightHover}'`);
2304
- }
2305
- const menuStyle = {
2306
- height: isScrollable ? menuHeight : '', gridTemplateColumns, gridTemplateRows, gap,
2307
- };
2308
- setElementStyle(menu, menuStyle);
2296
+ setAttribute(menu, ariaLabel, menuLabel);
2297
+
2298
+ if (isScrollable) {
2299
+ setCSSProperties(menu, {
2300
+ '--grid-item-size': `${optionSize}rem`,
2301
+ '--grid-fit': fit,
2302
+ '--grid-gap': gap,
2303
+ '--grid-height': menuHeight,
2304
+ '--grid-hover-height': menuHeightHover,
2305
+ });
2309
2306
  }
2310
2307
 
2311
2308
  colorsArray.forEach((x) => {
2312
- const [value, label] = x.trim().split(':');
2313
- const xRealColor = new Color(value, format).toString();
2314
- const isActive = xRealColor === getAttribute(input, 'value');
2309
+ let [value, label] = typeof x === 'string' ? x.trim().split(':') : [];
2310
+ if (x instanceof Color) {
2311
+ value = x.toHexString();
2312
+ label = value;
2313
+ }
2314
+ const color = new Color(x instanceof Color ? x : value, format);
2315
+ const isActive = color.toString() === getAttribute(input, 'value');
2315
2316
  const active = isActive ? ' active' : '';
2316
2317
 
2317
2318
  const option = createElement({
2318
2319
  tagName: 'li',
2319
2320
  className: `color-option${active}`,
2320
- innerText: `${label || x}`,
2321
+ innerText: `${label || value}`,
2321
2322
  });
2322
2323
 
2323
- setAttribute(option, 'tabindex', '0');
2324
+ setAttribute(option, tabIndex, '0');
2324
2325
  setAttribute(option, 'data-value', `${value}`);
2325
2326
  setAttribute(option, 'role', 'option');
2326
2327
  setAttribute(option, ariaSelected, isActive ? 'true' : 'false');
2327
2328
 
2328
2329
  if (isOptionsMenu) {
2329
- setElementStyle(option, {
2330
- width: `${optionSize}rem`, height: `${optionSize}rem`, backgroundColor: x,
2331
- });
2330
+ setElementStyle(option, { backgroundColor: value });
2332
2331
  }
2333
2332
 
2334
2333
  menu.append(option);
@@ -2337,55 +2336,10 @@
2337
2336
  }
2338
2337
 
2339
2338
  /**
2340
- * Check if a string is valid JSON string.
2341
- * @param {string} str the string input
2342
- * @returns {boolean} the query result
2343
- */
2344
- function isValidJSON(str) {
2345
- try {
2346
- JSON.parse(str);
2347
- } catch (e) {
2348
- return false;
2349
- }
2350
- return true;
2351
- }
2352
-
2353
- var version = "0.0.1alpha2";
2354
-
2355
- // @ts-ignore
2356
-
2357
- const Version = version;
2358
-
2359
- // ColorPicker GC
2360
- // ==============
2361
- const colorPickerString = 'color-picker';
2362
- const colorPickerSelector = `[data-function="${colorPickerString}"]`;
2363
- const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2364
- const colorPickerDefaults = {
2365
- componentLabels: colorPickerLabels,
2366
- colorLabels: colorNames,
2367
- format: 'rgb',
2368
- colorPresets: undefined,
2369
- colorKeywords: nonColors,
2370
- };
2371
-
2372
- // ColorPicker Static Methods
2373
- // ==========================
2374
-
2375
- /** @type {CP.GetInstance<ColorPicker>} */
2376
- const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
2377
-
2378
- /** @type {CP.InitCallback<ColorPicker>} */
2379
- const initColorPicker = (element) => new ColorPicker(element);
2380
-
2381
- // ColorPicker Private Methods
2382
- // ===========================
2383
-
2384
- /**
2385
- * Generate HTML markup and update instance properties.
2386
- * @param {ColorPicker} self
2387
- */
2388
- function initCallback(self) {
2339
+ * Generate HTML markup and update instance properties.
2340
+ * @param {CP.ColorPicker} self
2341
+ */
2342
+ function setMarkup(self) {
2389
2343
  const {
2390
2344
  input, parent, format, id, componentLabels, colorKeywords, colorPresets,
2391
2345
  } = self;
@@ -2400,9 +2354,7 @@
2400
2354
  self.color = new Color(color, format);
2401
2355
 
2402
2356
  // set initial controls dimensions
2403
- // make the controls smaller on mobile
2404
- const dropClass = isMobile ? ' mobile' : '';
2405
- const formatString = format === 'hex' ? hexLabel : format.toUpperCase();
2357
+ const formatString = format === 'hex' ? hexLabel : toUpperCase(format);
2406
2358
 
2407
2359
  const pickerBtn = createElement({
2408
2360
  id: `picker-btn-${id}`,
@@ -2419,7 +2371,7 @@
2419
2371
 
2420
2372
  const pickerDropdown = createElement({
2421
2373
  tagName: 'div',
2422
- className: `color-dropdown picker${dropClass}`,
2374
+ className: 'color-dropdown picker',
2423
2375
  });
2424
2376
  setAttribute(pickerDropdown, ariaLabelledBy, `picker-btn-${id}`);
2425
2377
  setAttribute(pickerDropdown, 'role', 'group');
@@ -2435,7 +2387,7 @@
2435
2387
  if (colorKeywords || colorPresets) {
2436
2388
  const presetsDropdown = createElement({
2437
2389
  tagName: 'div',
2438
- className: `color-dropdown scrollable menu${dropClass}`,
2390
+ className: 'color-dropdown scrollable menu',
2439
2391
  });
2440
2392
 
2441
2393
  // color presets
@@ -2455,7 +2407,7 @@
2455
2407
  tagName: 'button',
2456
2408
  className: 'menu-toggle btn-appearance',
2457
2409
  });
2458
- setAttribute(presetsBtn, 'tabindex', '-1');
2410
+ setAttribute(presetsBtn, tabIndex, '-1');
2459
2411
  setAttribute(presetsBtn, ariaExpanded, 'false');
2460
2412
  setAttribute(presetsBtn, ariaHasPopup, 'true');
2461
2413
 
@@ -2482,9 +2434,40 @@
2482
2434
  if (colorKeywords && nonColors.includes(colorValue)) {
2483
2435
  self.value = colorValue;
2484
2436
  }
2485
- setAttribute(input, 'tabindex', '-1');
2437
+ setAttribute(input, tabIndex, '-1');
2486
2438
  }
2487
2439
 
2440
+ var version = "0.0.2alpha1";
2441
+
2442
+ // @ts-ignore
2443
+
2444
+ const Version = version;
2445
+
2446
+ // ColorPicker GC
2447
+ // ==============
2448
+ const colorPickerString = 'color-picker';
2449
+ const colorPickerSelector = `[data-function="${colorPickerString}"]`;
2450
+ const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2451
+ const colorPickerDefaults = {
2452
+ componentLabels: colorPickerLabels,
2453
+ colorLabels: colorNames,
2454
+ format: 'rgb',
2455
+ colorPresets: false,
2456
+ colorKeywords: false,
2457
+ };
2458
+
2459
+ // ColorPicker Static Methods
2460
+ // ==========================
2461
+
2462
+ /** @type {CP.GetInstance<ColorPicker>} */
2463
+ const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
2464
+
2465
+ /** @type {CP.InitCallback<ColorPicker>} */
2466
+ const initColorPicker = (element) => new ColorPicker(element);
2467
+
2468
+ // ColorPicker Private Methods
2469
+ // ===========================
2470
+
2488
2471
  /**
2489
2472
  * Add / remove `ColorPicker` main event listeners.
2490
2473
  * @param {ColorPicker} self
@@ -2583,8 +2566,19 @@
2583
2566
  addClass(dropdown, 'bottom');
2584
2567
  reflow(dropdown);
2585
2568
  addClass(dropdown, 'show');
2569
+
2586
2570
  if (isPicker) self.update();
2587
- self.show();
2571
+
2572
+ if (!self.isOpen) {
2573
+ toggleEventsOnShown(self, true);
2574
+ self.updateDropdownPosition();
2575
+ self.isOpen = true;
2576
+ setAttribute(self.input, tabIndex, '0');
2577
+ if (menuToggle) {
2578
+ setAttribute(menuToggle, tabIndex, '0');
2579
+ }
2580
+ }
2581
+
2588
2582
  setAttribute(nextBtn, ariaExpanded, 'true');
2589
2583
  if (activeBtn) {
2590
2584
  setAttribute(activeBtn, ariaExpanded, 'false');
@@ -2707,7 +2701,7 @@
2707
2701
  self.handleKnobs = self.handleKnobs.bind(self);
2708
2702
 
2709
2703
  // generate markup
2710
- initCallback(self);
2704
+ setMarkup(self);
2711
2705
 
2712
2706
  const [colorPicker, colorMenu] = getElementsByClassName('color-dropdown', parent);
2713
2707
  // set main elements
@@ -2754,7 +2748,7 @@
2754
2748
  set value(v) { this.input.value = v; }
2755
2749
 
2756
2750
  /** Check if the colour presets include any non-colour. */
2757
- get includeNonColor() {
2751
+ get hasNonColor() {
2758
2752
  return this.colorKeywords instanceof Array
2759
2753
  && this.colorKeywords.some((x) => nonColors.includes(x));
2760
2754
  }
@@ -2810,7 +2804,7 @@
2810
2804
  const { r, g, b } = Color.hslToRgb(hue, 1, 0.5);
2811
2805
  const whiteGrad = 'linear-gradient(rgb(255,255,255) 0%, rgb(255,255,255) 100%)';
2812
2806
  const alpha = 1 - controlPositions.c3y / offsetHeight;
2813
- const roundA = Math.round((alpha * 100)) / 100;
2807
+ const roundA = roundPart((alpha * 100)) / 100;
2814
2808
 
2815
2809
  if (format !== 'hsl') {
2816
2810
  const fill = new Color({
@@ -2828,7 +2822,7 @@
2828
2822
  });
2829
2823
  setElementStyle(v2, { background: hueGradient });
2830
2824
  } else {
2831
- const saturation = Math.round((controlPositions.c2y / offsetHeight) * 100);
2825
+ const saturation = roundPart((controlPositions.c2y / offsetHeight) * 100);
2832
2826
  const fill0 = new Color({
2833
2827
  r: 255, g: 0, b: 0, a: alpha,
2834
2828
  }).saturate(-saturation).toRgbString();
@@ -2903,7 +2897,7 @@
2903
2897
  const self = this;
2904
2898
  const { activeElement } = getDocument(self.input);
2905
2899
 
2906
- if ((isMobile && self.dragElement)
2900
+ if ((e.type === touchmoveEvent && self.dragElement)
2907
2901
  || (activeElement && self.controlKnobs.includes(activeElement))) {
2908
2902
  e.stopPropagation();
2909
2903
  e.preventDefault();
@@ -2983,12 +2977,12 @@
2983
2977
 
2984
2978
  self.update();
2985
2979
 
2986
- if (currentActive) {
2987
- removeClass(currentActive, 'active');
2988
- removeAttribute(currentActive, ariaSelected);
2989
- }
2990
-
2991
2980
  if (currentActive !== target) {
2981
+ if (currentActive) {
2982
+ removeClass(currentActive, 'active');
2983
+ removeAttribute(currentActive, ariaSelected);
2984
+ }
2985
+
2992
2986
  addClass(target, 'active');
2993
2987
  setAttribute(target, ariaSelected, 'true');
2994
2988
 
@@ -3110,30 +3104,41 @@
3110
3104
  if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
3111
3105
  e.preventDefault();
3112
3106
 
3113
- const { controlKnobs } = self;
3107
+ const { format, controlKnobs, visuals } = self;
3108
+ const { offsetWidth, offsetHeight } = visuals[0];
3114
3109
  const [c1, c2, c3] = controlKnobs;
3115
3110
  const { activeElement } = getDocument(c1);
3116
3111
  const currentKnob = controlKnobs.find((x) => x === activeElement);
3112
+ const yRatio = offsetHeight / (format === 'hsl' ? 100 : 360);
3117
3113
 
3118
3114
  if (currentKnob) {
3119
3115
  let offsetX = 0;
3120
3116
  let offsetY = 0;
3117
+
3121
3118
  if (target === c1) {
3119
+ const xRatio = offsetWidth / (format === 'hsl' ? 360 : 100);
3120
+
3122
3121
  if ([keyArrowLeft, keyArrowRight].includes(code)) {
3123
- self.controlPositions.c1x += code === keyArrowRight ? +1 : -1;
3122
+ self.controlPositions.c1x += code === keyArrowRight ? xRatio : -xRatio;
3124
3123
  } else if ([keyArrowUp, keyArrowDown].includes(code)) {
3125
- self.controlPositions.c1y += code === keyArrowDown ? +1 : -1;
3124
+ self.controlPositions.c1y += code === keyArrowDown ? yRatio : -yRatio;
3126
3125
  }
3127
3126
 
3128
3127
  offsetX = self.controlPositions.c1x;
3129
3128
  offsetY = self.controlPositions.c1y;
3130
3129
  self.changeControl1(offsetX, offsetY);
3131
3130
  } else if (target === c2) {
3132
- self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3131
+ self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code)
3132
+ ? yRatio
3133
+ : -yRatio;
3134
+
3133
3135
  offsetY = self.controlPositions.c2y;
3134
3136
  self.changeControl2(offsetY);
3135
3137
  } else if (target === c3) {
3136
- self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3138
+ self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code)
3139
+ ? yRatio
3140
+ : -yRatio;
3141
+
3137
3142
  offsetY = self.controlPositions.c3y;
3138
3143
  self.changeAlpha(offsetY);
3139
3144
  }
@@ -3155,7 +3160,7 @@
3155
3160
  const [v1, v2, v3, v4] = format === 'rgb'
3156
3161
  ? inputs.map((i) => parseFloat(i.value) / (i === i4 ? 100 : 1))
3157
3162
  : inputs.map((i) => parseFloat(i.value) / (i !== i1 ? 100 : 360));
3158
- const isNonColorValue = self.includeNonColor && nonColors.includes(currentValue);
3163
+ const isNonColorValue = self.hasNonColor && nonColors.includes(currentValue);
3159
3164
  const alpha = i4 ? v4 : (1 - controlPositions.c3y / offsetHeight);
3160
3165
 
3161
3166
  if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
@@ -3414,11 +3419,11 @@
3414
3419
  } = componentLabels;
3415
3420
  const { r, g, b } = color.toRgb();
3416
3421
  const [knob1, knob2, knob3] = controlKnobs;
3417
- const hue = Math.round(hsl.h * 360);
3422
+ const hue = roundPart(hsl.h * 360);
3418
3423
  const alpha = color.a;
3419
3424
  const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
3420
- const saturation = Math.round(saturationSource * 100);
3421
- const lightness = Math.round(hsl.l * 100);
3425
+ const saturation = roundPart(saturationSource * 100);
3426
+ const lightness = roundPart(hsl.l * 100);
3422
3427
  const hsvl = hsv.v * 100;
3423
3428
  let colorName;
3424
3429
 
@@ -3465,8 +3470,8 @@
3465
3470
  setAttribute(knob2, ariaValueNow, `${saturation}`);
3466
3471
  } else if (format === 'hwb') {
3467
3472
  const { hwb } = self;
3468
- const whiteness = Math.round(hwb.w * 100);
3469
- const blackness = Math.round(hwb.b * 100);
3473
+ const whiteness = roundPart(hwb.w * 100);
3474
+ const blackness = roundPart(hwb.b * 100);
3470
3475
  colorLabel = `HWB: ${hue}°, ${whiteness}%, ${blackness}%`;
3471
3476
  setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3472
3477
  setAttribute(knob1, ariaValueText, `${whiteness}% & ${blackness}%`);
@@ -3482,7 +3487,7 @@
3482
3487
  setAttribute(knob2, ariaValueNow, `${hue}`);
3483
3488
  }
3484
3489
 
3485
- const alphaValue = Math.round(alpha * 100);
3490
+ const alphaValue = roundPart(alpha * 100);
3486
3491
  setAttribute(knob3, ariaValueText, `${alphaValue}%`);
3487
3492
  setAttribute(knob3, ariaValueNow, `${alphaValue}`);
3488
3493
 
@@ -3505,10 +3510,16 @@
3505
3510
  /** Updates the control knobs actual positions. */
3506
3511
  updateControls() {
3507
3512
  const { controlKnobs, controlPositions } = this;
3513
+ let {
3514
+ c1x, c1y, c2y, c3y,
3515
+ } = controlPositions;
3508
3516
  const [control1, control2, control3] = controlKnobs;
3509
- setElementStyle(control1, { transform: `translate3d(${controlPositions.c1x - 4}px,${controlPositions.c1y - 4}px,0)` });
3510
- setElementStyle(control2, { transform: `translate3d(0,${controlPositions.c2y - 4}px,0)` });
3511
- setElementStyle(control3, { transform: `translate3d(0,${controlPositions.c3y - 4}px,0)` });
3517
+ // round control positions
3518
+ [c1x, c1y, c2y, c3y] = [c1x, c1y, c2y, c3y].map(roundPart);
3519
+
3520
+ setElementStyle(control1, { transform: `translate3d(${c1x - 4}px,${c1y - 4}px,0)` });
3521
+ setElementStyle(control2, { transform: `translate3d(0,${c2y - 4}px,0)` });
3522
+ setElementStyle(control3, { transform: `translate3d(0,${c3y - 4}px,0)` });
3512
3523
  }
3513
3524
 
3514
3525
  /**
@@ -3521,16 +3532,16 @@
3521
3532
  value: oldColor, format, inputs, color, hsl,
3522
3533
  } = self;
3523
3534
  const [i1, i2, i3, i4] = inputs;
3524
- const alpha = Math.round(color.a * 100);
3525
- const hue = Math.round(hsl.h * 360);
3535
+ const alpha = roundPart(color.a * 100);
3536
+ const hue = roundPart(hsl.h * 360);
3526
3537
  let newColor;
3527
3538
 
3528
3539
  if (format === 'hex') {
3529
3540
  newColor = self.color.toHexString(true);
3530
3541
  i1.value = self.hex;
3531
3542
  } else if (format === 'hsl') {
3532
- const lightness = Math.round(hsl.l * 100);
3533
- const saturation = Math.round(hsl.s * 100);
3543
+ const lightness = roundPart(hsl.l * 100);
3544
+ const saturation = roundPart(hsl.s * 100);
3534
3545
  newColor = self.color.toHslString();
3535
3546
  i1.value = `${hue}`;
3536
3547
  i2.value = `${saturation}`;
@@ -3538,8 +3549,8 @@
3538
3549
  i4.value = `${alpha}`;
3539
3550
  } else if (format === 'hwb') {
3540
3551
  const { w, b } = self.hwb;
3541
- const whiteness = Math.round(w * 100);
3542
- const blackness = Math.round(b * 100);
3552
+ const whiteness = roundPart(w * 100);
3553
+ const blackness = roundPart(b * 100);
3543
3554
 
3544
3555
  newColor = self.color.toHwbString();
3545
3556
  i1.value = `${hue}`;
@@ -3547,7 +3558,8 @@
3547
3558
  i3.value = `${blackness}`;
3548
3559
  i4.value = `${alpha}`;
3549
3560
  } else if (format === 'rgb') {
3550
- const { r, g, b } = self.rgb;
3561
+ let { r, g, b } = self.rgb;
3562
+ [r, g, b] = [r, g, b].map(roundPart);
3551
3563
 
3552
3564
  newColor = self.color.toRgbString();
3553
3565
  i1.value = `${r}`;
@@ -3611,7 +3623,7 @@
3611
3623
  const self = this;
3612
3624
  const { colorPicker } = self;
3613
3625
 
3614
- if (!hasClass(colorPicker, 'show')) {
3626
+ if (!['top', 'bottom'].some((c) => hasClass(colorPicker, c))) {
3615
3627
  showDropdown(self, colorPicker);
3616
3628
  }
3617
3629
  }
@@ -3628,21 +3640,6 @@
3628
3640
  }
3629
3641
  }
3630
3642
 
3631
- /** Shows the `ColorPicker` dropdown or the presets menu. */
3632
- show() {
3633
- const self = this;
3634
- const { menuToggle } = self;
3635
- if (!self.isOpen) {
3636
- toggleEventsOnShown(self, true);
3637
- self.updateDropdownPosition();
3638
- self.isOpen = true;
3639
- setAttribute(self.input, 'tabindex', '0');
3640
- if (menuToggle) {
3641
- setAttribute(menuToggle, 'tabindex', '0');
3642
- }
3643
- }
3644
- }
3645
-
3646
3643
  /**
3647
3644
  * Hides the currently open `ColorPicker` dropdown.
3648
3645
  * @param {boolean=} focusPrevented
@@ -3677,9 +3674,9 @@
3677
3674
  if (!focusPrevented) {
3678
3675
  focus(pickerToggle);
3679
3676
  }
3680
- setAttribute(input, 'tabindex', '-1');
3677
+ setAttribute(input, tabIndex, '-1');
3681
3678
  if (menuToggle) {
3682
- setAttribute(menuToggle, 'tabindex', '-1');
3679
+ setAttribute(menuToggle, tabIndex, '-1');
3683
3680
  }
3684
3681
  }
3685
3682
  }
@@ -3693,7 +3690,10 @@
3693
3690
  [...parent.children].forEach((el) => {
3694
3691
  if (el !== input) el.remove();
3695
3692
  });
3693
+
3694
+ removeAttribute(input, tabIndex);
3696
3695
  setElementStyle(input, { backgroundColor: '' });
3696
+
3697
3697
  ['txt-light', 'txt-dark'].forEach((c) => removeClass(parent, c));
3698
3698
  Data.remove(input, colorPickerString);
3699
3699
  }
@@ -3701,12 +3701,18 @@
3701
3701
 
3702
3702
  ObjectAssign(ColorPicker, {
3703
3703
  Color,
3704
+ ColorPalette,
3704
3705
  Version,
3705
3706
  getInstance: getColorPickerInstance,
3706
3707
  init: initColorPicker,
3707
3708
  selector: colorPickerSelector,
3709
+ // utils important for render
3710
+ roundPart,
3711
+ setElementStyle,
3712
+ setAttribute,
3713
+ getBoundingClientRect,
3708
3714
  });
3709
3715
 
3710
3716
  return ColorPicker;
3711
3717
 
3712
- })));
3718
+ }));