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

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.
@@ -1,13 +1,13 @@
1
1
  /*!
2
- * ColorPickerElement v0.0.1alpha3 (http://thednp.github.io/color-picker)
2
+ * ColorPickerElement v0.0.2alpha2 (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.ColorPickerElement = factory());
10
- }(this, (function () { 'use strict';
9
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.ColorPickerElement = factory());
10
+ })(this, (function () { 'use strict';
11
11
 
12
12
  /**
13
13
  * Returns the `document` or the `#document` element.
@@ -88,14 +88,9 @@
88
88
  const getAttribute = (element, attribute) => element.getAttribute(attribute);
89
89
 
90
90
  /**
91
- * Returns the `document.head` or the `<head>` element.
92
- *
93
- * @param {(Node | HTMLElement | Element | globalThis)=} node
94
- * @returns {HTMLElement | HTMLHeadElement}
91
+ * A global namespace for `document.head`.
95
92
  */
96
- function getDocumentHead(node) {
97
- return getDocument(node).head;
98
- }
93
+ const { head: documentHead } = document;
99
94
 
100
95
  /**
101
96
  * Shortcut for `window.getComputedStyle(element).propertyName`
@@ -116,20 +111,21 @@
116
111
  return property in computedStyle ? computedStyle[property] : '';
117
112
  }
118
113
 
119
- /**
120
- * Shortcut for `Object.keys()` static method.
121
- * @param {Record<string, any>} obj a target object
122
- * @returns {string[]}
123
- */
124
- const ObjectKeys = (obj) => Object.keys(obj);
125
-
126
114
  /**
127
115
  * Shortcut for multiple uses of `HTMLElement.style.propertyName` method.
128
116
  * @param {HTMLElement | Element} element target element
129
117
  * @param {Partial<CSSStyleDeclaration>} styles attribute value
130
118
  */
131
119
  // @ts-ignore
132
- const setElementStyle = (element, styles) => ObjectAssign(element.style, styles);
120
+ const setElementStyle = (element, styles) => { ObjectAssign(element.style, styles); };
121
+
122
+ /**
123
+ * Shortcut for `String.toLowerCase()`.
124
+ *
125
+ * @param {string} source input string
126
+ * @returns {string} lowercase output string
127
+ */
128
+ const toLowerCase = (source) => source.toLowerCase();
133
129
 
134
130
  /**
135
131
  * A list of explicit default non-color values.
@@ -147,7 +143,7 @@
147
143
  }
148
144
 
149
145
  // Color supported formats
150
- const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsb', 'hwb'];
146
+ const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsv', 'hwb'];
151
147
 
152
148
  // Hue angles
153
149
  const ANGLES = 'deg|rad|grad|turn';
@@ -169,10 +165,17 @@
169
165
  // Add angles to the mix
170
166
  const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
171
167
 
168
+ // Start & end
169
+ const START_MATCH = '(?:[\\s|\\(\\s|\\s\\(\\s]+)?';
170
+ const END_MATCH = '(?:[\\s|\\)\\s]+)?';
171
+ // Components separation
172
+ const SEP = '(?:[,|\\s]+)';
173
+ const SEP2 = '(?:[,|\\/\\s]*)?';
174
+
172
175
  // Actual matching.
173
176
  // Parentheses and commas are optional, but not required.
174
177
  // Whitespace can take the place of commas or opening paren
175
- const PERMISSIVE_MATCH = `[\\s|\\(]+(${CSS_UNIT2})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s|\\/\\s]*(${CSS_UNIT})?\\s*\\)?`;
178
+ const PERMISSIVE_MATCH = `${START_MATCH}(${CSS_UNIT2})${SEP}(${CSS_UNIT})${SEP}(${CSS_UNIT})${SEP2}(${CSS_UNIT})?${END_MATCH}`;
176
179
 
177
180
  const matchers = {
178
181
  CSS_UNIT: new RegExp(CSS_UNIT2),
@@ -205,23 +208,24 @@
205
208
  return `${n}`.includes('%');
206
209
  }
207
210
 
208
- /**
209
- * Check to see if string passed in is an angle
210
- * @param {string} n testing string
211
- * @returns {boolean} the query result
212
- */
213
- function isAngle(n) {
214
- return ANGLES.split('|').some((a) => `${n}`.includes(a));
215
- }
216
-
217
211
  /**
218
212
  * Check to see if string passed is a web safe colour.
213
+ * @see https://stackoverflow.com/a/16994164
219
214
  * @param {string} color a colour name, EG: *red*
220
215
  * @returns {boolean} the query result
221
216
  */
222
217
  function isColorName(color) {
223
- return !['#', ...COLOR_FORMAT].some((s) => color.includes(s))
224
- && !/[0-9]/.test(color);
218
+ if (nonColors.includes(color)
219
+ || ['#', ...COLOR_FORMAT].some((f) => color.includes(f))) return false;
220
+
221
+ if (['black', 'white'].includes(color)) return true;
222
+
223
+ return ['rgb(255, 255, 255)', 'rgb(0, 0, 0)'].every((c) => {
224
+ setElementStyle(documentHead, { color });
225
+ const computedColor = getElementStyle(documentHead, 'color');
226
+ setElementStyle(documentHead, { color: '' });
227
+ return computedColor !== c;
228
+ });
225
229
  }
226
230
 
227
231
  /**
@@ -242,15 +246,20 @@
242
246
  */
243
247
  function bound01(N, max) {
244
248
  let n = N;
245
- if (isOnePointZero(n)) n = '100%';
246
249
 
247
- n = max === 360 ? n : Math.min(max, Math.max(0, parseFloat(n)));
250
+ if (typeof N === 'number'
251
+ && Math.min(N, 0) === 0 // round values to 6 decimals Math.round(N * (10 ** 6)) / 10 ** 6
252
+ && Math.max(N, 1) === 1) return N;
253
+
254
+ if (isOnePointZero(N)) n = '100%';
248
255
 
249
- // Handle hue angles
250
- if (isAngle(N)) n = N.replace(new RegExp(ANGLES), '');
256
+ const processPercent = isPercentage(n);
257
+ n = max === 360
258
+ ? parseFloat(n)
259
+ : Math.min(max, Math.max(0, parseFloat(n)));
251
260
 
252
261
  // Automatically convert percentage into number
253
- if (isPercentage(n)) n = parseInt(String(n * max), 10) / 100;
262
+ if (processPercent) n = (n * max) / 100;
254
263
 
255
264
  // Handle floating point rounding errors
256
265
  if (Math.abs(n - max) < 0.000001) {
@@ -261,11 +270,11 @@
261
270
  // If n is a hue given in degrees,
262
271
  // wrap around out-of-range values into [0, 360] range
263
272
  // then convert into [0, 1].
264
- n = (n < 0 ? (n % max) + max : n % max) / parseFloat(String(max));
273
+ n = (n < 0 ? (n % max) + max : n % max) / max;
265
274
  } else {
266
275
  // If n not a hue given in degrees
267
276
  // Convert into [0, 1] range if it isn't already.
268
- n = (n % max) / parseFloat(String(max));
277
+ n = (n % max) / max;
269
278
  }
270
279
  return n;
271
280
  }
@@ -300,7 +309,6 @@
300
309
  * @returns {string}
301
310
  */
