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

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