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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +32 -15
  2. package/dist/css/color-picker.css +38 -15
  3. package/dist/css/color-picker.min.css +2 -2
  4. package/dist/css/color-picker.rtl.css +38 -15
  5. package/dist/css/color-picker.rtl.min.css +2 -2
  6. package/dist/js/color-esm.js +1178 -0
  7. package/dist/js/color-esm.min.js +2 -0
  8. package/dist/js/color-palette-esm.js +1252 -0
  9. package/dist/js/color-palette-esm.min.js +2 -0
  10. package/dist/js/color-palette.js +1260 -0
  11. package/dist/js/color-palette.min.js +2 -0
  12. package/dist/js/color-picker-element-esm.js +433 -424
  13. package/dist/js/color-picker-element-esm.min.js +2 -2
  14. package/dist/js/color-picker-element.js +435 -426
  15. package/dist/js/color-picker-element.min.js +2 -2
  16. package/dist/js/color-picker-esm.js +745 -739
  17. package/dist/js/color-picker-esm.min.js +2 -2
  18. package/dist/js/color-picker.js +747 -741
  19. package/dist/js/color-picker.min.js +2 -2
  20. package/dist/js/color.js +1186 -0
  21. package/dist/js/color.min.js +2 -0
  22. package/package.json +19 -3
  23. package/src/js/color-palette.js +28 -12
  24. package/src/js/color-picker-element.js +8 -4
  25. package/src/js/color-picker.js +84 -172
  26. package/src/js/color.js +125 -131
  27. package/src/js/util/getColorControls.js +3 -3
  28. package/src/js/util/getColorForm.js +0 -1
  29. package/src/js/util/getColorMenu.js +31 -33
  30. package/src/js/util/roundPart.js +9 -0
  31. package/src/js/util/setCSSProperties.js +12 -0
  32. package/src/js/util/setMarkup.js +122 -0
  33. package/src/js/util/tabindex.js +3 -0
  34. package/src/js/util/version.js +6 -0
  35. package/src/scss/color-picker.scss +35 -16
  36. package/types/cp.d.ts +48 -20
  37. package/src/js/util/templates.js +0 -10
@@ -1,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
+ }));