302
311
  function getRGBFromName(name) {
303
- const documentHead = getDocumentHead();
304
312
  setElementStyle(documentHead, { color: name });
305
313
  const colorName = getElementStyle(documentHead, 'color');
306
314
  setElementStyle(documentHead, { color: '' });
@@ -346,15 +354,12 @@
346
354
  /**
347
355
  * Converts an RGB colour value to HSL.
348
356
  *
349
- * @param {number} R Red component [0, 255]
350
- * @param {number} G Green component [0, 255]
351
- * @param {number} B Blue component [0, 255]
357
+ * @param {number} r Red component [0, 1]
358
+ * @param {number} g Green component [0, 1]
359
+ * @param {number} b Blue component [0, 1]
352
360
  * @returns {CP.HSL} {h,s,l} object with [0, 1] ranged values
353
361
  */
354
- function rgbToHsl(R, G, B) {
355
- const r = R / 255;
356
- const g = G / 255;
357
- const b = B / 255;
362
+ function rgbToHsl(r, g, b) {
358
363
  const max = Math.max(r, g, b);
359
364
  const min = Math.min(r, g, b);
360
365
  let h = 0;
@@ -366,17 +371,10 @@
366
371
  } else {
367
372
  const d = max - min;
368
373
  s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
369
- switch (max) {
370
- case r:
371
- h = (g - b) / d + (g < b ? 6 : 0);
372
- break;
373
- case g:
374
- h = (b - r) / d + 2;
375
- break;
376
- case b:
377
- h = (r - g) / d + 4;
378
- break;
379
- }
374
+ if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
375
+ if (max === g) h = (b - r) / d + 2;
376
+ if (max === b) h = (r - g) / d + 4;
377
+
380
378
  h /= 6;
381
379
  }
382
380
  return { h, s, l };
@@ -399,21 +397,46 @@
399
397
  return p;
400
398
  }
401
399
 
400
+ /**
401
+ * Converts an HSL colour value to RGB.
402
+ *
403
+ * @param {number} h Hue Angle [0, 1]
404
+ * @param {number} s Saturation [0, 1]
405
+ * @param {number} l Lightness Angle [0, 1]
406
+ * @returns {CP.RGB} {r,g,b} object with [0, 1] ranged values
407
+ */
408
+ function hslToRgb(h, s, l) {
409
+ let r = 0;
410
+ let g = 0;
411
+ let b = 0;
412
+
413
+ if (s === 0) {
414
+ // achromatic
415
+ g = l;
416
+ b = l;
417
+ r = l;
418
+ } else {
419
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
420
+ const p = 2 * l - q;
421
+ r = hueToRgb(p, q, h + 1 / 3);
422
+ g = hueToRgb(p, q, h);
423
+ b = hueToRgb(p, q, h - 1 / 3);
424
+ }
425
+
426
+ return { r, g, b };
427
+ }
428
+
402
429
  /**
403
430
  * Returns an HWB colour object from an RGB colour object.
404
431
  * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
405
432
  * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
406
433
  *
407
- * @param {number} R Red component [0, 255]
408
- * @param {number} G Green [0, 255]
409
- * @param {number} B Blue [0, 255]
434
+ * @param {number} r Red component [0, 1]
435
+ * @param {number} g Green [0, 1]
436
+ * @param {number} b Blue [0, 1]
410
437
  * @return {CP.HWB} {h,w,b} object with [0, 1] ranged values
411
438
  */
412
- function rgbToHwb(R, G, B) {
413
- const r = R / 255;
414
- const g = G / 255;
415
- const b = B / 255;
416
-
439
+ function rgbToHwb(r, g, b) {
417
440
  let f = 0;
418
441
  let i = 0;
419
442
  const whiteness = Math.min(r, g, b);
@@ -443,50 +466,18 @@
443
466
  * @param {number} H Hue Angle [0, 1]
444
467
  * @param {number} W Whiteness [0, 1]
445
468
  * @param {number} B Blackness [0, 1]
446
- * @return {CP.RGB} {r,g,b} object with [0, 255] ranged values
469
+ * @return {CP.RGB} {r,g,b} object with [0, 1] ranged values
447
470
  *
448
471
  * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
449
472
  * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
450
473
  */
451
474
  function hwbToRgb(H, W, B) {
452
475
  if (W + B >= 1) {
453
- const gray = (W / (W + B)) * 255;
476
+ const gray = W / (W + B);
454
477
  return { r: gray, g: gray, b: gray };
455
478
  }
456
479
  let { r, g, b } = hslToRgb(H, 1, 0.5);
457
- [r, g, b] = [r, g, b]
458
- .map((v) => (v / 255) * (1 - W - B) + W)
459
- .map((v) => v * 255);
460
-
461
- return { r, g, b };
462
- }
463
-
464
- /**
465
- * Converts an HSL colour value to RGB.
466
- *
467
- * @param {number} h Hue Angle [0, 1]
468
- * @param {number} s Saturation [0, 1]
469
- * @param {number} l Lightness Angle [0, 1]
470
- * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
471
- */
472
- function hslToRgb(h, s, l) {
473
- let r = 0;
474
- let g = 0;
475
- let b = 0;
476
-
477
- if (s === 0) {
478
- // achromatic
479
- g = l;
480
- b = l;
481
- r = l;
482
- } else {
483
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
484
- const p = 2 * l - q;
485
- r = hueToRgb(p, q, h + 1 / 3);
486
- g = hueToRgb(p, q, h);
487
- b = hueToRgb(p, q, h - 1 / 3);
488
- }
489
- [r, g, b] = [r, g, b].map((x) => x * 255);
480
+ [r, g, b] = [r, g, b].map((v) => v * (1 - W - B) + W);
490
481
 
491
482
  return { r, g, b };
492
483
  }
@@ -494,15 +485,12 @@
494
485
  /**
495
486
  * Converts an RGB colour value to HSV.
496
487
  *
497
- * @param {number} R Red component [0, 255]
498
- * @param {number} G Green [0, 255]
499
- * @param {number} B Blue [0, 255]
488
+ * @param {number} r Red component [0, 1]
489
+ * @param {number} g Green [0, 1]
490
+ * @param {number} b Blue [0, 1]
500
491
  * @returns {CP.HSV} {h,s,v} object with [0, 1] ranged values
501
492
  */
502
- function rgbToHsv(R, G, B) {
503
- const r = R / 255;
504
- const g = G / 255;
505
- const b = B / 255;
493
+ function rgbToHsv(r, g, b) {
506
494
  const max = Math.max(r, g, b);
507
495
  const min = Math.min(r, g, b);
508
496
  let h = 0;
@@ -512,17 +500,10 @@
512
500
  if (max === min) {
513
501
  h = 0; // achromatic
514
502
  } else {
515
- switch (max) {
516
- case r:
517
- h = (g - b) / d + (g < b ? 6 : 0);
518
- break;
519
- case g:
520
- h = (b - r) / d + 2;
521
- break;
522
- case b:
523
- h = (r - g) / d + 4;
524
- break;
525
- }
503
+ if (r === max) h = (g - b) / d + (g < b ? 6 : 0);
504
+ if (g === max) h = (b - r) / d + 2;
505
+ if (b === max) h = (r - g) / d + 4;
506
+
526
507
  h /= 6;
527
508
  }
528
509
  return { h, s, v };
@@ -549,7 +530,7 @@
549
530
  const r = [v, q, p, p, t, v][mod];
550
531
  const g = [t, v, v, q, p, p][mod];
551
532
  const b = [p, p, t, v, v, q][mod];
552
- return { r: r * 255, g: g * 255, b: b * 255 };
533
+ return { r, g, b };
553
534
  }
554
535
 
555
536
  /**
@@ -573,7 +554,7 @@
573
554
  // Return a 3 character hex if possible
574
555
  if (allow3Char && hex[0].charAt(0) === hex[0].charAt(1)
575
556
  && hex[1].charAt(0) === hex[1].charAt(1)
576
- && hex[2].charAt(0) === hex[2].charAt(1)) {
557
+ && hex[2].charAt(0) === hex[2].charAt(1)) {
577
558
  return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
578
559
  }
579
560
 
@@ -601,51 +582,34 @@
601
582
  // Return a 4 character hex if possible
602
583
  if (allow4Char && hex[0].charAt(0) === hex[0].charAt(1)
603
584
  && hex[1].charAt(0) === hex[1].charAt(1)
604
- && hex[2].charAt(0) === hex[2].charAt(1)
605
- && hex[3].charAt(0) === hex[3].charAt(1)) {
585
+ && hex[2].charAt(0) === hex[2].charAt(1)
586
+ && hex[3].charAt(0) === hex[3].charAt(1)) {
606
587
  return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
607
588
  }
608
589
  return hex.join('');
609
590
  }
610
591
 
611
- /**
612
- * Returns a colour object corresponding to a given number.
613
- * @param {number} color input number
614
- * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
615
- */
616
- function numberInputToObject(color) {
617
- /* eslint-disable no-bitwise */
618
- return {
619
- r: color >> 16,
620
- g: (color & 0xff00) >> 8,
621
- b: color & 0xff,
622
- };
623
- /* eslint-enable no-bitwise */
624
- }
625
-
626
592
  /**
627
593
  * Permissive string parsing. Take in a number of formats, and output an object
628
594
  * based on detected format. Returns {r,g,b} or {h,s,l} or {h,s,v}
629
595
  * @param {string} input colour value in any format
630
- * @returns {Record<string, (number | string)> | false} an object matching the RegExp
596
+ * @returns {Record<string, (number | string | boolean)> | false} an object matching the RegExp
631
597
  */
632
598
  function stringInputToObject(input) {
633
- let color = input.trim().toLowerCase();
599
+ let color = toLowerCase(input.trim());
600
+
634
601
  if (color.length === 0) {
635
602
  return {
636
- r: 0, g: 0, b: 0, a: 0,
603
+ r: 0, g: 0, b: 0, a: 1,
637
604
  };
638
605
  }
639
- let named = false;
606
+
640
607
  if (isColorName(color)) {
641
608
  color = getRGBFromName(color);
642
- named = true;
643
609
  } else if (nonColors.includes(color)) {
644
- const isTransparent = color === 'transparent';
645
- const rgb = isTransparent ? 0 : 255;
646
- const a = isTransparent ? 0 : 1;
610
+ const a = color === 'transparent' ? 0 : 1;
647
611
  return {
648
- r: rgb, g: rgb, b: rgb, a, format: 'rgb',
612
+ r: 0, g: 0, b: 0, a, format: 'rgb', ok: true,
649
613
  };
650
614
  }
651
615
 
@@ -660,24 +624,28 @@
660
624
  r: m1, g: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'rgb',
661
625
  };
662
626
  }
627
+
663
628
  [, m1, m2, m3, m4] = matchers.hsl.exec(color) || [];
664
629
  if (m1 && m2 && m3/* && m4 */) {
665
630
  return {
666
631
  h: m1, s: m2, l: m3, a: m4 !== undefined ? m4 : 1, format: 'hsl',
667
632
  };
668
633
  }
634
+
669
635
  [, m1, m2, m3, m4] = matchers.hsv.exec(color) || [];
670
636
  if (m1 && m2 && m3/* && m4 */) {
671
637
  return {
672
638
  h: m1, s: m2, v: m3, a: m4 !== undefined ? m4 : 1, format: 'hsv',
673
639
  };
674
640
  }
641
+
675
642
  [, m1, m2, m3, m4] = matchers.hwb.exec(color) || [];
676
643
  if (m1 && m2 && m3) {
677
644
  return {
678
645
  h: m1, w: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'hwb',
679
646
  };
680
647
  }
648
+
681
649
  [, m1, m2, m3, m4] = matchers.hex8.exec(color) || [];
682
650
  if (m1 && m2 && m3 && m4) {
683
651
  return {
@@ -685,19 +653,20 @@
685
653
  g: parseIntFromHex(m2),
686
654
  b: parseIntFromHex(m3),
687
655
  a: convertHexToDecimal(m4),
688
- // format: named ? 'rgb' : 'hex8',
689
- format: named ? 'rgb' : 'hex',
656
+ format: 'hex',
690
657
  };
691
658
  }
659
+
692
660
  [, m1, m2, m3] = matchers.hex6.exec(color) || [];
693
661
  if (m1 && m2 && m3) {
694
662
  return {
695
663
  r: parseIntFromHex(m1),
696
664
  g: parseIntFromHex(m2),
697
665
  b: parseIntFromHex(m3),
698
- format: named ? 'rgb' : 'hex',
666
+ format: 'hex',
699
667
  };
700
668
  }
669
+
701
670
  [, m1, m2, m3, m4] = matchers.hex4.exec(color) || [];
702
671
  if (m1 && m2 && m3 && m4) {
703
672
  return {
@@ -705,19 +674,20 @@
705
674
  g: parseIntFromHex(m2 + m2),
706
675
  b: parseIntFromHex(m3 + m3),
707
676
  a: convertHexToDecimal(m4 + m4),
708
- // format: named ? 'rgb' : 'hex8',
709
- format: named ? 'rgb' : 'hex',
677
+ format: 'hex',
710
678
  };
711
679
  }
680
+
712
681
  [, m1, m2, m3] = matchers.hex3.exec(color) || [];
713
682
  if (m1 && m2 && m3) {
714
683
  return {
715
684
  r: parseIntFromHex(m1 + m1),
716
685
  g: parseIntFromHex(m2 + m2),
717
686
  b: parseIntFromHex(m3 + m3),
718
- format: named ? 'rgb' : 'hex',
687
+ format: 'hex',
719
688
  };
720
689
  }
690
+
721
691
  return false;
722
692
  }
723
693
 
@@ -748,7 +718,9 @@
748
718
  */
749
719
  function inputToRGB(input) {
750
720
  let rgb = { r: 0, g: 0, b: 0 };
721
+ /** @type {*} */
751
722
  let color = input;
723
+ /** @type {string | number} */
752
724
  let a = 1;
753
725
  let s = null;
754
726
  let v = null;
@@ -759,58 +731,67 @@
759
731
  let r = null;
760
732
  let g = null;
761
733
  let ok = false;
762
- let format = 'hex';
734
+ const inputFormat = typeof color === 'object' && color.format;
735
+ let format = inputFormat && COLOR_FORMAT.includes(inputFormat) ? inputFormat : 'rgb';
763
736
 
764
737
  if (typeof input === 'string') {
765
- // @ts-ignore -- this now is converted to object
766
738
  color = stringInputToObject(input);
767
739
  if (color) ok = true;
768
740
  }
769
741
  if (typeof color === 'object') {
770
742
  if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
771
743
  ({ r, g, b } = color);
772
- [r, g, b] = [...[r, g, b]]
773
- .map((n) => bound01(n, isPercentage(n) ? 100 : 255) * 255).map(roundPart);
774
- rgb = { r, g, b }; // RGB values now are all in [0, 255] range
744
+ // RGB values now are all in [0, 1] range
745
+ [r, g, b] = [r, g, b].map((n) => bound01(n, isPercentage(n) ? 100 : 255));
746
+ rgb = { r, g, b };
775
747
  ok = true;
776
- format = 'rgb';
777
- } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
748
+ format = color.format || 'rgb';
749
+ }
750
+ if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
778
751
  ({ h, s, v } = color);
779
- h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
780
- s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
781
- v = typeof v === 'number' ? v : bound01(v, 100); // brightness can be `5%` or a [0, 1] value
752
+ h = bound01(h, 360); // hue can be `5deg` or a [0, 1] value
753
+ s = bound01(s, 100); // saturation can be `5%` or a [0, 1] value
754
+ v = bound01(v, 100); // brightness can be `5%` or a [0, 1] value
782
755
  rgb = hsvToRgb(h, s, v);
783
756
  ok = true;
784
757
  format = 'hsv';
785
- } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
758
+ }
759
+ if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
786
760
  ({ h, s, l } = color);
787
- h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
788
- s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
789
- l = typeof l === 'number' ? l : bound01(l, 100); // lightness can be `5%` or a [0, 1] value
761
+ h = bound01(h, 360); // hue can be `5deg` or a [0, 1] value
762
+ s = bound01(s, 100); // saturation can be `5%` or a [0, 1] value
763
+ l = bound01(l, 100); // lightness can be `5%` or a [0, 1] value
790
764
  rgb = hslToRgb(h, s, l);
791
765
  ok = true;
792
766
  format = 'hsl';
793
- } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.w) && isValidCSSUnit(color.b)) {
767
+ }
768
+ if (isValidCSSUnit(color.h) && isValidCSSUnit(color.w) && isValidCSSUnit(color.b)) {
794
769
  ({ h, w, b } = color);
795
- h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
796
- w = typeof w === 'number' ? w : bound01(w, 100); // whiteness can be `5%` or a [0, 1] value
797
- b = typeof b === 'number' ? b : bound01(b, 100); // blackness can be `5%` or a [0, 1] value
770
+ h = bound01(h, 360); // hue can be `5deg` or a [0, 1] value
771
+ w = bound01(w, 100); // whiteness can be `5%` or a [0, 1] value
772
+ b = bound01(b, 100); // blackness can be `5%` or a [0, 1] value
798
773
  rgb = hwbToRgb(h, w, b);
799
774
  ok = true;
800
775
  format = 'hwb';
801
776
  }
802
777
  if (isValidCSSUnit(color.a)) {
803
- a = color.a;
804
- a = isPercentage(`${a}`) ? bound01(a, 100) : a;
778
+ a = color.a; // @ts-ignore -- `parseFloat` works with numbers too
779
+ a = isPercentage(`${a}`) || parseFloat(a) > 1 ? bound01(a, 100) : a;
805
780
  }
806
781
  }
782
+ if (typeof color === 'undefined') {
783
+ ok = true;
784
+ }
807
785
 
808
786
  return {
809
- ok, // @ts-ignore
810
- format: color.format || format,
811
- r: Math.min(255, Math.max(rgb.r, 0)),
812
- g: Math.min(255, Math.max(rgb.g, 0)),
813
- b: Math.min(255, Math.max(rgb.b, 0)),
787
+ ok,
788
+ format,
789
+ // r: Math.min(255, Math.max(rgb.r, 0)),
790
+ // g: Math.min(255, Math.max(rgb.g, 0)),
791
+ // b: Math.min(255, Math.max(rgb.b, 0)),
792
+ r: rgb.r,
793
+ g: rgb.g,
794
+ b: rgb.b,
814
795
  a: boundAlpha(a),
815
796
  };
816
797
  }
@@ -829,15 +810,13 @@
829
810
  constructor(input, config) {
830
811
  let color = input;
831
812
  const configFormat = config && COLOR_FORMAT.includes(config)
832
- ? config : 'rgb';
813
+ ? config : '';
833
814
 
834
- // If input is already a `Color`, return itself
815
+ // If input is already a `Color`, clone its values
835
816
  if (color instanceof Color) {
836
817
  color = inputToRGB(color);
837
818
  }
838
- if (typeof color === 'number') {
839
- color = numberInputToObject(color);
840
- }
819
+
841
820
  const {
842
821
  r, g, b, a, ok, format,
843
822
  } = inputToRGB(color);
@@ -846,7 +825,7 @@
846
825
  const self = this;
847
826
 
848
827
  /** @type {CP.ColorInput} */
849
- self.originalInput = color;
828
+ self.originalInput = input;
850
829
  /** @type {number} */
851
830
  self.r = r;
852
831
  /** @type {number} */
@@ -887,24 +866,21 @@
887
866
  let R = 0;
888
867
  let G = 0;
889
868
  let B = 0;
890
- const rp = r / 255;
891
- const rg = g / 255;
892
- const rb = b / 255;
893
869
 
894
- if (rp <= 0.03928) {
895
- R = rp / 12.92;
870
+ if (r <= 0.03928) {
871
+ R = r / 12.92;
896
872
  } else {
897
- R = ((rp + 0.055) / 1.055) ** 2.4;
873
+ R = ((r + 0.055) / 1.055) ** 2.4;
898
874
  }
899
- if (rg <= 0.03928) {
900
- G = rg / 12.92;
875
+ if (g <= 0.03928) {
876
+ G = g / 12.92;
901
877
  } else {
902
- G = ((rg + 0.055) / 1.055) ** 2.4;
878
+ G = ((g + 0.055) / 1.055) ** 2.4;
903
879
  }
904
- if (rb <= 0.03928) {
905
- B = rb / 12.92;
880
+ if (b <= 0.03928) {
881
+ B = b / 12.92;
906
882
  } else {
907
- B = ((rb + 0.055) / 1.055) ** 2.4;
883
+ B = ((b + 0.055) / 1.055) ** 2.4;
908
884
  }
909
885
  return 0.2126 * R + 0.7152 * G + 0.0722 * B;
910
886
  }
@@ -914,7 +890,7 @@
914
890
  * @returns {number} a number in the [0, 255] range
915
891
  */
916
892
  get brightness() {
917
- const { r, g, b } = this;
893
+ const { r, g, b } = this.toRgb();
918
894
  return (r * 299 + g * 587 + b * 114) / 1000;
919
895
  }
920
896
 
@@ -923,16 +899,14 @@
923
899
  * @returns {CP.RGBA} an {r,g,b,a} object with [0, 255] ranged values
924
900
  */
925
901
  toRgb() {
926
- const {
902
+ let {
927
903
  r, g, b, a,
928
904
  } = this;
929
- const [R, G, B] = [r, g, b].map((x) => roundPart(x));
930
905
 
906
+ [r, g, b] = [r, g, b].map((n) => roundPart(n * 255 * 100) / 100);
907
+ a = roundPart(a * 100) / 100;
931
908
  return {
932
- r: R,
933
- g: G,
934
- b: B,
935
- a: roundPart(a * 100) / 100,
909
+ r, g, b, a,
936
910
  };
937
911
  }
938
912
 
@@ -946,10 +920,11 @@
946
920
  const {
947
921
  r, g, b, a,
948
922
  } = this.toRgb();
923
+ const [R, G, B] = [r, g, b].map(roundPart);
949
924
 
950
925
  return a === 1
951
- ? `rgb(${r}, ${g}, ${b})`
952
- : `rgba(${r}, ${g}, ${b}, ${a})`;
926
+ ? `rgb(${R}, ${G}, ${B})`
927
+ : `rgba(${R}, ${G}, ${B}, ${a})`;
953
928
  }
954
929
 
955
930
  /**
@@ -962,9 +937,10 @@
962
937
  const {
963
938
  r, g, b, a,
964
939
  } = this.toRgb();
940
+ const [R, G, B] = [r, g, b].map(roundPart);
965
941
  const A = a === 1 ? '' : ` / ${roundPart(a * 100)}%`;
966
942
 
967
- return `rgb(${r} ${g} ${b}${A})`;
943
+ return `rgb(${R} ${G} ${B}${A})`;
968
944
  }
969
945
 
970
946
  /**
@@ -1024,7 +1000,7 @@
1024
1000
  toHsv() {
1025
1001
  const {
1026
1002
  r, g, b, a,
1027
- } = this.toRgb();
1003
+ } = this;
1028
1004
  const { h, s, v } = rgbToHsv(r, g, b);
1029
1005
 
1030
1006
  return {
@@ -1039,7 +1015,7 @@
1039
1015
  toHsl() {
1040
1016
  const {
1041
1017
  r, g, b, a,
1042
- } = this.toRgb();
1018
+ } = this;
1043
1019
  const { h, s, l } = rgbToHsl(r, g, b);
1044
1020
 
1045
1021
  return {
@@ -1124,6 +1100,7 @@
1124
1100
  */
1125
1101
  setAlpha(alpha) {
1126
1102
  const self = this;
1103
+ if (typeof alpha !== 'number') return self;
1127
1104
  self.a = boundAlpha(alpha);
1128
1105
  return self;
1129
1106
  }
@@ -1238,6 +1215,7 @@
1238
1215
  isOnePointZero,
1239
1216
  isPercentage,
1240
1217
  isValidCSSUnit,
1218
+ isColorName,
1241
1219
  pad2,
1242
1220
  clamp01,
1243
1221
  bound01,
@@ -1255,10 +1233,11 @@
1255
1233
  hueToRgb,
1256
1234
  hwbToRgb,
1257
1235
  parseIntFromHex,
1258
- numberInputToObject,
1259
1236
  stringInputToObject,
1260
1237
  inputToRGB,
1261
1238
  roundPart,
1239
+ getElementStyle,
1240
+ setElementStyle,
1262
1241
  ObjectAssign,
1263
1242
  });
1264
1243
 
@@ -1388,24 +1367,6 @@
1388
1367
  */
1389
1368
  const ariaValueNow = 'aria-valuenow';
1390
1369
 
1391
- /**
1392
- * A global namespace for aria-haspopup.
1393
- * @type {string}
1394
- */
1395
- const ariaHasPopup = 'aria-haspopup';
1396
-
1397
- /**
1398
- * A global namespace for aria-hidden.
1399
- * @type {string}
1400
- */
1401
- const ariaHidden = 'aria-hidden';
1402
-
1403
- /**
1404
- * A global namespace for aria-labelledby.
1405
- * @type {string}
1406
- */
1407
- const ariaLabelledBy = 'aria-labelledby';
1408
-
1409
1370
  /**
1410
1371
  * A global namespace for `ArrowDown` key.
1411
1372
  * @type {string} e.which = 40 equivalent
@@ -1532,37 +1493,6 @@
1532
1493
  */
1533
1494
  const focusoutEvent = 'focusout';
1534
1495
 
1535
- // @ts-ignore
1536
- const { userAgentData: uaDATA } = navigator;
1537
-
1538
- /**
1539
- * A global namespace for `userAgentData` object.
1540
- */
1541
- const userAgentData = uaDATA;
1542
-
1543
- const { userAgent: userAgentString } = navigator;
1544
-
1545
- /**
1546
- * A global namespace for `navigator.userAgent` string.
1547
- */
1548
- const userAgent = userAgentString;
1549
-
1550
- const mobileBrands = /iPhone|iPad|iPod|Android/i;
1551
- let isMobileCheck = false;
1552
-
1553
- if (userAgentData) {
1554
- isMobileCheck = userAgentData.brands
1555
- .some((/** @type {Record<String, any>} */x) => mobileBrands.test(x.brand));
1556
- } else {
1557
- isMobileCheck = mobileBrands.test(userAgent);
1558
- }
1559
-
1560
- /**
1561
- * A global `boolean` for mobile detection.
1562
- * @type {boolean}
1563
- */
1564
- const isMobile = isMobileCheck;
1565
-
1566
1496
  /**
1567
1497
  * Returns the `document.documentElement` or the `<html>` element.
1568
1498
  *
@@ -1747,30 +1677,6 @@
1747
1677
  return lookUp.getElementsByClassName(selector);
1748
1678
  }
1749
1679
 
1750
- /**
1751
- * This is a shortie for `document.createElementNS` method
1752
- * which allows you to create a new `HTMLElement` for a given `tagName`
1753
- * or based on an object with specific non-readonly attributes:
1754
- * `id`, `className`, `textContent`, `style`, etc.
1755
- * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
1756
- *
1757
- * @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
1758
- * @param {Record<string, string> | string} param `tagName` or object
1759
- * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
1760
- */
1761
- function createElementNS(namespace, param) {
1762
- if (typeof param === 'string') {
1763
- return getDocument().createElementNS(namespace, param);
1764
- }
1765
-
1766
- const { tagName } = param;
1767
- const attr = { ...param };
1768
- const newElement = createElementNS(namespace, tagName);
1769
- delete attr.tagName;
1770
- ObjectAssign(newElement, attr);
1771
- return newElement;
1772
- }
1773
-
1774
1680
  /**
1775
1681
  * Shortcut for the `Element.dispatchEvent(Event)` method.
1776
1682
  *
@@ -1888,12 +1794,11 @@
1888
1794
  }
1889
1795
 
1890
1796
  /**
1891
- * Shortcut for `String.toLowerCase()`.
1892
- *
1893
- * @param {string} source input string
1894
- * @returns {string} lowercase output string
1797
+ * Shortcut for `Object.keys()` static method.
1798
+ * @param {Record<string, any>} obj a target object
1799
+ * @returns {string[]}
1895
1800
  */
1896
- const toLowerCase = (source) => source.toLowerCase();
1801
+ const ObjectKeys = (obj) => Object.keys(obj);
1897
1802
 
1898
1803
  /**
1899
1804
  * Utility to normalize component options.
@@ -1998,6 +1903,77 @@
1998
1903
  */
1999
1904
  const removeAttribute = (element, attribute) => element.removeAttribute(attribute);
2000
1905
 
1906
+ /**
1907
+ * @class
1908
+ * Returns a color palette with a given set of parameters.
1909
+ * @example
1910
+ * new ColorPalette(0, 12, 10);
1911
+ * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: Array<Color> }
1912
+ */
1913
+ class ColorPalette {
1914
+ /**
1915
+ * The `hue` parameter is optional, which would be set to 0.
1916
+ * @param {number[]} args represeinting hue, hueSteps, lightSteps
1917
+ * * `args.hue` the starting Hue [0, 360]
1918
+ * * `args.hueSteps` Hue Steps Count [5, 24]
1919
+ * * `args.lightSteps` Lightness Steps Count [5, 12]
1920
+ */
1921
+ constructor(...args) {
1922
+ let hue = 0;
1923
+ let hueSteps = 12;
1924
+ let lightSteps = 10;
1925
+ let lightnessArray = [0.5];
1926
+
1927
+ if (args.length === 3) {
1928
+ [hue, hueSteps, lightSteps] = args;
1929
+ } else if (args.length === 2) {
1930
+ [hueSteps, lightSteps] = args;
1931
+ if ([hueSteps, lightSteps].some((n) => n < 1)) {
1932
+ throw TypeError('ColorPalette: both arguments must be higher than 0.');
1933
+ }
1934
+ }
1935
+
1936
+ /** @type {*} */
1937
+ const colors = [];
1938
+ const hueStep = 360 / hueSteps;
1939
+ const half = roundPart((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
1940
+ const steps1To13 = [0.25, 0.2, 0.15, 0.11, 0.09, 0.075];
1941
+ const lightSets = [[1, 2, 3], [4, 5], [6, 7], [8, 9], [10, 11], [12, 13]];
1942
+ const closestSet = lightSets.find((set) => set.includes(lightSteps));
1943
+
1944
+ // find a lightStep that won't go beyond black and white
1945
+ // something within the [10-90] range of lightness
1946
+ const lightStep = closestSet
1947
+ ? steps1To13[lightSets.indexOf(closestSet)]
1948
+ : (100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100);
1949
+
1950
+ // light tints
1951
+ for (let i = 1; i < half + 1; i += 1) {
1952
+ lightnessArray = [...lightnessArray, (0.5 + lightStep * (i))];
1953
+ }
1954
+
1955
+ // dark tints
1956
+ for (let i = 1; i < lightSteps - half; i += 1) {
1957
+ lightnessArray = [(0.5 - lightStep * (i)), ...lightnessArray];
1958
+ }
1959
+
1960
+ // feed `colors` Array
1961
+ for (let i = 0; i < hueSteps; i += 1) {
1962
+ const currentHue = ((hue + i * hueStep) % 360) / 360;
1963
+ lightnessArray.forEach((l) => {
1964
+ colors.push(new Color({ h: currentHue, s: 1, l }));
1965
+ });
1966
+ }
1967
+
1968
+ this.hue = hue;
1969
+ this.hueSteps = hueSteps;
1970
+ this.lightSteps = lightSteps;
1971
+ this.colors = colors;
1972
+ }
1973
+ }
1974
+
1975
+ ObjectAssign(ColorPalette, { Color });
1976
+
2001
1977
  /** @type {Record<string, string>} */
2002
1978
  const colorPickerLabels = {
2003
1979
  pickerLabel: 'Colour Picker',
@@ -2025,14 +2001,72 @@
2025
2001
  */
2026
2002
  const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
2027
2003
 
2004
+ const tabIndex = 'tabindex';
2005
+
2028
2006
  /**
2029
- * Shortcut for `String.toUpperCase()`.
2030
- *
2031
- * @param {string} source input string
2032
- * @returns {string} uppercase output string
2007
+ * Check if a string is valid JSON string.
2008
+ * @param {string} str the string input
2009
+ * @returns {boolean} the query result
2010
+ */
2011
+ function isValidJSON(str) {
2012
+ try {
2013
+ JSON.parse(str);
2014
+ } catch (e) {
2015
+ return false;
2016
+ }
2017
+ return true;
2018
+ }
2019
+
2020
+ /**
2021
+ * Shortcut for `String.toUpperCase()`.
2022
+ *
2023
+ * @param {string} source input string
2024
+ * @returns {string} uppercase output string
2033
2025
  */
2034
2026
  const toUpperCase = (source) => source.toUpperCase();
2035
2027
 
2028
+ /**
2029
+ * A global namespace for aria-haspopup.
2030
+ * @type {string}
2031
+ */
2032
+ const ariaHasPopup = 'aria-haspopup';
2033
+
2034
+ /**
2035
+ * A global namespace for aria-hidden.
2036
+ * @type {string}
2037
+ */
2038
+ const ariaHidden = 'aria-hidden';
2039
+
2040
+ /**
2041
+ * A global namespace for aria-labelledby.
2042
+ * @type {string}
2043
+ */
2044
+ const ariaLabelledBy = 'aria-labelledby';
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
+
2036
2070
  const vHidden = 'v-hidden';
2037
2071
 
2038
2072
  /**
@@ -2112,8 +2146,6 @@
2112
2146
  */
2113
2147
  const ariaValueMax = 'aria-valuemax';
2114
2148
 
2115
- const tabIndex = 'tabindex';
2116
-
2117
2149
  /**
2118
2150
  * Returns all color controls for `ColorPicker`.
2119
2151
  *
@@ -2221,75 +2253,6 @@
2221
2253
  });
2222
2254
  }
2223
2255
 
2224
- /**
2225
- * @class
2226
- * Returns a color palette with a given set of parameters.
2227
- * @example
2228
- * new ColorPalette(0, 12, 10);
2229
- * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: array }
2230
- */
2231
- class ColorPalette {
2232
- /**
2233
- * The `hue` parameter is optional, which would be set to 0.
2234
- * @param {number[]} args represeinting hue, hueSteps, lightSteps
2235
- * * `args.hue` the starting Hue [0, 360]
2236
- * * `args.hueSteps` Hue Steps Count [5, 24]
2237
- * * `args.lightSteps` Lightness Steps Count [5, 12]
2238
- */
2239
- constructor(...args) {
2240
- let hue = 0;
2241
- let hueSteps = 12;
2242
- let lightSteps = 10;
2243
- let lightnessArray = [0.5];
2244
-
2245
- if (args.length === 3) {
2246
- [hue, hueSteps, lightSteps] = args;
2247
- } else if (args.length === 2) {
2248
- [hueSteps, lightSteps] = args;
2249
- } else {
2250
- throw TypeError('ColorPalette requires minimum 2 arguments');
2251
- }
2252
-
2253
- /** @type {string[]} */
2254
- const colors = [];
2255
-
2256
- const hueStep = 360 / hueSteps;
2257
- const half = roundPart((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
2258
- const estimatedStep = 100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100;
2259
-
2260
- let lightStep = 0.25;
2261
- lightStep = [4, 5].includes(lightSteps) ? 0.2 : lightStep;
2262
- lightStep = [6, 7].includes(lightSteps) ? 0.15 : lightStep;
2263
- lightStep = [8, 9].includes(lightSteps) ? 0.11 : lightStep;
2264
- lightStep = [10, 11].includes(lightSteps) ? 0.09 : lightStep;
2265
- lightStep = [12, 13].includes(lightSteps) ? 0.075 : lightStep;
2266
- lightStep = lightSteps > 13 ? estimatedStep : lightStep;
2267
-
2268
- // light tints
2269
- for (let i = 1; i < half + 1; i += 1) {
2270
- lightnessArray = [...lightnessArray, (0.5 + lightStep * (i))];
2271
- }
2272
-
2273
- // dark tints
2274
- for (let i = 1; i < lightSteps - half; i += 1) {
2275
- lightnessArray = [(0.5 - lightStep * (i)), ...lightnessArray];
2276
- }
2277
-
2278
- // feed `colors` Array
2279
- for (let i = 0; i < hueSteps; i += 1) {
2280
- const currentHue = ((hue + i * hueStep) % 360) / 360;
2281
- lightnessArray.forEach((l) => {
2282
- colors.push(new Color({ h: currentHue, s: 1, l }).toHexString());
2283
- });
2284
- }
2285
-
2286
- this.hue = hue;
2287
- this.hueSteps = hueSteps;
2288
- this.lightSteps = lightSteps;
2289
- this.colors = colors;
2290
- }
2291
- }
2292
-
2293
2256
  /**
2294
2257
  * Returns a color-defaults with given values and class.
2295
2258
  * @param {CP.ColorPicker} self
@@ -2323,7 +2286,8 @@
2323
2286
  optionSize = fit > 5 && isMultiLine ? 1.5 : optionSize;
2324
2287
  const menuHeight = `${(rowCount || 1) * optionSize}rem`;
2325
2288
  const menuHeightHover = `calc(${rowCountHover} * ${optionSize}rem + ${rowCountHover - 1} * ${gap})`;
2326
-
2289
+ /** @type {HTMLUListElement} */
2290
+ // @ts-ignore -- <UL> is an `HTMLElement`
2327
2291
  const menu = createElement({
2328
2292
  tagName: 'ul',
2329
2293
  className: finalClass,
@@ -2331,7 +2295,7 @@
2331
2295
  setAttribute(menu, 'role', 'listbox');
2332
2296
  setAttribute(menu, ariaLabel, menuLabel);
2333
2297
 
2334
- if (isScrollable) { // @ts-ignore
2298
+ if (isScrollable) {
2335
2299
  setCSSProperties(menu, {
2336
2300
  '--grid-item-size': `${optionSize}rem`,
2337
2301
  '--grid-fit': fit,
@@ -2342,15 +2306,19 @@
2342
2306
  }
2343
2307
 
2344
2308
  colorsArray.forEach((x) => {
2345
- const [value, label] = x.trim().split(':');
2346
- const xRealColor = new Color(value, format).toString();
2347
- 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');
2348
2316
  const active = isActive ? ' active' : '';
2349
2317
 
2350
2318
  const option = createElement({
2351
2319
  tagName: 'li',
2352
2320
  className: `color-option${active}`,
2353
- innerText: `${label || x}`,
2321
+ innerText: `${label || value}`,
2354
2322
  });
2355
2323
 
2356
2324
  setAttribute(option, tabIndex, '0');
@@ -2359,7 +2327,7 @@
2359
2327
  setAttribute(option, ariaSelected, isActive ? 'true' : 'false');
2360
2328
 
2361
2329
  if (isOptionsMenu) {
2362
- setElementStyle(option, { backgroundColor: x });
2330
+ setElementStyle(option, { backgroundColor: value });
2363
2331
  }
2364
2332
 
2365
2333
  menu.append(option);
@@ -2368,55 +2336,10 @@
2368
2336
  }
2369
2337
 
2370
2338
  /**
2371
- * Check if a string is valid JSON string.
2372
- * @param {string} str the string input
2373
- * @returns {boolean} the query result
2374
- */
2375
- function isValidJSON(str) {
2376
- try {
2377
- JSON.parse(str);
2378
- } catch (e) {
2379
- return false;
2380
- }
2381
- return true;
2382
- }
2383
-
2384
- var version = "0.0.1alpha3";
2385
-
2386
- // @ts-ignore
2387
-
2388
- const Version = version;
2389
-
2390
- // ColorPicker GC
2391
- // ==============
2392
- const colorPickerString = 'color-picker';
2393
- const colorPickerSelector = `[data-function="${colorPickerString}"]`;
2394
- const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2395
- const colorPickerDefaults = {
2396
- componentLabels: colorPickerLabels,
2397
- colorLabels: colorNames,
2398
- format: 'rgb',
2399
- colorPresets: false,
2400
- colorKeywords: false,
2401
- };
2402
-
2403
- // ColorPicker Static Methods
2404
- // ==========================
2405
-
2406
- /** @type {CP.GetInstance<ColorPicker>} */
2407
- const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
2408
-
2409
- /** @type {CP.InitCallback<ColorPicker>} */
2410
- const initColorPicker = (element) => new ColorPicker(element);
2411
-
2412
- // ColorPicker Private Methods
2413
- // ===========================
2414
-
2415
- /**
2416
- * Generate HTML markup and update instance properties.
2417
- * @param {ColorPicker} self
2418
- */
2419
- function initCallback(self) {
2339
+ * Generate HTML markup and update instance properties.
2340
+ * @param {CP.ColorPicker} self
2341
+ */
2342
+ function setMarkup(self) {
2420
2343
  const {
2421
2344
  input, parent, format, id, componentLabels, colorKeywords, colorPresets,
2422
2345
  } = self;
@@ -2431,9 +2354,7 @@
2431
2354
  self.color = new Color(color, format);
2432
2355
 
2433
2356
  // set initial controls dimensions
2434
- // make the controls smaller on mobile
2435
- const dropClass = isMobile ? ' mobile' : '';
2436
- const formatString = format === 'hex' ? hexLabel : format.toUpperCase();
2357
+ const formatString = format === 'hex' ? hexLabel : toUpperCase(format);
2437
2358
 
2438
2359
  const pickerBtn = createElement({
2439
2360
  id: `picker-btn-${id}`,
@@ -2450,7 +2371,7 @@
2450
2371
 
2451
2372
  const pickerDropdown = createElement({
2452
2373
  tagName: 'div',
2453
- className: `color-dropdown picker${dropClass}`,
2374
+ className: 'color-dropdown picker',
2454
2375
  });
2455
2376
  setAttribute(pickerDropdown, ariaLabelledBy, `picker-btn-${id}`);
2456
2377
  setAttribute(pickerDropdown, 'role', 'group');
@@ -2466,7 +2387,7 @@
2466
2387
  if (colorKeywords || colorPresets) {
2467
2388
  const presetsDropdown = createElement({
2468
2389
  tagName: 'div',
2469
- className: `color-dropdown scrollable menu${dropClass}`,
2390
+ className: 'color-dropdown scrollable menu',
2470
2391
  });
2471
2392
 
2472
2393
  // color presets
@@ -2516,6 +2437,37 @@
2516
2437
  setAttribute(input, tabIndex, '-1');
2517
2438
  }
2518
2439
 
2440
+ var version = "0.0.2alpha2";
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
+
2519
2471
  /**
2520
2472
  * Add / remove `ColorPicker` main event listeners.
2521
2473
  * @param {ColorPicker} self
@@ -2528,8 +2480,6 @@
2528
2480
  fn(input, focusinEvent, self.showPicker);
2529
2481
  fn(pickerToggle, mouseclickEvent, self.togglePicker);
2530
2482
 
2531
- fn(input, keydownEvent, self.keyToggle);
2532
-
2533
2483
  if (menuToggle) {
2534
2484
  fn(menuToggle, mouseclickEvent, self.toggleMenu);
2535
2485
  }
@@ -2567,8 +2517,7 @@
2567
2517
  fn(doc, pointerEvents.move, self.pointerMove);
2568
2518
  fn(doc, pointerEvents.up, self.pointerUp);
2569
2519
  fn(parent, focusoutEvent, self.handleFocusOut);
2570
- // @ts-ignore -- this is `Window`
2571
- fn(win, keyupEvent, self.handleDismiss);
2520
+ fn(doc, keyupEvent, self.handleDismiss);
2572
2521
  }
2573
2522
 
2574
2523
  /**
@@ -2652,7 +2601,7 @@
2652
2601
  const input = querySelector(target);
2653
2602
 
2654
2603
  // invalidate
2655
- if (!input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
2604
+ if (!input) throw new TypeError(`ColorPicker target "${target}" cannot be found.`);
2656
2605
  self.input = input;
2657
2606
 
2658
2607
  const parent = closest(input, colorPickerParentSelector);
@@ -2699,15 +2648,14 @@
2699
2648
  });
2700
2649
 
2701
2650
  // update and expose component labels
2702
- const tempLabels = ObjectAssign({}, colorPickerLabels);
2703
- const jsonLabels = componentLabels && isValidJSON(componentLabels)
2704
- ? JSON.parse(componentLabels) : componentLabels || {};
2651
+ const tempComponentLabels = componentLabels && isValidJSON(componentLabels)
2652
+ ? JSON.parse(componentLabels) : componentLabels;
2705
2653
 
2706
2654
  /** @type {Record<string, string>} */
2707
- self.componentLabels = ObjectAssign(tempLabels, jsonLabels);
2655
+ self.componentLabels = ObjectAssign(colorPickerLabels, tempComponentLabels);
2708
2656
 
2709
2657
  /** @type {Color} */
2710
- self.color = new Color('white', format);
2658
+ self.color = new Color(input.value || '#fff', format);
2711
2659
 
2712
2660
  /** @type {CP.ColorFormats} */
2713
2661
  self.format = format;
@@ -2716,7 +2664,7 @@
2716
2664
  if (colorKeywords instanceof Array) {
2717
2665
  self.colorKeywords = colorKeywords;
2718
2666
  } else if (typeof colorKeywords === 'string' && colorKeywords.length) {
2719
- self.colorKeywords = colorKeywords.split(',');
2667
+ self.colorKeywords = colorKeywords.split(',').map((x) => x.trim());
2720
2668
  }
2721
2669
 
2722
2670
  // set colour presets
@@ -2745,11 +2693,10 @@
2745
2693
  self.handleFocusOut = self.handleFocusOut.bind(self);
2746
2694
  self.changeHandler = self.changeHandler.bind(self);
2747
2695
  self.handleDismiss = self.handleDismiss.bind(self);
2748
- self.keyToggle = self.keyToggle.bind(self);
2749
2696
  self.handleKnobs = self.handleKnobs.bind(self);
2750
2697
 
2751
2698
  // generate markup
2752
- initCallback(self);
2699
+ setMarkup(self);
2753
2700
 
2754
2701
  const [colorPicker, colorMenu] = getElementsByClassName('color-dropdown', parent);
2755
2702
  // set main elements
@@ -2837,76 +2784,83 @@
2837
2784
  return inputValue !== '' && new Color(inputValue).isValid;
2838
2785
  }
2839
2786
 
2787
+ /** Returns the colour appearance, usually the closest colour name for the current value. */
2788
+ get appearance() {
2789
+ const {
2790
+ colorLabels, hsl, hsv, format,
2791
+ } = this;
2792
+
2793
+ const hue = roundPart(hsl.h * 360);
2794
+ const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
2795
+ const saturation = roundPart(saturationSource * 100);
2796
+ const lightness = roundPart(hsl.l * 100);
2797
+ const hsvl = hsv.v * 100;
2798
+
2799
+ let colorName;
2800
+
2801
+ // determine color appearance
2802
+ if (lightness === 100 && saturation === 0) {
2803
+ colorName = colorLabels.white;
2804
+ } else if (lightness === 0) {
2805
+ colorName = colorLabels.black;
2806
+ } else if (saturation === 0) {
2807
+ colorName = colorLabels.grey;
2808
+ } else if (hue < 15 || hue >= 345) {
2809
+ colorName = colorLabels.red;
2810
+ } else if (hue >= 15 && hue < 45) {
2811
+ colorName = hsvl > 80 && saturation > 80 ? colorLabels.orange : colorLabels.brown;
2812
+ } else if (hue >= 45 && hue < 75) {
2813
+ const isGold = hue > 46 && hue < 54 && hsvl < 80 && saturation > 90;
2814
+ const isOlive = hue >= 54 && hue < 75 && hsvl < 80;
2815
+ colorName = isGold ? colorLabels.gold : colorLabels.yellow;
2816
+ colorName = isOlive ? colorLabels.olive : colorName;
2817
+ } else if (hue >= 75 && hue < 155) {
2818
+ colorName = hsvl < 68 ? colorLabels.green : colorLabels.lime;
2819
+ } else if (hue >= 155 && hue < 175) {
2820
+ colorName = colorLabels.teal;
2821
+ } else if (hue >= 175 && hue < 195) {
2822
+ colorName = colorLabels.cyan;
2823
+ } else if (hue >= 195 && hue < 255) {
2824
+ colorName = colorLabels.blue;
2825
+ } else if (hue >= 255 && hue < 270) {
2826
+ colorName = colorLabels.violet;
2827
+ } else if (hue >= 270 && hue < 295) {
2828
+ colorName = colorLabels.magenta;
2829
+ } else if (hue >= 295 && hue < 345) {
2830
+ colorName = colorLabels.pink;
2831
+ }
2832
+ return colorName;
2833
+ }
2834
+
2840
2835
  /** Updates `ColorPicker` visuals. */
2841
2836
  updateVisuals() {
2842
2837
  const self = this;
2843
2838
  const {
2844
- format, controlPositions, visuals,
2839
+ controlPositions, visuals,
2845
2840
  } = self;
2846
2841
  const [v1, v2, v3] = visuals;
2847
- const { offsetWidth, offsetHeight } = v1;
2848
- const hue = format === 'hsl'
2849
- ? controlPositions.c1x / offsetWidth
2850
- : controlPositions.c2y / offsetHeight;
2851
- // @ts-ignore - `hslToRgb` is assigned to `Color` as static method
2852
- const { r, g, b } = Color.hslToRgb(hue, 1, 0.5);
2842
+ const { offsetHeight } = v1;
2843
+ const hue = controlPositions.c2y / offsetHeight;
2844
+ const { r, g, b } = new Color({ h: hue, s: 1, l: 0.5 }).toRgb();
2853
2845
  const whiteGrad = 'linear-gradient(rgb(255,255,255) 0%, rgb(255,255,255) 100%)';
2854
2846
  const alpha = 1 - controlPositions.c3y / offsetHeight;
2855
2847
  const roundA = roundPart((alpha * 100)) / 100;
2856
2848
 
2857
- if (format !== 'hsl') {
2858
- const fill = new Color({
2859
- h: hue, s: 1, l: 0.5, a: alpha,
2860
- }).toRgbString();
2861
- const hueGradient = `linear-gradient(
2862
- rgb(255,0,0) 0%, rgb(255,255,0) 16.67%,
2863
- rgb(0,255,0) 33.33%, rgb(0,255,255) 50%,
2864
- rgb(0,0,255) 66.67%, rgb(255,0,255) 83.33%,
2865
- rgb(255,0,0) 100%)`;
2866
- setElementStyle(v1, {
2867
- background: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,${roundA}) 100%),
2868
- linear-gradient(to right, rgba(255,255,255,${roundA}) 0%, ${fill} 100%),
2869
- ${whiteGrad}`,
2870
- });
2871
- setElementStyle(v2, { background: hueGradient });
2872
- } else {
2873
- const saturation = roundPart((controlPositions.c2y / offsetHeight) * 100);
2874
- const fill0 = new Color({
2875
- r: 255, g: 0, b: 0, a: alpha,
2876
- }).saturate(-saturation).toRgbString();
2877
- const fill1 = new Color({
2878
- r: 255, g: 255, b: 0, a: alpha,
2879
- }).saturate(-saturation).toRgbString();
2880
- const fill2 = new Color({
2881
- r: 0, g: 255, b: 0, a: alpha,
2882
- }).saturate(-saturation).toRgbString();
2883
- const fill3 = new Color({
2884
- r: 0, g: 255, b: 255, a: alpha,
2885
- }).saturate(-saturation).toRgbString();
2886
- const fill4 = new Color({
2887
- r: 0, g: 0, b: 255, a: alpha,
2888
- }).saturate(-saturation).toRgbString();
2889
- const fill5 = new Color({
2890
- r: 255, g: 0, b: 255, a: alpha,
2891
- }).saturate(-saturation).toRgbString();
2892
- const fill6 = new Color({
2893
- r: 255, g: 0, b: 0, a: alpha,
2894
- }).saturate(-saturation).toRgbString();
2895
- const fillGradient = `linear-gradient(to right,
2896
- ${fill0} 0%, ${fill1} 16.67%, ${fill2} 33.33%, ${fill3} 50%,
2897
- ${fill4} 66.67%, ${fill5} 83.33%, ${fill6} 100%)`;
2898
- const lightGrad = `linear-gradient(rgba(255,255,255,${roundA}) 0%, rgba(255,255,255,0) 50%),
2899
- linear-gradient(rgba(0,0,0,0) 50%, rgba(0,0,0,${roundA}) 100%)`;
2900
-
2901
- setElementStyle(v1, { background: `${lightGrad},${fillGradient},${whiteGrad}` });
2902
- const {
2903
- r: gr, g: gg, b: gb,
2904
- } = new Color({ r, g, b }).greyscale().toRgb();
2849
+ const fill = new Color({
2850
+ h: hue, s: 1, l: 0.5, a: alpha,
2851
+ }).toRgbString();
2852
+ const hueGradient = `linear-gradient(
2853
+ rgb(255,0,0) 0%, rgb(255,255,0) 16.67%,
2854
+ rgb(0,255,0) 33.33%, rgb(0,255,255) 50%,
2855
+ rgb(0,0,255) 66.67%, rgb(255,0,255) 83.33%,
2856
+ rgb(255,0,0) 100%)`;
2857
+ setElementStyle(v1, {
2858
+ background: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,${roundA}) 100%),
2859
+ linear-gradient(to right, rgba(255,255,255,${roundA}) 0%, ${fill} 100%),
2860
+ ${whiteGrad}`,
2861
+ });
2862
+ setElementStyle(v2, { background: hueGradient });
2905
2863
 
2906
- setElementStyle(v2, {
2907
- background: `linear-gradient(rgb(${r},${g},${b}) 0%, rgb(${gr},${gg},${gb}) 100%)`,
2908
- });
2909
- }
2910
2864
  setElementStyle(v3, {
2911
2865
  background: `linear-gradient(rgba(${r},${g},${b},1) 0%,rgba(${r},${g},${b},0) 100%)`,
2912
2866
  });
@@ -2945,7 +2899,7 @@
2945
2899
  const self = this;
2946
2900
  const { activeElement } = getDocument(self.input);
2947
2901
 
2948
- if ((isMobile && self.dragElement)
2902
+ if ((e.type === touchmoveEvent && self.dragElement)
2949
2903
  || (activeElement && self.controlKnobs.includes(activeElement))) {
2950
2904
  e.stopPropagation();
2951
2905
  e.preventDefault();
@@ -3056,13 +3010,13 @@
3056
3010
  const [v1, v2, v3] = visuals;
3057
3011
  const [c1, c2, c3] = controlKnobs;
3058
3012
  /** @type {HTMLElement} */
3059
- const visual = hasClass(target, 'visual-control')
3060
- ? target : querySelector('.visual-control', target.parentElement);
3013
+ const visual = controlKnobs.includes(target) ? target.previousElementSibling : target;
3061
3014
  const visualRect = getBoundingClientRect(visual);
3015
+ const html = getDocumentElement(v1);
3062
3016
  const X = type === 'touchstart' ? touches[0].pageX : pageX;
3063
3017
  const Y = type === 'touchstart' ? touches[0].pageY : pageY;
3064
- const offsetX = X - window.pageXOffset - visualRect.left;
3065
- const offsetY = Y - window.pageYOffset - visualRect.top;
3018
+ const offsetX = X - html.scrollLeft - visualRect.left;
3019
+ const offsetY = Y - html.scrollTop - visualRect.top;
3066
3020
 
3067
3021
  if (target === v1 || target === c1) {
3068
3022
  self.dragElement = visual;
@@ -3122,10 +3076,11 @@
3122
3076
  if (!dragElement) return;
3123
3077
 
3124
3078
  const controlRect = getBoundingClientRect(dragElement);
3125
- const X = type === 'touchmove' ? touches[0].pageX : pageX;
3126
- const Y = type === 'touchmove' ? touches[0].pageY : pageY;
3127
- const offsetX = X - window.pageXOffset - controlRect.left;
3128
- const offsetY = Y - window.pageYOffset - controlRect.top;
3079
+ const win = getDocumentElement(v1);
3080
+ const X = type === touchmoveEvent ? touches[0].pageX : pageX;
3081
+ const Y = type === touchmoveEvent ? touches[0].pageY : pageY;
3082
+ const offsetX = X - win.scrollLeft - controlRect.left;
3083
+ const offsetY = Y - win.scrollTop - controlRect.top;
3129
3084
 
3130
3085
  if (dragElement === v1) {
3131
3086
  self.changeControl1(offsetX, offsetY);
@@ -3152,30 +3107,41 @@
3152
3107
  if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
3153
3108
  e.preventDefault();
3154
3109
 
3155
- const { controlKnobs } = self;
3110
+ const { controlKnobs, visuals } = self;
3111
+ const { offsetWidth, offsetHeight } = visuals[0];
3156
3112
  const [c1, c2, c3] = controlKnobs;
3157
3113
  const { activeElement } = getDocument(c1);
3158
3114
  const currentKnob = controlKnobs.find((x) => x === activeElement);
3115
+ const yRatio = offsetHeight / 360;
3159
3116
 
3160
3117
  if (currentKnob) {
3161
3118
  let offsetX = 0;
3162
3119
  let offsetY = 0;
3120
+
3163
3121
  if (target === c1) {
3122
+ const xRatio = offsetWidth / 100;
3123
+
3164
3124
  if ([keyArrowLeft, keyArrowRight].includes(code)) {
3165
- self.controlPositions.c1x += code === keyArrowRight ? +1 : -1;
3125
+ self.controlPositions.c1x += code === keyArrowRight ? xRatio : -xRatio;
3166
3126
  } else if ([keyArrowUp, keyArrowDown].includes(code)) {
3167
- self.controlPositions.c1y += code === keyArrowDown ? +1 : -1;
3127
+ self.controlPositions.c1y += code === keyArrowDown ? yRatio : -yRatio;
3168
3128
  }
3169
3129
 
3170
3130
  offsetX = self.controlPositions.c1x;
3171
3131
  offsetY = self.controlPositions.c1y;
3172
3132
  self.changeControl1(offsetX, offsetY);
3173
3133
  } else if (target === c2) {
3174
- self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3134
+ self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code)
3135
+ ? yRatio
3136
+ : -yRatio;
3137
+
3175
3138
  offsetY = self.controlPositions.c2y;
3176
3139
  self.changeControl2(offsetY);
3177
3140
  } else if (target === c3) {
3178
- self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3141
+ self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code)
3142
+ ? yRatio
3143
+ : -yRatio;
3144
+
3179
3145
  offsetY = self.controlPositions.c3y;
3180
3146
  self.changeAlpha(offsetY);
3181
3147
  }
@@ -3203,7 +3169,7 @@
3203
3169
  if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
3204
3170
  if (activeElement === input) {
3205
3171
  if (isNonColorValue) {
3206
- colorSource = 'white';
3172
+ colorSource = currentValue === 'transparent' ? 'rgba(0,0,0,0)' : 'rgb(0,0,0)';
3207
3173
  } else {
3208
3174
  colorSource = currentValue;
3209
3175
  }
@@ -3254,9 +3220,7 @@
3254
3220
  changeControl1(X, Y) {
3255
3221
  const self = this;
3256
3222
  let [offsetX, offsetY] = [0, 0];
3257
- const {
3258
- format, controlPositions, visuals,
3259
- } = self;
3223
+ const { controlPositions, visuals } = self;
3260
3224
  const { offsetHeight, offsetWidth } = visuals[0];
3261
3225
 
3262
3226
  if (X > offsetWidth) offsetX = offsetWidth;
@@ -3265,29 +3229,19 @@
3265
3229
  if (Y > offsetHeight) offsetY = offsetHeight;
3266
3230
  else if (Y >= 0) offsetY = Y;
3267
3231
 
3268
- const hue = format === 'hsl'
3269
- ? offsetX / offsetWidth
3270
- : controlPositions.c2y / offsetHeight;
3232
+ const hue = controlPositions.c2y / offsetHeight;
3271
3233
 
3272
- const saturation = format === 'hsl'
3273
- ? 1 - controlPositions.c2y / offsetHeight
3274
- : offsetX / offsetWidth;
3234
+ const saturation = offsetX / offsetWidth;
3275
3235
 
3276
3236
  const lightness = 1 - offsetY / offsetHeight;
3277
3237
  const alpha = 1 - controlPositions.c3y / offsetHeight;
3278
3238
 
3279
- const colorObject = format === 'hsl'
3280
- ? {
3281
- h: hue, s: saturation, l: lightness, a: alpha,
3282
- }
3283
- : {
3284
- h: hue, s: saturation, v: lightness, a: alpha,
3285
- };
3286
-
3287
3239
  // new color
3288
3240
  const {
3289
3241
  r, g, b, a,
3290
- } = new Color(colorObject);
3242
+ } = new Color({
3243
+ h: hue, s: saturation, v: lightness, a: alpha,
3244
+ });
3291
3245
 
3292
3246
  ObjectAssign(self.color, {
3293
3247
  r, g, b, a,
@@ -3314,7 +3268,7 @@
3314
3268
  changeControl2(Y) {
3315
3269
  const self = this;
3316
3270
  const {
3317
- format, controlPositions, visuals,
3271
+ controlPositions, visuals,
3318
3272
  } = self;
3319
3273
  const { offsetHeight, offsetWidth } = visuals[0];
3320
3274
 
@@ -3323,26 +3277,17 @@
3323
3277
  if (Y > offsetHeight) offsetY = offsetHeight;
3324
3278
  else if (Y >= 0) offsetY = Y;
3325
3279
 
3326
- const hue = format === 'hsl'
3327
- ? controlPositions.c1x / offsetWidth
3328
- : offsetY / offsetHeight;
3329
- const saturation = format === 'hsl'
3330
- ? 1 - offsetY / offsetHeight
3331
- : controlPositions.c1x / offsetWidth;
3280
+ const hue = offsetY / offsetHeight;
3281
+ const saturation = controlPositions.c1x / offsetWidth;
3332
3282
  const lightness = 1 - controlPositions.c1y / offsetHeight;
3333
3283
  const alpha = 1 - controlPositions.c3y / offsetHeight;
3334
- const colorObject = format === 'hsl'
3335
- ? {
3336
- h: hue, s: saturation, l: lightness, a: alpha,
3337
- }
3338
- : {
3339
- h: hue, s: saturation, v: lightness, a: alpha,
3340
- };
3341
3284
 
3342
3285
  // new color
3343
3286
  const {
3344
3287
  r, g, b, a,
3345
- } = new Color(colorObject);
3288
+ } = new Color({
3289
+ h: hue, s: saturation, v: lightness, a: alpha,
3290
+ });
3346
3291
 
3347
3292
  ObjectAssign(self.color, {
3348
3293
  r, g, b, a,
@@ -3429,18 +3374,18 @@
3429
3374
  setControlPositions() {
3430
3375
  const self = this;
3431
3376
  const {
3432
- format, visuals, color, hsl, hsv,
3377
+ visuals, color, hsv,
3433
3378
  } = self;
3434
3379
  const { offsetHeight, offsetWidth } = visuals[0];
3435
3380
  const alpha = color.a;
3436
- const hue = hsl.h;
3381
+ const hue = hsv.h;
3437
3382
 
3438
- const saturation = format !== 'hsl' ? hsv.s : hsl.s;
3439
- const lightness = format !== 'hsl' ? hsv.v : hsl.l;
3383
+ const saturation = hsv.s;
3384
+ const lightness = hsv.v;
3440
3385
 
3441
- self.controlPositions.c1x = format !== 'hsl' ? saturation * offsetWidth : hue * offsetWidth;
3386
+ self.controlPositions.c1x = saturation * offsetWidth;
3442
3387
  self.controlPositions.c1y = (1 - lightness) * offsetHeight;
3443
- self.controlPositions.c2y = format !== 'hsl' ? hue * offsetHeight : (1 - saturation) * offsetHeight;
3388
+ self.controlPositions.c2y = hue * offsetHeight;
3444
3389
  self.controlPositions.c3y = (1 - alpha) * offsetHeight;
3445
3390
  }
3446
3391
 
@@ -3448,78 +3393,40 @@
3448
3393
  updateAppearance() {
3449
3394
  const self = this;
3450
3395
  const {
3451
- componentLabels, colorLabels, color, parent,
3452
- hsl, hsv, hex, format, controlKnobs,
3396
+ componentLabels, color, parent,
3397
+ hsv, hex, format, controlKnobs,
3453
3398
  } = self;
3454
3399
  const {
3455
3400
  appearanceLabel, hexLabel, valueLabel,
3456
3401
  } = componentLabels;
3457
- const { r, g, b } = color.toRgb();
3402
+ let { r, g, b } = color.toRgb();
3458
3403
  const [knob1, knob2, knob3] = controlKnobs;
3459
- const hue = roundPart(hsl.h * 360);
3404
+ const hue = roundPart(hsv.h * 360);
3460
3405
  const alpha = color.a;
3461
- const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
3462
- const saturation = roundPart(saturationSource * 100);
3463
- const lightness = roundPart(hsl.l * 100);
3464
- const hsvl = hsv.v * 100;
3465
- let colorName;
3466
-
3467
- // determine color appearance
3468
- if (lightness === 100 && saturation === 0) {
3469
- colorName = colorLabels.white;
3470
- } else if (lightness === 0) {
3471
- colorName = colorLabels.black;
3472
- } else if (saturation === 0) {
3473
- colorName = colorLabels.grey;
3474
- } else if (hue < 15 || hue >= 345) {
3475
- colorName = colorLabels.red;
3476
- } else if (hue >= 15 && hue < 45) {
3477
- colorName = hsvl > 80 && saturation > 80 ? colorLabels.orange : colorLabels.brown;
3478
- } else if (hue >= 45 && hue < 75) {
3479
- const isGold = hue > 46 && hue < 54 && hsvl < 80 && saturation > 90;
3480
- const isOlive = hue >= 54 && hue < 75 && hsvl < 80;
3481
- colorName = isGold ? colorLabels.gold : colorLabels.yellow;
3482
- colorName = isOlive ? colorLabels.olive : colorName;
3483
- } else if (hue >= 75 && hue < 155) {
3484
- colorName = hsvl < 68 ? colorLabels.green : colorLabels.lime;
3485
- } else if (hue >= 155 && hue < 175) {
3486
- colorName = colorLabels.teal;
3487
- } else if (hue >= 175 && hue < 195) {
3488
- colorName = colorLabels.cyan;
3489
- } else if (hue >= 195 && hue < 255) {
3490
- colorName = colorLabels.blue;
3491
- } else if (hue >= 255 && hue < 270) {
3492
- colorName = colorLabels.violet;
3493
- } else if (hue >= 270 && hue < 295) {
3494
- colorName = colorLabels.magenta;
3495
- } else if (hue >= 295 && hue < 345) {
3496
- colorName = colorLabels.pink;
3497
- }
3406
+ const saturation = roundPart(hsv.s * 100);
3407
+ const lightness = roundPart(hsv.v * 100);
3408
+ const colorName = self.appearance;
3498
3409
 
3499
3410
  let colorLabel = `${hexLabel} ${hex.split('').join(' ')}`;
3500
3411
 
3501
- if (format === 'hsl') {
3502
- colorLabel = `HSL: ${hue}°, ${saturation}%, ${lightness}%`;
3503
- setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3504
- setAttribute(knob1, ariaValueText, `${hue}° & ${lightness}%`);
3505
- setAttribute(knob1, ariaValueNow, `${hue}`);
3506
- setAttribute(knob2, ariaValueText, `${saturation}%`);
3507
- setAttribute(knob2, ariaValueNow, `${saturation}`);
3508
- } else if (format === 'hwb') {
3412
+ if (format === 'hwb') {
3509
3413
  const { hwb } = self;
3510
3414
  const whiteness = roundPart(hwb.w * 100);
3511
3415
  const blackness = roundPart(hwb.b * 100);
3512
3416
  colorLabel = `HWB: ${hue}°, ${whiteness}%, ${blackness}%`;
3513
- setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3514
3417
  setAttribute(knob1, ariaValueText, `${whiteness}% & ${blackness}%`);
3515
3418
  setAttribute(knob1, ariaValueNow, `${whiteness}`);
3419
+ setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3516
3420
  setAttribute(knob2, ariaValueText, `${hue}%`);
3517
3421
  setAttribute(knob2, ariaValueNow, `${hue}`);
3518
3422
  } else {
3423
+ [r, g, b] = [r, g, b].map(roundPart);
3424
+ colorLabel = format === 'hsl' ? `HSL: ${hue}°, ${saturation}%, ${lightness}%` : colorLabel;
3519
3425
  colorLabel = format === 'rgb' ? `RGB: ${r}, ${g}, ${b}` : colorLabel;
3520
- setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3426
+
3521
3427
  setAttribute(knob1, ariaValueText, `${lightness}% & ${saturation}%`);
3522
3428
  setAttribute(knob1, ariaValueNow, `${lightness}`);
3429
+ setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3523
3430
  setAttribute(knob2, ariaValueText, `${hue}°`);
3524
3431
  setAttribute(knob2, ariaValueNow, `${hue}`);
3525
3432
  }
@@ -3547,10 +3454,12 @@
3547
3454
  /** Updates the control knobs actual positions. */
3548
3455
  updateControls() {
3549
3456
  const { controlKnobs, controlPositions } = this;
3550
- const {
3457
+ let {
3551
3458
  c1x, c1y, c2y, c3y,
3552
3459
  } = controlPositions;
3553
3460
  const [control1, control2, control3] = controlKnobs;
3461
+ // round control positions
3462
+ [c1x, c1y, c2y, c3y] = [c1x, c1y, c2y, c3y].map(roundPart);
3554
3463
 
3555
3464
  setElementStyle(control1, { transform: `translate3d(${c1x - 4}px,${c1y - 4}px,0)` });
3556
3465
  setElementStyle(control2, { transform: `translate3d(0,${c2y - 4}px,0)` });
@@ -3593,7 +3502,8 @@
3593
3502
  i3.value = `${blackness}`;
3594
3503
  i4.value = `${alpha}`;
3595
3504
  } else if (format === 'rgb') {
3596
- const { r, g, b } = self.rgb;
3505
+ let { r, g, b } = self.rgb;
3506
+ [r, g, b] = [r, g, b].map(roundPart);
3597
3507
 
3598
3508
  newColor = self.color.toRgbString();
3599
3509
  i1.value = `${r}`;
@@ -3611,37 +3521,13 @@
3611
3521
  }
3612
3522
  }
3613
3523
 
3614
- /**
3615
- * The `Space` & `Enter` keys specific event listener.
3616
- * Toggle visibility of the `ColorPicker` / the presets menu, showing one will hide the other.
3617
- * @param {KeyboardEvent} e
3618
- * @this {ColorPicker}
3619
- */
3620
- keyToggle(e) {
3621
- const self = this;
3622
- const { menuToggle } = self;
3623
- const { activeElement } = getDocument(menuToggle);
3624
- const { code } = e;
3625
-
3626
- if ([keyEnter, keySpace].includes(code)) {
3627
- if ((menuToggle && activeElement === menuToggle) || !activeElement) {
3628
- e.preventDefault();
3629
- if (!activeElement) {
3630
- self.togglePicker(e);
3631
- } else {
3632
- self.toggleMenu();
3633
- }
3634
- }
3635
- }
3636
- }
3637
-
3638
3524
  /**
3639
3525
  * Toggle the `ColorPicker` dropdown visibility.
3640
- * @param {Event} e
3526
+ * @param {Event=} e
3641
3527
  * @this {ColorPicker}
3642
3528
  */
3643
3529
  togglePicker(e) {
3644
- e.preventDefault();
3530
+ if (e) e.preventDefault();
3645
3531
  const self = this;
3646
3532
  const { colorPicker } = self;
3647
3533
 
@@ -3662,8 +3548,13 @@
3662
3548
  }
3663
3549
  }
3664
3550
 
3665
- /** Toggles the visibility of the `ColorPicker` presets menu. */
3666
- toggleMenu() {
3551
+ /**
3552
+ * Toggles the visibility of the `ColorPicker` presets menu.
3553
+ * @param {Event=} e
3554
+ * @this {ColorPicker}
3555
+ */
3556
+ toggleMenu(e) {
3557
+ if (e) e.preventDefault();
3667
3558
  const self = this;
3668
3559
  const { colorMenu } = self;
3669
3560
 
@@ -3689,6 +3580,10 @@
3689
3580
  const relatedBtn = openPicker ? pickerToggle : menuToggle;
3690
3581
  const animationDuration = openDropdown && getElementTransitionDuration(openDropdown);
3691
3582
 
3583
+ // if (!self.isValid) {
3584
+ self.value = self.color.toString(true);
3585
+ // }
3586
+
3692
3587
  if (openDropdown) {
3693
3588
  removeClass(openDropdown, 'show');
3694
3589
  setAttribute(relatedBtn, ariaExpanded, 'false');
@@ -3702,9 +3597,6 @@
3702
3597
  }, animationDuration);
3703
3598
  }
3704
3599
 
3705
- if (!self.isValid) {
3706
- self.value = self.color.toString();
3707
- }
3708
3600
  if (!focusPrevented) {
3709
3601
  focus(pickerToggle);
3710
3602
  }
@@ -3753,90 +3645,82 @@
3753
3645
  * `ColorPickerElement` Web Component.
3754
3646
  * @example
3755
3647
  * <label for="UNIQUE_ID">Label</label>
3756
- * <color-picker data-format="hex" data-value="#075">
3757
- * <input id="UNIQUE_ID" type="text" class="color-preview btn-appearance">
3648
+ * <color-picker>
3649
+ * <input id="UNIQUE_ID" value="red" format="hex" class="color-preview btn-appearance">
3758
3650
  * </color-picker>
3651
+ * // or
3652
+ * <label for="UNIQUE_ID">Label</label>
3653
+ * <color-picker data-id="UNIQUE_ID" data-value="red" data-format="hex"></color-picker>
3759
3654
  */
3760
3655
  class ColorPickerElement extends HTMLElement {
3761
3656
  constructor() {
3762
3657
  super();
3763
- /** @type {boolean} */
3764
- this.isDisconnected = true;
3765
3658
  this.attachShadow({ mode: 'open' });
3766
3659
  }
3767
3660
 
3768
3661
  /**
3769
3662
  * Returns the current color value.
3770
- * @returns {string?}
3663
+ * @returns {string | undefined}
3771
3664
  */
3772
- get value() { return this.input ? this.input.value : null; }
3665
+ get value() { return this.input && this.input.value; }
3773
3666
 
3774
3667
  connectedCallback() {
3775
- if (this.colorPicker) {
3776
- if (this.isDisconnected) {
3777
- this.isDisconnected = false;
3778
- }
3779
- return;
3780
- }
3668
+ if (this.input) return;
3781
3669
 
3782
- const inputs = getElementsByTagName('input', this);
3670
+ let [input] = getElementsByTagName('input', this);
3671
+ const value = (input && getAttribute(input, 'value')) || getAttribute(this, 'data-value') || '#fff';
3672
+ const format = (input && getAttribute(input, 'format')) || getAttribute(this, 'data-format') || 'rgb';
3673
+ let id = (input && getAttribute(input, 'id')) || getAttribute(this, 'data-id');
3674
+
3675
+ if (!id) {
3676
+ id = `color-picker-${format}-${CPID}`;
3677
+ CPID += 1;
3678
+ }
3783
3679
 
3784
- if (!inputs.length) {
3785
- const label = getAttribute(this, 'data-label');
3786
- const value = getAttribute(this, 'data-value') || '#069';
3787
- const format = getAttribute(this, 'data-format') || 'rgb';
3788
- const newInput = createElement({
3680
+ if (!input) {
3681
+ input = createElement({
3789
3682
  tagName: 'input',
3790
3683
  type: 'text',
3791
3684
  className: 'color-preview btn-appearance',
3792
3685
  });
3793
- let id = getAttribute(this, 'data-id');
3794
- if (!id) {
3795
- id = `color-picker-${format}-${CPID}`;
3796
- CPID += 1;
3797
- }
3798
3686
 
3799
- const labelElement = createElement({ tagName: 'label', innerText: label || 'Color Picker' });
3800
- this.before(labelElement);
3801
- setAttribute(labelElement, 'for', id);
3802
- setAttribute(newInput, 'id', id);
3803
- setAttribute(newInput, 'name', id);
3804
- setAttribute(newInput, 'autocomplete', 'off');
3805
- setAttribute(newInput, 'spellcheck', 'false');
3806
- setAttribute(newInput, 'value', value);
3807
- this.append(newInput);
3687
+ setAttribute(input, 'id', id);
3688
+ setAttribute(input, 'name', id);
3689
+ setAttribute(input, 'autocomplete', 'off');
3690
+ setAttribute(input, 'spellcheck', 'false');
3691
+ setAttribute(input, 'value', value);
3692
+ this.append(input);
3808
3693
  }
3694
+ /** @type {HTMLInputElement} */
3695
+ // @ts-ignore - `HTMLInputElement` is `HTMLElement`
3696
+ this.input = input;
3809
3697
 
3810
- const [input] = inputs;
3811
-
3812
- if (input) {
3813
- /** @type {HTMLInputElement} */
3814
- // @ts-ignore - `HTMLInputElement` is `HTMLElement`
3815
- this.input = input;
3816
-
3817
- // @ts-ignore - `HTMLInputElement` is `HTMLElement`
3818
- this.colorPicker = new ColorPicker(input);
3819
- this.color = this.colorPicker.color;
3820
-
3821
- if (this.shadowRoot) {
3822
- this.shadowRoot.append(createElement('slot'));
3823
- }
3698
+ // @ts-ignore - `HTMLInputElement` is `HTMLElement`
3699
+ this.colorPicker = new ColorPicker(input);
3824
3700
 
3825
- this.isDisconnected = false;
3826
- }
3701
+ // @ts-ignore - `shadowRoot` is defined in the constructor
3702
+ this.shadowRoot.append(createElement('slot'));
3827
3703
  }
3828
3704
 
3705
+ /** @this {ColorPickerElement} */
3829
3706
  disconnectedCallback() {
3830
- if (this.colorPicker) this.colorPicker.dispose();
3831
- this.isDisconnected = true;
3707
+ const { input, colorPicker, shadowRoot } = this;
3708
+ if (colorPicker) colorPicker.dispose();
3709
+ if (input) input.remove();
3710
+ if (shadowRoot) shadowRoot.innerHTML = '';
3711
+
3712
+ ObjectAssign(this, {
3713
+ colorPicker: undefined,
3714
+ input: undefined,
3715
+ });
3832
3716
  }
3833
3717
  }
3834
3718
 
3835
3719
  ObjectAssign(ColorPickerElement, {
3836
3720
  Color,
3837
3721
  ColorPicker,
3838
- ColorPalette,
3839
- getInstance: getColorPickerInstance,
3722
+ ColorPalette, // @ts-ignore
3723
+ getInstance: ColorPicker.getInstance,
3840
3724
  Version,
3841
3725
  });
3842
3726
 
@@ -3844,4 +3728,4 @@
3844
3728
 
3845
3729
  return ColorPickerElement;
3846
3730
 
3847
- })));
3731
+ }));