@thednp/color-picker 0.0.1-alpha1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3051 @@
1
+ /*!
2
+ * ColorPickerElement v0.0.1alpha1 (http://thednp.github.io/color-picker)
3
+ * Copyright 2022 © thednp
4
+ * Licensed under MIT (https://github.com/thednp/color-picker/blob/master/LICENSE)
5
+ */
6
+ (function (global, factory) {
7
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
8
+ typeof define === 'function' && define.amd ? define(factory) :
9
+ (global = global || self, global.ColorPickerElement = factory());
10
+ }(this, (function () { 'use strict';
11
+
12
+ /**
13
+ * Returns the `document` or the `#document` element.
14
+ * @see https://github.com/floating-ui/floating-ui
15
+ * @param {(Node | HTMLElement | Element | globalThis)=} node
16
+ * @returns {Document}
17
+ */
18
+ function getDocument(node) {
19
+ if (node instanceof HTMLElement) return node.ownerDocument;
20
+ if (node instanceof Window) return node.document;
21
+ return window.document;
22
+ }
23
+
24
+ /**
25
+ * A global array of possible `ParentNode`.
26
+ */
27
+ const parentNodes = [Document, Element, HTMLElement];
28
+
29
+ /**
30
+ * A global array with `Element` | `HTMLElement`.
31
+ */
32
+ const elementNodes = [Element, HTMLElement];
33
+
34
+ /**
35
+ * Utility to check if target is typeof `HTMLElement`, `Element`, `Node`
36
+ * or find one that matches a selector.
37
+ *
38
+ * @param {HTMLElement | Element | string} selector the input selector or target element
39
+ * @param {(HTMLElement | Element | Document)=} parent optional node to look into
40
+ * @return {(HTMLElement | Element)?} the `HTMLElement` or `querySelector` result
41
+ */
42
+ function querySelector(selector, parent) {
43
+ const lookUp = parentNodes.some((x) => parent instanceof x)
44
+ ? parent : getDocument();
45
+
46
+ // @ts-ignore
47
+ return elementNodes.some((x) => selector instanceof x)
48
+ // @ts-ignore
49
+ ? selector : lookUp.querySelector(selector);
50
+ }
51
+
52
+ /**
53
+ * Shortcut for `Object.assign()` static method.
54
+ * @param {Record<string, any>} obj a target object
55
+ * @param {Record<string, any>} source a source object
56
+ */
57
+ const ObjectAssign = (obj, source) => Object.assign(obj, source);
58
+
59
+ /**
60
+ * This is a shortie for `document.createElement` method
61
+ * which allows you to create a new `HTMLElement` for a given `tagName`
62
+ * or based on an object with specific non-readonly attributes:
63
+ * `id`, `className`, `textContent`, `style`, etc.
64
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
65
+ *
66
+ * @param {Record<string, string> | string} param `tagName` or object
67
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
68
+ */
69
+ function createElement(param) {
70
+ if (typeof param === 'string') {
71
+ return getDocument().createElement(param);
72
+ }
73
+
74
+ const { tagName } = param;
75
+ const attr = { ...param };
76
+ const newElement = createElement(tagName);
77
+ delete attr.tagName;
78
+ ObjectAssign(newElement, attr);
79
+ return newElement;
80
+ }
81
+
82
+ /**
83
+ * Returns the `document.head` or the `<head>` element.
84
+ *
85
+ * @param {(Node | HTMLElement | Element | globalThis)=} node
86
+ * @returns {HTMLElement | HTMLHeadElement}
87
+ */
88
+ function getDocumentHead(node) {
89
+ return getDocument(node).head;
90
+ }
91
+
92
+ /**
93
+ * Shortcut for `window.getComputedStyle(element).propertyName`
94
+ * static method.
95
+ *
96
+ * * If `element` parameter is not an `HTMLElement`, `getComputedStyle`
97
+ * throws a `ReferenceError`.
98
+ *
99
+ * @param {HTMLElement | Element} element target
100
+ * @param {string} property the css property
101
+ * @return {string} the css property value
102
+ */
103
+ function getElementStyle(element, property) {
104
+ const computedStyle = getComputedStyle(element);
105
+
106
+ // @ts-ignore -- must use camelcase strings,
107
+ // or non-camelcase strings with `getPropertyValue`
108
+ return property in computedStyle ? computedStyle[property] : '';
109
+ }
110
+
111
+ /**
112
+ * Shortcut for multiple uses of `HTMLElement.style.propertyName` method.
113
+ * @param {HTMLElement | Element} element target element
114
+ * @param {Partial<CSSStyleDeclaration>} styles attribute value
115
+ */
116
+ // @ts-ignore
117
+ const setElementStyle = (element, styles) => { ObjectAssign(element.style, styles); };
118
+
119
+ /**
120
+ * A complete list of web safe colors.
121
+ * @see https://github.com/bahamas10/css-color-names/blob/master/css-color-names.json
122
+ * @type {string[]}
123
+ */
124
+ const colorNames = [
125
+ 'aliceblue',
126
+ 'antiquewhite',
127
+ 'aqua',
128
+ 'aquamarine',
129
+ 'azure',
130
+ 'beige',
131
+ 'bisque',
132
+ 'black',
133
+ 'blanchedalmond',
134
+ 'blue',
135
+ 'blueviolet',
136
+ 'brown',
137
+ 'burlywood',
138
+ 'cadetblue',
139
+ 'chartreuse',
140
+ 'chocolate',
141
+ 'coral',
142
+ 'cornflowerblue',
143
+ 'cornsilk',
144
+ 'crimson',
145
+ 'cyan',
146
+ 'darkblue',
147
+ 'darkcyan',
148
+ 'darkgoldenrod',
149
+ 'darkgray',
150
+ 'darkgreen',
151
+ 'darkgrey',
152
+ 'darkkhaki',
153
+ 'darkmagenta',
154
+ 'darkolivegreen',
155
+ 'darkorange',
156
+ 'darkorchid',
157
+ 'darkred',
158
+ 'darksalmon',
159
+ 'darkseagreen',
160
+ 'darkslateblue',
161
+ 'darkslategray',
162
+ 'darkslategrey',
163
+ 'darkturquoise',
164
+ 'darkviolet',
165
+ 'deeppink',
166
+ 'deepskyblue',
167
+ 'dimgray',
168
+ 'dimgrey',
169
+ 'dodgerblue',
170
+ 'firebrick',
171
+ 'floralwhite',
172
+ 'forestgreen',
173
+ 'fuchsia',
174
+ 'gainsboro',
175
+ 'ghostwhite',
176
+ 'goldenrod',
177
+ 'gold',
178
+ 'gray',
179
+ 'green',
180
+ 'greenyellow',
181
+ 'grey',
182
+ 'honeydew',
183
+ 'hotpink',
184
+ 'indianred',
185
+ 'indigo',
186
+ 'ivory',
187
+ 'khaki',
188
+ 'lavenderblush',
189
+ 'lavender',
190
+ 'lawngreen',
191
+ 'lemonchiffon',
192
+ 'lightblue',
193
+ 'lightcoral',
194
+ 'lightcyan',
195
+ 'lightgoldenrodyellow',
196
+ 'lightgray',
197
+ 'lightgreen',
198
+ 'lightgrey',
199
+ 'lightpink',
200
+ 'lightsalmon',
201
+ 'lightseagreen',
202
+ 'lightskyblue',
203
+ 'lightslategray',
204
+ 'lightslategrey',
205
+ 'lightsteelblue',
206
+ 'lightyellow',
207
+ 'lime',
208
+ 'limegreen',
209
+ 'linen',
210
+ 'magenta',
211
+ 'maroon',
212
+ 'mediumaquamarine',
213
+ 'mediumblue',
214
+ 'mediumorchid',
215
+ 'mediumpurple',
216
+ 'mediumseagreen',
217
+ 'mediumslateblue',
218
+ 'mediumspringgreen',
219
+ 'mediumturquoise',
220
+ 'mediumvioletred',
221
+ 'midnightblue',
222
+ 'mintcream',
223
+ 'mistyrose',
224
+ 'moccasin',
225
+ 'navajowhite',
226
+ 'navy',
227
+ 'oldlace',
228
+ 'olive',
229
+ 'olivedrab',
230
+ 'orange',
231
+ 'orangered',
232
+ 'orchid',
233
+ 'palegoldenrod',
234
+ 'palegreen',
235
+ 'paleturquoise',
236
+ 'palevioletred',
237
+ 'papayawhip',
238
+ 'peachpuff',
239
+ 'peru',
240
+ 'pink',
241
+ 'plum',
242
+ 'powderblue',
243
+ 'purple',
244
+ 'rebeccapurple',
245
+ 'red',
246
+ 'rosybrown',
247
+ 'royalblue',
248
+ 'saddlebrown',
249
+ 'salmon',
250
+ 'sandybrown',
251
+ 'seagreen',
252
+ 'seashell',
253
+ 'sienna',
254
+ 'silver',
255
+ 'skyblue',
256
+ 'slateblue',
257
+ 'slategray',
258
+ 'slategrey',
259
+ 'snow',
260
+ 'springgreen',
261
+ 'steelblue',
262
+ 'tan',
263
+ 'teal',
264
+ 'thistle',
265
+ 'tomato',
266
+ 'turquoise',
267
+ 'violet',
268
+ 'wheat',
269
+ 'white',
270
+ 'whitesmoke',
271
+ 'yellow',
272
+ 'yellowgreen',
273
+ ];
274
+
275
+ // <http://www.w3.org/TR/css3-values/#integers>
276
+ const CSS_INTEGER = '[-\\+]?\\d+%?';
277
+
278
+ // <http://www.w3.org/TR/css3-values/#number-value>
279
+ const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
280
+
281
+ // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
282
+ const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
283
+
284
+ // Actual matching.
285
+ // Parentheses and commas are optional, but not required.
286
+ // Whitespace can take the place of commas or opening paren
287
+ const PERMISSIVE_MATCH3 = `[\\s|\\(]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})\\s*\\)?`;
288
+ const PERMISSIVE_MATCH4 = `[\\s|\\(]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})\\s*\\)?`;
289
+
290
+ const matchers = {
291
+ CSS_UNIT: new RegExp(CSS_UNIT),
292
+ rgb: new RegExp(`rgb${PERMISSIVE_MATCH3}`),
293
+ rgba: new RegExp(`rgba${PERMISSIVE_MATCH4}`),
294
+ hsl: new RegExp(`hsl${PERMISSIVE_MATCH3}`),
295
+ hsla: new RegExp(`hsla${PERMISSIVE_MATCH4}`),
296
+ hsv: new RegExp(`hsv${PERMISSIVE_MATCH3}`),
297
+ hsva: new RegExp(`hsva${PERMISSIVE_MATCH4}`),
298
+ hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
299
+ hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
300
+ hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
301
+ hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
302
+ };
303
+
304
+ /**
305
+ * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
306
+ * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
307
+ * @param {string} n
308
+ * @returns {boolean}
309
+ */
310
+ function isOnePointZero(n) {
311
+ return typeof n === 'string' && n.includes('.') && parseFloat(n) === 1;
312
+ }
313
+
314
+ /**
315
+ * Check to see if string passed in is a percentage
316
+ * @param {string} n
317
+ * @returns {boolean}
318
+ */
319
+ function isPercentage(n) {
320
+ return typeof n === 'string' && n.includes('%');
321
+ }
322
+
323
+ /**
324
+ * Check to see if it looks like a CSS unit
325
+ * (see `matchers` above for definition).
326
+ * @param {string | number} color
327
+ * @returns {boolean}
328
+ */
329
+ function isValidCSSUnit(color) {
330
+ return Boolean(matchers.CSS_UNIT.exec(String(color)));
331
+ }
332
+
333
+ /**
334
+ * Take input from [0, n] and return it as [0, 1]
335
+ * @param {*} n
336
+ * @param {number} max
337
+ * @returns {number}
338
+ */
339
+ function bound01(n, max) {
340
+ let N = n;
341
+ if (isOnePointZero(n)) N = '100%';
342
+
343
+ N = max === 360 ? N : Math.min(max, Math.max(0, parseFloat(N)));
344
+
345
+ // Automatically convert percentage into number
346
+ if (isPercentage(N)) {
347
+ N = parseInt(String(N * max), 10) / 100;
348
+ }
349
+ // Handle floating point rounding errors
350
+ if (Math.abs(N - max) < 0.000001) {
351
+ return 1;
352
+ }
353
+ // Convert into [0, 1] range if it isn't already
354
+ if (max === 360) {
355
+ // If n is a hue given in degrees,
356
+ // wrap around out-of-range values into [0, 360] range
357
+ // then convert into [0, 1].
358
+ N = (N < 0 ? (N % max) + max : N % max) / parseFloat(String(max));
359
+ } else {
360
+ // If n not a hue given in degrees
361
+ // Convert into [0, 1] range if it isn't already.
362
+ N = (N % max) / parseFloat(String(max));
363
+ }
364
+ return N;
365
+ }
366
+
367
+ /**
368
+ * Return a valid alpha value [0,1] with all invalid values being set to 1.
369
+ * @param {string | number} a
370
+ * @returns {number}
371
+ */
372
+ function boundAlpha(a) {
373
+ // @ts-ignore
374
+ let na = parseFloat(a);
375
+
376
+ if (Number.isNaN(na) || na < 0 || na > 1) {
377
+ na = 1;
378
+ }
379
+
380
+ return na;
381
+ }
382
+
383
+ /**
384
+ * Force a number between 0 and 1
385
+ * @param {number} val
386
+ * @returns {number}
387
+ */
388
+ function clamp01(val) {
389
+ return Math.min(1, Math.max(0, val));
390
+ }
391
+
392
+ /**
393
+ * Returns the hexadecimal value of a web safe colour.
394
+ * @param {string} name
395
+ * @returns {string}
396
+ */
397
+ function getHexFromColorName(name) {
398
+ const documentHead = getDocumentHead();
399
+ setElementStyle(documentHead, { color: name });
400
+ const colorName = getElementStyle(documentHead, 'color');
401
+ setElementStyle(documentHead, { color: '' });
402
+ return colorName;
403
+ }
404
+
405
+ /**
406
+ * Replace a decimal with it's percentage value
407
+ * @param {number | string} n
408
+ * @return {string | number}
409
+ */
410
+ function convertToPercentage(n) {
411
+ if (n <= 1) {
412
+ return `${Number(n) * 100}%`;
413
+ }
414
+ return n;
415
+ }
416
+
417
+ /**
418
+ * Force a hex value to have 2 characters
419
+ * @param {string} c
420
+ * @returns {string}
421
+ */
422
+ function pad2(c) {
423
+ return c.length === 1 ? `0${c}` : String(c);
424
+ }
425
+
426
+ // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
427
+ // <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
428
+ /**
429
+ * Handle bounds / percentage checking to conform to CSS color spec
430
+ * * *Assumes:* r, g, b in [0, 255] or [0, 1]
431
+ * * *Returns:* { r, g, b } in [0, 255]
432
+ * @see http://www.w3.org/TR/css3-color/
433
+ * @param {number | string} r
434
+ * @param {number | string} g
435
+ * @param {number | string} b
436
+ * @returns {CP.RGB}
437
+ */
438
+ function rgbToRgb(r, g, b) {
439
+ return {
440
+ r: bound01(r, 255) * 255,
441
+ g: bound01(g, 255) * 255,
442
+ b: bound01(b, 255) * 255,
443
+ };
444
+ }
445
+
446
+ /**
447
+ * Converts an RGB color value to HSL.
448
+ * *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
449
+ * *Returns:* { h, s, l } in [0,1]
450
+ * @param {number} R
451
+ * @param {number} G
452
+ * @param {number} B
453
+ * @returns {CP.HSL}
454
+ */
455
+ function rgbToHsl(R, G, B) {
456
+ const r = bound01(R, 255);
457
+ const g = bound01(G, 255);
458
+ const b = bound01(B, 255);
459
+ const max = Math.max(r, g, b);
460
+ const min = Math.min(r, g, b);
461
+ let h = 0;
462
+ let s = 0;
463
+ const l = (max + min) / 2;
464
+ if (max === min) {
465
+ s = 0;
466
+ h = 0; // achromatic
467
+ } else {
468
+ const d = max - min;
469
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
470
+ switch (max) {
471
+ case r:
472
+ h = (g - b) / d + (g < b ? 6 : 0);
473
+ break;
474
+ case g:
475
+ h = (b - r) / d + 2;
476
+ break;
477
+ case b:
478
+ h = (r - g) / d + 4;
479
+ break;
480
+ }
481
+ h /= 6;
482
+ }
483
+ return { h, s, l };
484
+ }
485
+
486
+ /**
487
+ * Returns a normalized RGB component value.
488
+ * @param {number} P
489
+ * @param {number} Q
490
+ * @param {number} T
491
+ * @returns {number}
492
+ */
493
+ function hue2rgb(P, Q, T) {
494
+ const p = P;
495
+ const q = Q;
496
+ let t = T;
497
+ if (t < 0) {
498
+ t += 1;
499
+ }
500
+ if (t > 1) {
501
+ t -= 1;
502
+ }
503
+ if (t < 1 / 6) {
504
+ return p + (q - p) * (6 * t);
505
+ }
506
+ if (t < 1 / 2) {
507
+ return q;
508
+ }
509
+ if (t < 2 / 3) {
510
+ return p + (q - p) * (2 / 3 - t) * 6;
511
+ }
512
+ return p;
513
+ }
514
+
515
+ /**
516
+ * Converts an HSL colour value to RGB.
517
+ *
518
+ * * *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
519
+ * * *Returns:* { r, g, b } in the set [0, 255]
520
+ * @param {number | string} H
521
+ * @param {number | string} S
522
+ * @param {number | string} L
523
+ * @returns {CP.RGB}
524
+ */
525
+ function hslToRgb(H, S, L) {
526
+ let r = 0;
527
+ let g = 0;
528
+ let b = 0;
529
+ const h = bound01(H, 360);
530
+ const s = bound01(S, 100);
531
+ const l = bound01(L, 100);
532
+
533
+ if (s === 0) {
534
+ // achromatic
535
+ g = l;
536
+ b = l;
537
+ r = l;
538
+ } else {
539
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
540
+ const p = 2 * l - q;
541
+ r = hue2rgb(p, q, h + 1 / 3);
542
+ g = hue2rgb(p, q, h);
543
+ b = hue2rgb(p, q, h - 1 / 3);
544
+ }
545
+ return { r: r * 255, g: g * 255, b: b * 255 };
546
+ }
547
+
548
+ /**
549
+ * Converts an RGB colour value to HSV.
550
+ *
551
+ * *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
552
+ * *Returns:* { h, s, v } in [0,1]
553
+ * @param {number | string} R
554
+ * @param {number | string} G
555
+ * @param {number | string} B
556
+ * @returns {CP.HSV}
557
+ */
558
+ function rgbToHsv(R, G, B) {
559
+ const r = bound01(R, 255);
560
+ const g = bound01(G, 255);
561
+ const b = bound01(B, 255);
562
+ const max = Math.max(r, g, b);
563
+ const min = Math.min(r, g, b);
564
+ let h = 0;
565
+ const v = max;
566
+ const d = max - min;
567
+ const s = max === 0 ? 0 : d / max;
568
+ if (max === min) {
569
+ h = 0; // achromatic
570
+ } else {
571
+ switch (max) {
572
+ case r:
573
+ h = (g - b) / d + (g < b ? 6 : 0);
574
+ break;
575
+ case g:
576
+ h = (b - r) / d + 2;
577
+ break;
578
+ case b:
579
+ h = (r - g) / d + 4;
580
+ break;
581
+ }
582
+ h /= 6;
583
+ }
584
+ return { h, s, v };
585
+ }
586
+
587
+ /**
588
+ * Converts an HSV color value to RGB.
589
+ *
590
+ * *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
591
+ * *Returns:* { r, g, b } in the set [0, 255]
592
+ * @param {number | string} H
593
+ * @param {number | string} S
594
+ * @param {number | string} V
595
+ * @returns {CP.RGB}
596
+ */
597
+ function hsvToRgb(H, S, V) {
598
+ const h = bound01(H, 360) * 6;
599
+ const s = bound01(S, 100);
600
+ const v = bound01(V, 100);
601
+ const i = Math.floor(h);
602
+ const f = h - i;
603
+ const p = v * (1 - s);
604
+ const q = v * (1 - f * s);
605
+ const t = v * (1 - (1 - f) * s);
606
+ const mod = i % 6;
607
+ const r = [v, q, p, p, t, v][mod];
608
+ const g = [t, v, v, q, p, p][mod];
609
+ const b = [p, p, t, v, v, q][mod];
610
+ return { r: r * 255, g: g * 255, b: b * 255 };
611
+ }
612
+
613
+ /**
614
+ * Converts an RGB color to hex
615
+ *
616
+ * Assumes r, g, and b are contained in the set [0, 255]
617
+ * Returns a 3 or 6 character hex
618
+ * @param {number} r
619
+ * @param {number} g
620
+ * @param {number} b
621
+ * @returns {string}
622
+ */
623
+ function rgbToHex(r, g, b) {
624
+ const hex = [
625
+ pad2(Math.round(r).toString(16)),
626
+ pad2(Math.round(g).toString(16)),
627
+ pad2(Math.round(b).toString(16)),
628
+ ];
629
+
630
+ return hex.join('');
631
+ }
632
+
633
+ /**
634
+ * Converts a hex value to a decimal.
635
+ * @param {string} h
636
+ * @returns {number}
637
+ */
638
+ function convertHexToDecimal(h) {
639
+ return parseIntFromHex(h) / 255;
640
+ }
641
+
642
+ /**
643
+ * Parse a base-16 hex value into a base-10 integer.
644
+ * @param {string} val
645
+ * @returns {number}
646
+ */
647
+ function parseIntFromHex(val) {
648
+ return parseInt(val, 16);
649
+ }
650
+
651
+ /**
652
+ * Returns an `{r,g,b}` color object corresponding to a given number.
653
+ * @param {number} color
654
+ * @returns {CP.RGB}
655
+ */
656
+ function numberInputToObject(color) {
657
+ /* eslint-disable no-bitwise */
658
+ return {
659
+ r: color >> 16,
660
+ g: (color & 0xff00) >> 8,
661
+ b: color & 0xff,
662
+ };
663
+ /* eslint-enable no-bitwise */
664
+ }
665
+
666
+ /**
667
+ * Permissive string parsing. Take in a number of formats, and output an object
668
+ * based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
669
+ * @param {string} input
670
+ * @returns {Record<string, (number | string)> | false}
671
+ */
672
+ function stringInputToObject(input) {
673
+ let color = input.trim().toLowerCase();
674
+ if (color.length === 0) {
675
+ return {
676
+ r: 0, g: 0, b: 0, a: 0,
677
+ };
678
+ }
679
+ let named = false;
680
+ if (colorNames.includes(color)) {
681
+ color = getHexFromColorName(color);
682
+ named = true;
683
+ } else if (color === 'transparent') {
684
+ return {
685
+ r: 0, g: 0, b: 0, a: 0, format: 'name',
686
+ };
687
+ }
688
+
689
+ // Try to match string input using regular expressions.
690
+ // Keep most of the number bounding out of this function,
691
+ // don't worry about [0,1] or [0,100] or [0,360]
692
+ // Just return an object and let the conversion functions handle that.
693
+ // This way the result will be the same whether Color is initialized with string or object.
694
+ let match = matchers.rgb.exec(color);
695
+ if (match) {
696
+ return { r: match[1], g: match[2], b: match[3] };
697
+ }
698
+ match = matchers.rgba.exec(color);
699
+ if (match) {
700
+ return {
701
+ r: match[1], g: match[2], b: match[3], a: match[4],
702
+ };
703
+ }
704
+ match = matchers.hsl.exec(color);
705
+ if (match) {
706
+ return { h: match[1], s: match[2], l: match[3] };
707
+ }
708
+ match = matchers.hsla.exec(color);
709
+ if (match) {
710
+ return {
711
+ h: match[1], s: match[2], l: match[3], a: match[4],
712
+ };
713
+ }
714
+ match = matchers.hsv.exec(color);
715
+ if (match) {
716
+ return { h: match[1], s: match[2], v: match[3] };
717
+ }
718
+ match = matchers.hsva.exec(color);
719
+ if (match) {
720
+ return {
721
+ h: match[1], s: match[2], v: match[3], a: match[4],
722
+ };
723
+ }
724
+ match = matchers.hex8.exec(color);
725
+ if (match) {
726
+ return {
727
+ r: parseIntFromHex(match[1]),
728
+ g: parseIntFromHex(match[2]),
729
+ b: parseIntFromHex(match[3]),
730
+ a: convertHexToDecimal(match[4]),
731
+ format: named ? 'name' : 'hex8',
732
+ };
733
+ }
734
+ match = matchers.hex6.exec(color);
735
+ if (match) {
736
+ return {
737
+ r: parseIntFromHex(match[1]),
738
+ g: parseIntFromHex(match[2]),
739
+ b: parseIntFromHex(match[3]),
740
+ format: named ? 'name' : 'hex',
741
+ };
742
+ }
743
+ match = matchers.hex4.exec(color);
744
+ if (match) {
745
+ return {
746
+ r: parseIntFromHex(match[1] + match[1]),
747
+ g: parseIntFromHex(match[2] + match[2]),
748
+ b: parseIntFromHex(match[3] + match[3]),
749
+ a: convertHexToDecimal(match[4] + match[4]),
750
+ format: named ? 'name' : 'hex8',
751
+ };
752
+ }
753
+ match = matchers.hex3.exec(color);
754
+ if (match) {
755
+ return {
756
+ r: parseIntFromHex(match[1] + match[1]),
757
+ g: parseIntFromHex(match[2] + match[2]),
758
+ b: parseIntFromHex(match[3] + match[3]),
759
+ format: named ? 'name' : 'hex',
760
+ };
761
+ }
762
+ return false;
763
+ }
764
+
765
+ /**
766
+ * Given a string or object, convert that input to RGB
767
+ *
768
+ * Possible string inputs:
769
+ * ```
770
+ * "red"
771
+ * "#f00" or "f00"
772
+ * "#ff0000" or "ff0000"
773
+ * "#ff000000" or "ff000000"
774
+ * "rgb 255 0 0" or "rgb (255, 0, 0)"
775
+ * "rgb 1.0 0 0" or "rgb (1, 0, 0)"
776
+ * "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
777
+ * "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
778
+ * "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
779
+ * "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
780
+ * "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
781
+ * ```
782
+ * @param {string | Record<string, any>} input
783
+ * @returns {CP.ColorObject}
784
+ */
785
+ function inputToRGB(input) {
786
+ /** @type {CP.RGB} */
787
+ let rgb = { r: 0, g: 0, b: 0 };
788
+ let color = input;
789
+ let a;
790
+ let s = null;
791
+ let v = null;
792
+ let l = null;
793
+ let ok = false;
794
+ let format = 'hex';
795
+
796
+ if (typeof input === 'string') {
797
+ // @ts-ignore -- this now is converted to object
798
+ color = stringInputToObject(input);
799
+ if (color) ok = true;
800
+ }
801
+ if (typeof color === 'object') {
802
+ if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
803
+ rgb = rgbToRgb(color.r, color.g, color.b);
804
+ ok = true;
805
+ format = `${color.r}`.slice(-1) === '%' ? 'prgb' : 'rgb';
806
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
807
+ s = convertToPercentage(color.s);
808
+ v = convertToPercentage(color.v);
809
+ rgb = hsvToRgb(color.h, s, v);
810
+ ok = true;
811
+ format = 'hsv';
812
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
813
+ s = convertToPercentage(color.s);
814
+ l = convertToPercentage(color.l);
815
+ rgb = hslToRgb(color.h, s, l);
816
+ ok = true;
817
+ format = 'hsl';
818
+ }
819
+ if ('a' in color) a = color.a;
820
+ }
821
+
822
+ return {
823
+ ok, // @ts-ignore
824
+ format: color.format || format,
825
+ r: Math.min(255, Math.max(rgb.r, 0)),
826
+ g: Math.min(255, Math.max(rgb.g, 0)),
827
+ b: Math.min(255, Math.max(rgb.b, 0)),
828
+ a: boundAlpha(a),
829
+ };
830
+ }
831
+
832
+ /** @type {CP.ColorOptions} */
833
+ const colorPickerDefaults = {
834
+ format: 'hex',
835
+ };
836
+
837
+ /**
838
+ * Returns a new `Color` instance.
839
+ * @see https://github.com/bgrins/TinyColor
840
+ * @class
841
+ */
842
+ class Color {
843
+ /**
844
+ * @constructor
845
+ * @param {CP.ColorInput} input
846
+ * @param {CP.ColorOptions=} config
847
+ */
848
+ constructor(input, config) {
849
+ let color = input;
850
+ const opts = typeof config === 'object'
851
+ ? ObjectAssign(colorPickerDefaults, config)
852
+ : ObjectAssign({}, colorPickerDefaults);
853
+
854
+ // If input is already a `Color`, return itself
855
+ if (color instanceof Color) {
856
+ color = inputToRGB(color);
857
+ }
858
+ if (typeof color === 'number') {
859
+ color = numberInputToObject(color);
860
+ }
861
+ const {
862
+ r, g, b, a, ok, format,
863
+ } = inputToRGB(color);
864
+
865
+ /** @type {CP.ColorInput} */
866
+ this.originalInput = color;
867
+ /** @type {number} */
868
+ this.r = r;
869
+ /** @type {number} */
870
+ this.g = g;
871
+ /** @type {number} */
872
+ this.b = b;
873
+ /** @type {number} */
874
+ this.a = a;
875
+ /** @type {boolean} */
876
+ this.ok = ok;
877
+ /** @type {number} */
878
+ this.roundA = Math.round(100 * this.a) / 100;
879
+ /** @type {CP.ColorFormats} */
880
+ this.format = opts.format || format;
881
+
882
+ // Don't let the range of [0,255] come back in [0,1].
883
+ // Potentially lose a little bit of precision here, but will fix issues where
884
+ // .5 gets interpreted as half of the total, instead of half of 1
885
+ // If it was supposed to be 128, this was already taken care of by `inputToRgb`
886
+ if (this.r < 1) {
887
+ this.r = Math.round(this.r);
888
+ }
889
+ if (this.g < 1) {
890
+ this.g = Math.round(this.g);
891
+ }
892
+ if (this.b < 1) {
893
+ this.b = Math.round(this.b);
894
+ }
895
+ }
896
+
897
+ /**
898
+ * Checks if the current input value is a valid colour.
899
+ * @returns {boolean} the query result
900
+ */
901
+ get isValid() {
902
+ return this.ok;
903
+ }
904
+
905
+ /**
906
+ * Checks if the current colour requires a light text colour.
907
+ * @returns {boolean} the query result
908
+ */
909
+ get isDark() {
910
+ return this.brightness < 128;
911
+ }
912
+
913
+ /**
914
+ * Returns the perceived luminance of a color.
915
+ * @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
916
+ * @returns {number} a number in [0-1] range
917
+ */
918
+ get luminance() {
919
+ const { r, g, b } = this;
920
+ let R = 0;
921
+ let G = 0;
922
+ let B = 0;
923
+ const RsRGB = r / 255;
924
+ const GsRGB = g / 255;
925
+ const BsRGB = b / 255;
926
+
927
+ if (RsRGB <= 0.03928) {
928
+ R = RsRGB / 12.92;
929
+ } else {
930
+ R = ((RsRGB + 0.055) / 1.055) ** 2.4;
931
+ }
932
+ if (GsRGB <= 0.03928) {
933
+ G = GsRGB / 12.92;
934
+ } else {
935
+ G = ((GsRGB + 0.055) / 1.055) ** 2.4;
936
+ }
937
+ if (BsRGB <= 0.03928) {
938
+ B = BsRGB / 12.92;
939
+ } else {
940
+ B = ((BsRGB + 0.055) / 1.055) ** 2.4;
941
+ }
942
+ return 0.2126 * R + 0.7152 * G + 0.0722 * B;
943
+ }
944
+
945
+ /**
946
+ * Returns the perceived brightness of the color.
947
+ * @returns {number} a number in [0-255] range
948
+ */
949
+ get brightness() {
950
+ const { r, g, b } = this;
951
+ return (r * 299 + g * 587 + b * 114) / 1000;
952
+ }
953
+
954
+ /**
955
+ * Returns the color as a RGBA object.
956
+ * @returns {CP.RGBA}
957
+ */
958
+ toRgb() {
959
+ return {
960
+ r: Math.round(this.r),
961
+ g: Math.round(this.g),
962
+ b: Math.round(this.b),
963
+ a: this.a,
964
+ };
965
+ }
966
+
967
+ /**
968
+ * Returns the RGBA values concatenated into a string.
969
+ * @returns {string} the CSS valid color in RGB/RGBA format
970
+ */
971
+ toRgbString() {
972
+ const r = Math.round(this.r);
973
+ const g = Math.round(this.g);
974
+ const b = Math.round(this.b);
975
+ return this.a === 1
976
+ ? `rgb(${r},${g},${b})`
977
+ : `rgba(${r},${g},${b},${this.roundA})`;
978
+ }
979
+
980
+ /**
981
+ * Returns the HEX value of the color.
982
+ * @returns {string} the hexadecimal color format
983
+ */
984
+ toHex() {
985
+ return rgbToHex(this.r, this.g, this.b);
986
+ }
987
+
988
+ /**
989
+ * Returns the HEX value of the color.
990
+ * @returns {string} the CSS valid color in hexadecimal format
991
+ */
992
+ toHexString() {
993
+ return `#${this.toHex()}`;
994
+ }
995
+
996
+ /**
997
+ * Returns the color as a HSVA object.
998
+ * @returns {CP.HSVA} the `{h,s,v,a}` object
999
+ */
1000
+ toHsv() {
1001
+ const { h, s, v } = rgbToHsv(this.r, this.g, this.b);
1002
+ return {
1003
+ h: h * 360, s, v, a: this.a,
1004
+ };
1005
+ }
1006
+
1007
+ /**
1008
+ * Returns the color as a HSLA object.
1009
+ * @returns {CP.HSLA}
1010
+ */
1011
+ toHsl() {
1012
+ const { h, s, l } = rgbToHsl(this.r, this.g, this.b);
1013
+ return {
1014
+ h: h * 360, s, l, a: this.a,
1015
+ };
1016
+ }
1017
+
1018
+ /**
1019
+ * Returns the HSLA values concatenated into a string.
1020
+ * @returns {string} the CSS valid color in HSL/HSLA format
1021
+ */
1022
+ toHslString() {
1023
+ const hsl = this.toHsl();
1024
+ const h = Math.round(hsl.h);
1025
+ const s = Math.round(hsl.s * 100);
1026
+ const l = Math.round(hsl.l * 100);
1027
+ return this.a === 1
1028
+ ? `hsl(${h},${s}%,${l}%)`
1029
+ : `hsla(${h},${s}%,${l}%,${this.roundA})`;
1030
+ }
1031
+
1032
+ /**
1033
+ * Sets the alpha value on the current color.
1034
+ * @param {number} alpha a new alpha value in [0-1] range.
1035
+ * @returns {Color} a new `Color` instance
1036
+ */
1037
+ setAlpha(alpha) {
1038
+ this.a = boundAlpha(alpha);
1039
+ this.roundA = Math.round(100 * this.a) / 100;
1040
+ return this;
1041
+ }
1042
+
1043
+ /**
1044
+ * Saturate the color with a given amount.
1045
+ * @param {number=} amount a value in [0-100] range
1046
+ * @returns {Color} a new `Color` instance
1047
+ */
1048
+ saturate(amount) {
1049
+ if (!amount) return this;
1050
+ const hsl = this.toHsl();
1051
+ hsl.s += amount / 100;
1052
+ hsl.s = clamp01(hsl.s);
1053
+ return new Color(hsl);
1054
+ }
1055
+
1056
+ /**
1057
+ * Desaturate the color with a given amount.
1058
+ * @param {number=} amount a value in [0-100] range
1059
+ * @returns {Color} a new `Color` instance
1060
+ */
1061
+ desaturate(amount) {
1062
+ return amount ? this.saturate(-amount) : this;
1063
+ }
1064
+
1065
+ /**
1066
+ * Completely desaturates a color into greyscale.
1067
+ * Same as calling `desaturate(100)`
1068
+ * @returns {Color} a new `Color` instance
1069
+ */
1070
+ greyscale() {
1071
+ return this.desaturate(100);
1072
+ }
1073
+
1074
+ /** Returns a clone of the current `Color` instance. */
1075
+ clone() {
1076
+ return new Color(this);
1077
+ }
1078
+
1079
+ /**
1080
+ * Returns the color value in CSS valid string format.
1081
+ * @returns {string}
1082
+ */
1083
+ toString() {
1084
+ const { format } = this;
1085
+
1086
+ if (format === 'rgb') {
1087
+ return this.toRgbString();
1088
+ }
1089
+ if (format === 'hsl') {
1090
+ return this.toHslString();
1091
+ }
1092
+ return this.toHexString();
1093
+ }
1094
+ }
1095
+
1096
+ ObjectAssign(Color, {
1097
+ colorNames,
1098
+ CSS_INTEGER,
1099
+ CSS_NUMBER,
1100
+ CSS_UNIT,
1101
+ PERMISSIVE_MATCH3,
1102
+ PERMISSIVE_MATCH4,
1103
+ matchers,
1104
+ isOnePointZero,
1105
+ isPercentage,
1106
+ isValidCSSUnit,
1107
+ bound01,
1108
+ boundAlpha,
1109
+ clamp01,
1110
+ getHexFromColorName,
1111
+ convertToPercentage,
1112
+ convertHexToDecimal,
1113
+ pad2,
1114
+ rgbToRgb,
1115
+ rgbToHsl,
1116
+ rgbToHex,
1117
+ rgbToHsv,
1118
+ hslToRgb,
1119
+ hsvToRgb,
1120
+ hue2rgb,
1121
+ parseIntFromHex,
1122
+ numberInputToObject,
1123
+ stringInputToObject,
1124
+ inputToRGB,
1125
+ });
1126
+
1127
+ /** @type {Record<string, any>} */
1128
+ const EventRegistry = {};
1129
+
1130
+ /**
1131
+ * The global event listener.
1132
+ *
1133
+ * @this {Element | HTMLElement | Window | Document}
1134
+ * @param {Event} e
1135
+ * @returns {void}
1136
+ */
1137
+ function globalListener(e) {
1138
+ const that = this;
1139
+ const { type } = e;
1140
+ const oneEvMap = EventRegistry[type] ? [...EventRegistry[type]] : [];
1141
+
1142
+ oneEvMap.forEach((elementsMap) => {
1143
+ const [element, listenersMap] = elementsMap;
1144
+ [...listenersMap].forEach((listenerMap) => {
1145
+ if (element === that) {
1146
+ const [listener, options] = listenerMap;
1147
+ listener.apply(element, [e]);
1148
+
1149
+ if (options && options.once) {
1150
+ removeListener(element, type, listener, options);
1151
+ }
1152
+ }
1153
+ });
1154
+ });
1155
+ }
1156
+
1157
+ /**
1158
+ * Register a new listener with its options and attach the `globalListener`
1159
+ * to the target if this is the first listener.
1160
+ *
1161
+ * @param {Element | HTMLElement | Window | Document} element
1162
+ * @param {string} eventType
1163
+ * @param {EventListenerObject['handleEvent']} listener
1164
+ * @param {AddEventListenerOptions=} options
1165
+ */
1166
+ const addListener = (element, eventType, listener, options) => {
1167
+ // get element listeners first
1168
+ if (!EventRegistry[eventType]) {
1169
+ EventRegistry[eventType] = new Map();
1170
+ }
1171
+ const oneEventMap = EventRegistry[eventType];
1172
+
1173
+ if (!oneEventMap.has(element)) {
1174
+ oneEventMap.set(element, new Map());
1175
+ }
1176
+ const oneElementMap = oneEventMap.get(element);
1177
+
1178
+ // get listeners size
1179
+ const { size } = oneElementMap;
1180
+
1181
+ // register listener with its options
1182
+ if (oneElementMap) {
1183
+ oneElementMap.set(listener, options);
1184
+ }
1185
+
1186
+ // add listener last
1187
+ if (!size) {
1188
+ element.addEventListener(eventType, globalListener, options);
1189
+ }
1190
+ };
1191
+
1192
+ /**
1193
+ * Remove a listener from registry and detach the `globalListener`
1194
+ * if no listeners are found in the registry.
1195
+ *
1196
+ * @param {Element | HTMLElement | Window | Document} element
1197
+ * @param {string} eventType
1198
+ * @param {EventListenerObject['handleEvent']} listener
1199
+ * @param {AddEventListenerOptions=} options
1200
+ */
1201
+ const removeListener = (element, eventType, listener, options) => {
1202
+ // get listener first
1203
+ const oneEventMap = EventRegistry[eventType];
1204
+ const oneElementMap = oneEventMap && oneEventMap.get(element);
1205
+ const savedOptions = oneElementMap && oneElementMap.get(listener);
1206
+
1207
+ // also recover initial options
1208
+ const { options: eventOptions } = savedOptions !== undefined
1209
+ ? savedOptions
1210
+ : { options };
1211
+
1212
+ // unsubscribe second, remove from registry
1213
+ if (oneElementMap && oneElementMap.has(listener)) oneElementMap.delete(listener);
1214
+ if (oneEventMap && (!oneElementMap || !oneElementMap.size)) oneEventMap.delete(element);
1215
+ if (!oneEventMap || !oneEventMap.size) delete EventRegistry[eventType];
1216
+
1217
+ // remove listener last
1218
+ if (!oneElementMap || !oneElementMap.size) {
1219
+ element.removeEventListener(eventType, globalListener, eventOptions);
1220
+ }
1221
+ };
1222
+
1223
+ /**
1224
+ * A global namespace for aria-selected.
1225
+ * @type {string}
1226
+ */
1227
+ const ariaSelected = 'aria-selected';
1228
+
1229
+ /**
1230
+ * A global namespace for aria-expanded.
1231
+ * @type {string}
1232
+ */
1233
+ const ariaExpanded = 'aria-expanded';
1234
+
1235
+ /**
1236
+ * A global namespace for aria-hidden.
1237
+ * @type {string}
1238
+ */
1239
+ const ariaHidden = 'aria-hidden';
1240
+
1241
+ /**
1242
+ * A global namespace for aria-labelledby.
1243
+ * @type {string}
1244
+ */
1245
+ const ariaLabelledBy = 'aria-labelledby';
1246
+
1247
+ /**
1248
+ * A global namespace for `ArrowDown` key.
1249
+ * @type {string} e.which = 40 equivalent
1250
+ */
1251
+ const keyArrowDown = 'ArrowDown';
1252
+
1253
+ /**
1254
+ * A global namespace for `ArrowUp` key.
1255
+ * @type {string} e.which = 38 equivalent
1256
+ */
1257
+ const keyArrowUp = 'ArrowUp';
1258
+
1259
+ /**
1260
+ * A global namespace for `ArrowLeft` key.
1261
+ * @type {string} e.which = 37 equivalent
1262
+ */
1263
+ const keyArrowLeft = 'ArrowLeft';
1264
+
1265
+ /**
1266
+ * A global namespace for `ArrowRight` key.
1267
+ * @type {string} e.which = 39 equivalent
1268
+ */
1269
+ const keyArrowRight = 'ArrowRight';
1270
+
1271
+ /**
1272
+ * A global namespace for `Enter` key.
1273
+ * @type {string} e.which = 13 equivalent
1274
+ */
1275
+ const keyEnter = 'Enter';
1276
+
1277
+ /**
1278
+ * A global namespace for `Space` key.
1279
+ * @type {string} e.which = 32 equivalent
1280
+ */
1281
+ const keySpace = 'Space';
1282
+
1283
+ /**
1284
+ * A global namespace for `Escape` key.
1285
+ * @type {string} e.which = 27 equivalent
1286
+ */
1287
+ const keyEscape = 'Escape';
1288
+
1289
+ // @ts-ignore
1290
+ const { userAgentData: uaDATA } = navigator;
1291
+
1292
+ /**
1293
+ * A global namespace for `userAgentData` object.
1294
+ */
1295
+ const userAgentData = uaDATA;
1296
+
1297
+ const { userAgent: userAgentString } = navigator;
1298
+
1299
+ /**
1300
+ * A global namespace for `navigator.userAgent` string.
1301
+ */
1302
+ const userAgent = userAgentString;
1303
+
1304
+ const mobileBrands = /iPhone|iPad|iPod|Android/i;
1305
+ let isMobileCheck = false;
1306
+
1307
+ if (userAgentData) {
1308
+ isMobileCheck = userAgentData.brands
1309
+ .some((/** @type {Record<String, any>} */x) => mobileBrands.test(x.brand));
1310
+ } else {
1311
+ isMobileCheck = mobileBrands.test(userAgent);
1312
+ }
1313
+
1314
+ /**
1315
+ * A global `boolean` for mobile detection.
1316
+ * @type {boolean}
1317
+ */
1318
+ const isMobile = isMobileCheck;
1319
+
1320
+ let elementUID = 1;
1321
+ const elementIDMap = new Map();
1322
+
1323
+ /**
1324
+ * Returns a unique identifier for popover, tooltip, scrollspy.
1325
+ *
1326
+ * @param {HTMLElement | Element} element target element
1327
+ * @param {string=} key predefined key
1328
+ * @returns {number} an existing or new unique ID
1329
+ */
1330
+ function getUID(element, key) {
1331
+ elementUID += 1;
1332
+ let elMap = elementIDMap.get(element);
1333
+ let result = elementUID;
1334
+
1335
+ if (key && key.length) {
1336
+ if (elMap) {
1337
+ const elMapId = elMap.get(key);
1338
+ if (!Number.isNaN(elMapId)) {
1339
+ result = elMapId;
1340
+ } else {
1341
+ elMap.set(key, result);
1342
+ }
1343
+ } else {
1344
+ elementIDMap.set(element, new Map());
1345
+ elMap = elementIDMap.get(element);
1346
+ elMap.set(key, result);
1347
+ }
1348
+ } else if (!Number.isNaN(elMap)) {
1349
+ result = elMap;
1350
+ } else {
1351
+ elementIDMap.set(element, result);
1352
+ }
1353
+ return result;
1354
+ }
1355
+
1356
+ /**
1357
+ * Returns the bounding client rect of a target `HTMLElement`.
1358
+ *
1359
+ * @see https://github.com/floating-ui/floating-ui
1360
+ *
1361
+ * @param {HTMLElement | Element} element event.target
1362
+ * @param {boolean=} includeScale when *true*, the target scale is also computed
1363
+ * @returns {SHORTER.BoundingClientRect} the bounding client rect object
1364
+ */
1365
+ function getBoundingClientRect(element, includeScale) {
1366
+ const {
1367
+ width, height, top, right, bottom, left,
1368
+ } = element.getBoundingClientRect();
1369
+ let scaleX = 1;
1370
+ let scaleY = 1;
1371
+
1372
+ if (includeScale && element instanceof HTMLElement) {
1373
+ const { offsetWidth, offsetHeight } = element;
1374
+ scaleX = offsetWidth > 0 ? Math.round(width) / offsetWidth || 1 : 1;
1375
+ scaleY = offsetHeight > 0 ? Math.round(height) / offsetHeight || 1 : 1;
1376
+ }
1377
+
1378
+ return {
1379
+ width: width / scaleX,
1380
+ height: height / scaleY,
1381
+ top: top / scaleY,
1382
+ right: right / scaleX,
1383
+ bottom: bottom / scaleY,
1384
+ left: left / scaleX,
1385
+ x: left / scaleX,
1386
+ y: top / scaleY,
1387
+ };
1388
+ }
1389
+
1390
+ /**
1391
+ * A shortcut for `(document|Element).querySelectorAll`.
1392
+ *
1393
+ * @param {string} selector the input selector
1394
+ * @param {(HTMLElement | Element | Document | Node)=} parent optional node to look into
1395
+ * @return {NodeListOf<HTMLElement | Element>} the query result
1396
+ */
1397
+ function querySelectorAll(selector, parent) {
1398
+ const lookUp = parent && parentNodes
1399
+ .some((x) => parent instanceof x) ? parent : getDocument();
1400
+ // @ts-ignore -- `ShadowRoot` is also a node
1401
+ return lookUp.querySelectorAll(selector);
1402
+ }
1403
+
1404
+ /**
1405
+ * Shortcut for `HTMLElement.closest` method which also works
1406
+ * with children of `ShadowRoot`. The order of the parameters
1407
+ * is intentional since they're both required.
1408
+ *
1409
+ * @see https://stackoverflow.com/q/54520554/803358
1410
+ *
1411
+ * @param {HTMLElement | Element} element Element to look into
1412
+ * @param {string} selector the selector name
1413
+ * @return {(HTMLElement | Element)?} the query result
1414
+ */
1415
+ function closest(element, selector) {
1416
+ return element ? (element.closest(selector)
1417
+ // @ts-ignore -- break out of `ShadowRoot`
1418
+ || closest(element.getRootNode().host, selector)) : null;
1419
+ }
1420
+
1421
+ /**
1422
+ * This is a shortie for `document.createElementNS` method
1423
+ * which allows you to create a new `HTMLElement` for a given `tagName`
1424
+ * or based on an object with specific non-readonly attributes:
1425
+ * `id`, `className`, `textContent`, `style`, etc.
1426
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
1427
+ *
1428
+ * @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
1429
+ * @param {Record<string, string> | string} param `tagName` or object
1430
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
1431
+ */
1432
+ function createElementNS(namespace, param) {
1433
+ if (typeof param === 'string') {
1434
+ return getDocument().createElementNS(namespace, param);
1435
+ }
1436
+
1437
+ const { tagName } = param;
1438
+ const attr = { ...param };
1439
+ const newElement = createElementNS(namespace, tagName);
1440
+ delete attr.tagName;
1441
+ ObjectAssign(newElement, attr);
1442
+ return newElement;
1443
+ }
1444
+
1445
+ /**
1446
+ * Shortcut for the `Element.dispatchEvent(Event)` method.
1447
+ *
1448
+ * @param {HTMLElement | Element} element is the target
1449
+ * @param {Event} event is the `Event` object
1450
+ */
1451
+ const dispatchEvent = (element, event) => element.dispatchEvent(event);
1452
+
1453
+ /** @type {Map<string, Map<HTMLElement | Element, Record<string, any>>>} */
1454
+ const componentData = new Map();
1455
+ /**
1456
+ * An interface for web components background data.
1457
+ * @see https://github.com/thednp/bootstrap.native/blob/master/src/components/base-component.js
1458
+ */
1459
+ const Data = {
1460
+ /**
1461
+ * Sets web components data.
1462
+ * @param {HTMLElement | Element | string} target target element
1463
+ * @param {string} component the component's name or a unique key
1464
+ * @param {Record<string, any>} instance the component instance
1465
+ */
1466
+ set: (target, component, instance) => {
1467
+ const element = querySelector(target);
1468
+ if (!element) return;
1469
+
1470
+ if (!componentData.has(component)) {
1471
+ componentData.set(component, new Map());
1472
+ }
1473
+
1474
+ const instanceMap = componentData.get(component);
1475
+ // @ts-ignore - not undefined, but defined right above
1476
+ instanceMap.set(element, instance);
1477
+ },
1478
+
1479
+ /**
1480
+ * Returns all instances for specified component.
1481
+ * @param {string} component the component's name or a unique key
1482
+ * @returns {Map<HTMLElement | Element, Record<string, any>>?} all the component instances
1483
+ */
1484
+ getAllFor: (component) => {
1485
+ const instanceMap = componentData.get(component);
1486
+
1487
+ return instanceMap || null;
1488
+ },
1489
+
1490
+ /**
1491
+ * Returns the instance associated with the target.
1492
+ * @param {HTMLElement | Element | string} target target element
1493
+ * @param {string} component the component's name or a unique key
1494
+ * @returns {Record<string, any>?} the instance
1495
+ */
1496
+ get: (target, component) => {
1497
+ const element = querySelector(target);
1498
+ const allForC = Data.getAllFor(component);
1499
+ const instance = element && allForC && allForC.get(element);
1500
+
1501
+ return instance || null;
1502
+ },
1503
+
1504
+ /**
1505
+ * Removes web components data.
1506
+ * @param {HTMLElement | Element | string} target target element
1507
+ * @param {string} component the component's name or a unique key
1508
+ */
1509
+ remove: (target, component) => {
1510
+ const element = querySelector(target);
1511
+ const instanceMap = componentData.get(component);
1512
+ if (!instanceMap || !element) return;
1513
+
1514
+ instanceMap.delete(element);
1515
+
1516
+ if (instanceMap.size === 0) {
1517
+ componentData.delete(component);
1518
+ }
1519
+ },
1520
+ };
1521
+
1522
+ /**
1523
+ * An alias for `Data.get()`.
1524
+ * @type {SHORTER.getInstance<any>}
1525
+ */
1526
+ const getInstance = (target, component) => Data.get(target, component);
1527
+
1528
+ /**
1529
+ * Check class in `HTMLElement.classList`.
1530
+ *
1531
+ * @param {HTMLElement | Element} element target
1532
+ * @param {string} classNAME to check
1533
+ * @returns {boolean}
1534
+ */
1535
+ function hasClass(element, classNAME) {
1536
+ return element.classList.contains(classNAME);
1537
+ }
1538
+
1539
+ /**
1540
+ * Add class to `HTMLElement.classList`.
1541
+ *
1542
+ * @param {HTMLElement | Element} element target
1543
+ * @param {string} classNAME to add
1544
+ * @returns {void}
1545
+ */
1546
+ function addClass(element, classNAME) {
1547
+ element.classList.add(classNAME);
1548
+ }
1549
+
1550
+ /**
1551
+ * Remove class from `HTMLElement.classList`.
1552
+ *
1553
+ * @param {HTMLElement | Element} element target
1554
+ * @param {string} classNAME to remove
1555
+ * @returns {void}
1556
+ */
1557
+ function removeClass(element, classNAME) {
1558
+ element.classList.remove(classNAME);
1559
+ }
1560
+
1561
+ /**
1562
+ * Shortcut for `HTMLElement.hasAttribute()` method.
1563
+ * @param {HTMLElement | Element} element target element
1564
+ * @param {string} attribute attribute name
1565
+ * @returns {boolean} the query result
1566
+ */
1567
+ const hasAttribute = (element, attribute) => element.hasAttribute(attribute);
1568
+
1569
+ /**
1570
+ * Shortcut for `HTMLElement.setAttribute()` method.
1571
+ * @param {HTMLElement | Element} element target element
1572
+ * @param {string} attribute attribute name
1573
+ * @param {string} value attribute value
1574
+ * @returns {void}
1575
+ */
1576
+ const setAttribute = (element, attribute, value) => element.setAttribute(attribute, value);
1577
+
1578
+ /**
1579
+ * Shortcut for `HTMLElement.getAttribute()` method.
1580
+ * @param {HTMLElement | Element} element target element
1581
+ * @param {string} attribute attribute name
1582
+ * @returns {string?} attribute value
1583
+ */
1584
+ const getAttribute = (element, attribute) => element.getAttribute(attribute);
1585
+
1586
+ /**
1587
+ * Shortcut for `HTMLElement.removeAttribute()` method.
1588
+ * @param {HTMLElement | Element} element target element
1589
+ * @param {string} attribute attribute name
1590
+ * @returns {void}
1591
+ */
1592
+ const removeAttribute = (element, attribute) => element.removeAttribute(attribute);
1593
+
1594
+ /**
1595
+ * Shortcut for `String.toUpperCase()`.
1596
+ *
1597
+ * @param {string} source input string
1598
+ * @returns {string} uppercase output string
1599
+ */
1600
+ const toUpperCase = (source) => source.toUpperCase();
1601
+
1602
+ const vHidden = 'v-hidden';
1603
+
1604
+ /**
1605
+ * Returns the color form.
1606
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
1607
+ * @returns {HTMLElement | Element}
1608
+ */
1609
+ function getColorForm(self) {
1610
+ const { format, id } = self;
1611
+ const colorForm = createElement({
1612
+ tagName: 'div',
1613
+ className: `color-form ${format}`,
1614
+ });
1615
+
1616
+ let components = ['hex'];
1617
+ if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
1618
+ else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
1619
+
1620
+ components.forEach((c) => {
1621
+ const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
1622
+ const cID = `color_${format}_${c}_${id}`;
1623
+ const cInputLabel = createElement({ tagName: 'label' });
1624
+ setAttribute(cInputLabel, 'for', cID);
1625
+ cInputLabel.append(
1626
+ createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
1627
+ createElement({ tagName: 'span', className: vHidden, innerText: `${c}` }),
1628
+ );
1629
+ const cInput = createElement({
1630
+ tagName: 'input',
1631
+ id: cID, // name: cID,
1632
+ type: format === 'hex' ? 'text' : 'number',
1633
+ value: c === 'alpha' ? '1' : '0',
1634
+ className: `color-input ${c}`,
1635
+ autocomplete: 'off',
1636
+ spellcheck: 'false',
1637
+ });
1638
+ if (format !== 'hex') {
1639
+ // alpha
1640
+ let max = '1';
1641
+ let step = '0.01';
1642
+ if (c !== 'alpha') {
1643
+ if (format === 'rgb') { max = '255'; step = '1'; } else if (c === 'hue') { max = '360'; step = '1'; } else { max = '100'; step = '1'; }
1644
+ }
1645
+ ObjectAssign(cInput, {
1646
+ min: '0',
1647
+ max,
1648
+ step,
1649
+ });
1650
+ }
1651
+ colorForm.append(cInputLabel, cInput);
1652
+ });
1653
+ return colorForm;
1654
+ }
1655
+
1656
+ /**
1657
+ * Returns a new color control `HTMLElement`.
1658
+ * @param {number} iteration
1659
+ * @param {number} id
1660
+ * @param {number} width
1661
+ * @param {number} height
1662
+ * @param {string=} labelledby
1663
+ * @returns {HTMLElement | Element}
1664
+ */
1665
+ function getColorControl(iteration, id, width, height, labelledby) {
1666
+ const labelID = `appearance${iteration}_${id}`;
1667
+ const knobClass = iteration === 1 ? 'color-pointer' : 'color-slider';
1668
+ const control = createElement({
1669
+ tagName: 'div',
1670
+ className: 'color-control',
1671
+ });
1672
+ setAttribute(control, 'role', 'presentation');
1673
+
1674
+ control.append(
1675
+ createElement({
1676
+ id: labelID,
1677
+ tagName: 'label',
1678
+ className: `color-label ${vHidden}`,
1679
+ ariaLive: 'polite',
1680
+ }),
1681
+ createElement({
1682
+ tagName: 'canvas',
1683
+ className: `visual-control${iteration}`,
1684
+ ariaHidden: 'true',
1685
+ width: `${width}`,
1686
+ height: `${height}`,
1687
+ }),
1688
+ );
1689
+
1690
+ const knob = createElement({
1691
+ tagName: 'div',
1692
+ className: `${knobClass} knob`,
1693
+ });
1694
+ setAttribute(knob, ariaLabelledBy, labelledby || labelID);
1695
+ setAttribute(knob, 'tabindex', '0');
1696
+ control.append(knob);
1697
+ return control;
1698
+ }
1699
+
1700
+ // ColorPicker GC
1701
+ // ==============
1702
+ const colorPickerString = 'color-picker';
1703
+ const colorPickerSelector = `[data-function="${colorPickerString}"]`;
1704
+ const nonColors = ['transparent', 'currentColor', 'inherit', 'initial'];
1705
+ const colorNames$1 = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
1706
+ const colorPickerLabels = {
1707
+ pickerLabel: 'Colour Picker',
1708
+ toggleLabel: 'Select colour',
1709
+ menuLabel: 'Select colour preset',
1710
+ requiredLabel: 'Required',
1711
+ formatLabel: 'Colour Format',
1712
+ formatHEX: 'Hexadecimal Format',
1713
+ formatRGB: 'RGB Format',
1714
+ formatHSL: 'HSL Format',
1715
+ alphaLabel: 'Alpha',
1716
+ appearanceLabel: 'Colour Appearance',
1717
+ hexLabel: 'Hexadecimal',
1718
+ hueLabel: 'Hue',
1719
+ saturationLabel: 'Saturation',
1720
+ lightnessLabel: 'Lightness',
1721
+ redLabel: 'Red',
1722
+ greenLabel: 'Green',
1723
+ blueLabel: 'Blue',
1724
+ };
1725
+
1726
+ // ColorPicker Static Methods
1727
+ // ==========================
1728
+
1729
+ /** @type {CP.GetInstance<ColorPicker>} */
1730
+ const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
1731
+
1732
+ /** @type {CP.InitCallback<ColorPicker>} */
1733
+ const initColorPicker = (element) => new ColorPicker(element);
1734
+
1735
+ // ColorPicker Private Methods
1736
+ // ===========================
1737
+
1738
+ /**
1739
+ * Add / remove `ColorPicker` main event listeners.
1740
+ * @param {ColorPicker} self
1741
+ * @param {boolean=} action
1742
+ */
1743
+ function toggleEvents(self, action) {
1744
+ const fn = action ? addListener : removeListener;
1745
+ const { input, pickerToggle, menuToggle } = self;
1746
+
1747
+ fn(input, 'focusin', self.showPicker);
1748
+ fn(pickerToggle, 'click', self.togglePicker);
1749
+
1750
+ fn(input, 'keydown', self.keyHandler);
1751
+
1752
+ if (menuToggle) {
1753
+ fn(menuToggle, 'click', self.toggleMenu);
1754
+ }
1755
+ }
1756
+
1757
+ /**
1758
+ * Generate HTML markup and update instance properties.
1759
+ * @param {ColorPicker} self
1760
+ */
1761
+ function initCallback(self) {
1762
+ const {
1763
+ input, parent, format, id, componentLabels, keywords,
1764
+ } = self;
1765
+ const colorValue = getAttribute(input, 'value') || '#fff';
1766
+
1767
+ const {
1768
+ toggleLabel, menuLabel, formatLabel, pickerLabel, appearanceLabel,
1769
+ } = componentLabels;
1770
+
1771
+ // update color
1772
+ const color = nonColors.includes(colorValue) ? '#fff' : colorValue;
1773
+ self.color = new Color(color, { format });
1774
+
1775
+ // set initial controls dimensions
1776
+ // make the controls smaller on mobile
1777
+ const cv1w = isMobile ? 150 : 230;
1778
+ const cvh = isMobile ? 150 : 230;
1779
+ const cv2w = 21;
1780
+ const dropClass = isMobile ? ' mobile' : '';
1781
+ const ctrl1Labelledby = format === 'hsl' ? `appearance_${id} appearance1_${id}` : `appearance1_${id}`;
1782
+ const ctrl2Labelledby = format === 'hsl' ? `appearance2_${id}` : `appearance_${id} appearance2_${id}`;
1783
+
1784
+ const pickerBtn = createElement({
1785
+ tagName: 'button',
1786
+ className: 'picker-toggle button-appearance',
1787
+ ariaExpanded: 'false',
1788
+ ariaHasPopup: 'true',
1789
+ ariaLive: 'polite',
1790
+ });
1791
+ setAttribute(pickerBtn, 'tabindex', '-1');
1792
+ pickerBtn.append(createElement({
1793
+ tagName: 'span',
1794
+ className: vHidden,
1795
+ innerText: 'Open Color Picker',
1796
+ }));
1797
+
1798
+ const colorPickerDropdown = createElement({
1799
+ tagName: 'div',
1800
+ className: `color-dropdown picker${dropClass}`,
1801
+ });
1802
+ setAttribute(colorPickerDropdown, ariaLabelledBy, `picker-label-${id} format-label-${id}`);
1803
+ setAttribute(colorPickerDropdown, 'role', 'group');
1804
+ colorPickerDropdown.append(
1805
+ createElement({
1806
+ tagName: 'label',
1807
+ className: vHidden,
1808
+ ariaHidden: 'true',
1809
+ id: `picker-label-${id}`,
1810
+ innerText: `${pickerLabel}`,
1811
+ }),
1812
+ createElement({
1813
+ tagName: 'label',
1814
+ className: vHidden,
1815
+ ariaHidden: 'true',
1816
+ id: `format-label-${id}`,
1817
+ innerText: `${formatLabel}`,
1818
+ }),
1819
+ createElement({
1820
+ tagName: 'label',
1821
+ className: `color-appearance ${vHidden}`,
1822
+ ariaHidden: 'true',
1823
+ ariaLive: 'polite',
1824
+ id: `appearance_${id}`,
1825
+ innerText: `${appearanceLabel}`,
1826
+ }),
1827
+ );
1828
+
1829
+ const colorControls = createElement({
1830
+ tagName: 'div',
1831
+ className: `color-controls ${format}`,
1832
+ });
1833
+
1834
+ colorControls.append(
1835
+ getColorControl(1, id, cv1w, cvh, ctrl1Labelledby),
1836
+ getColorControl(2, id, cv2w, cvh, ctrl2Labelledby),
1837
+ );
1838
+
1839
+ if (format !== 'hex') {
1840
+ colorControls.append(
1841
+ getColorControl(3, id, cv2w, cvh),
1842
+ );
1843
+ }
1844
+
1845
+ // @ts-ignore
1846
+ const colorForm = getColorForm(self);
1847
+ colorPickerDropdown.append(colorControls, colorForm);
1848
+ parent.append(pickerBtn, colorPickerDropdown);
1849
+
1850
+ // set color key menu template
1851
+ if (keywords) {
1852
+ const colorKeys = keywords;
1853
+ const presetsDropdown = createElement({
1854
+ tagName: 'div',
1855
+ className: `color-dropdown menu${dropClass}`,
1856
+ });
1857
+ const presetsMenu = createElement({
1858
+ tagName: 'ul',
1859
+ ariaLabel: `${menuLabel}`,
1860
+ className: 'color-menu',
1861
+ });
1862
+ setAttribute(presetsMenu, 'role', 'listbox');
1863
+ presetsDropdown.append(presetsMenu);
1864
+
1865
+ colorKeys.forEach((x) => {
1866
+ const xKey = x.trim();
1867
+ const xRealColor = new Color(xKey, { format }).toString();
1868
+ const isActive = xRealColor === getAttribute(input, 'value');
1869
+ const active = isActive ? ' active' : '';
1870
+
1871
+ const keyOption = createElement({
1872
+ tagName: 'li',
1873
+ className: `color-option${active}`,
1874
+ ariaSelected: isActive ? 'true' : 'false',
1875
+ innerText: `${x}`,
1876
+ });
1877
+ setAttribute(keyOption, 'role', 'option');
1878
+ setAttribute(keyOption, 'tabindex', '0');
1879
+ setAttribute(keyOption, 'data-value', `${xKey}`);
1880
+ presetsMenu.append(keyOption);
1881
+ });
1882
+ const presetsBtn = createElement({
1883
+ tagName: 'button',
1884
+ className: 'menu-toggle button-appearance',
1885
+ ariaExpanded: 'false',
1886
+ ariaHasPopup: 'true',
1887
+ });
1888
+ const xmlns = encodeURI('http://www.w3.org/2000/svg');
1889
+ const presetsIcon = createElementNS(xmlns, { tagName: 'svg' });
1890
+ setAttribute(presetsIcon, 'xmlns', xmlns);
1891
+ setAttribute(presetsIcon, ariaHidden, 'true');
1892
+ setAttribute(presetsIcon, 'viewBox', '0 0 512 512');
1893
+ const piPath = createElementNS(xmlns, { tagName: 'path' });
1894
+ setAttribute(piPath, 'd', 'M98,158l157,156L411,158l27,27L255,368L71,185L98,158z');
1895
+ setAttribute(piPath, 'fill', '#fff');
1896
+ presetsIcon.append(piPath);
1897
+ presetsBtn.append(createElement({
1898
+ tagName: 'span',
1899
+ className: vHidden,
1900
+ innerText: `${toggleLabel}`,
1901
+ }), presetsIcon);
1902
+
1903
+ parent.append(presetsBtn, presetsDropdown);
1904
+ }
1905
+
1906
+ // solve non-colors after settings save
1907
+ if (keywords && nonColors.includes(colorValue)) {
1908
+ self.value = colorValue;
1909
+ }
1910
+ }
1911
+
1912
+ /**
1913
+ * Add / remove `ColorPicker` event listeners active only when open.
1914
+ * @param {ColorPicker} self
1915
+ * @param {boolean=} action
1916
+ */
1917
+ function toggleEventsOnShown(self, action) {
1918
+ const fn = action ? addListener : removeListener;
1919
+ const pointerEvents = 'ontouchstart' in document
1920
+ ? { down: 'touchstart', move: 'touchmove', up: 'touchend' }
1921
+ : { down: 'mousedown', move: 'mousemove', up: 'mouseup' };
1922
+
1923
+ fn(self.controls, pointerEvents.down, self.pointerDown);
1924
+ self.controlKnobs.forEach((x) => fn(x, 'keydown', self.handleKnobs));
1925
+
1926
+ fn(window, 'scroll', self.handleScroll);
1927
+
1928
+ [self.input, ...self.inputs].forEach((x) => fn(x, 'change', self.changeHandler));
1929
+
1930
+ if (self.colorMenu) {
1931
+ fn(self.colorMenu, 'click', self.menuClickHandler);
1932
+ fn(self.colorMenu, 'keydown', self.menuKeyHandler);
1933
+ }
1934
+
1935
+ fn(document, pointerEvents.move, self.pointerMove);
1936
+ fn(document, pointerEvents.up, self.pointerUp);
1937
+ fn(window, 'keyup', self.handleDismiss);
1938
+ fn(self.parent, 'focusout', self.handleFocusOut);
1939
+ }
1940
+
1941
+ /**
1942
+ * Triggers the `ColorPicker` original event.
1943
+ * @param {ColorPicker} self
1944
+ */
1945
+ function firePickerChange(self) {
1946
+ dispatchEvent(self.input, new CustomEvent('colorpicker.change'));
1947
+ }
1948
+
1949
+ /**
1950
+ * Toggles the visibility of a dropdown or returns false if none is visible.
1951
+ * @param {HTMLElement} element
1952
+ * @param {boolean=} check
1953
+ * @returns {void | boolean}
1954
+ */
1955
+ function classToggle(element, check) {
1956
+ const fn1 = !check ? 'forEach' : 'some';
1957
+ const fn2 = !check ? removeClass : hasClass;
1958
+
1959
+ if (element) {
1960
+ return ['show', 'show-top'][fn1]((x) => fn2(element, x));
1961
+ }
1962
+
1963
+ return false;
1964
+ }
1965
+
1966
+ /**
1967
+ * Shows the `ColorPicker` presets menu.
1968
+ * @param {ColorPicker} self
1969
+ */
1970
+ function showMenu(self) {
1971
+ classToggle(self.colorPicker);
1972
+ addClass(self.colorMenu, 'show');
1973
+ self.show();
1974
+ setAttribute(self.menuToggle, ariaExpanded, 'true');
1975
+ }
1976
+
1977
+ /**
1978
+ * Color Picker
1979
+ * @see http://thednp.github.io/color-picker
1980
+ */
1981
+ class ColorPicker {
1982
+ /**
1983
+ * Returns a new ColorPicker instance.
1984
+ * @param {HTMLInputElement | string} target the target `<input>` element
1985
+ */
1986
+ constructor(target) {
1987
+ const self = this;
1988
+ /** @type {HTMLInputElement} */
1989
+ // @ts-ignore
1990
+ self.input = querySelector(target);
1991
+ // invalidate
1992
+ if (!self.input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
1993
+ const { input } = self;
1994
+
1995
+ /** @type {HTMLElement} */
1996
+ // @ts-ignore
1997
+ self.parent = closest(input, `.${colorPickerString},${colorPickerString}`);
1998
+ if (!self.parent) throw new TypeError('ColorPicker requires a specific markup to work.');
1999
+
2000
+ /** @type {number} */
2001
+ self.id = getUID(input, colorPickerString);
2002
+
2003
+ // set initial state
2004
+ /** @type {HTMLCanvasElement?} */
2005
+ self.dragElement = null;
2006
+ /** @type {boolean} */
2007
+ self.isOpen = false;
2008
+ /** @type {Record<string, number>} */
2009
+ self.controlPositions = {
2010
+ c1x: 0, c1y: 0, c2y: 0, c3y: 0,
2011
+ };
2012
+ /** @type {Record<string, string>} */
2013
+ self.colorLabels = {};
2014
+ /** @type {Array<string> | false} */
2015
+ self.keywords = false;
2016
+ /** @type {Color} */
2017
+ self.color = new Color('white', { format: self.format });
2018
+ /** @type {Record<string, string>} */
2019
+ self.componentLabels = ObjectAssign({}, colorPickerLabels);
2020
+
2021
+ const { componentLabels, colorLabels, keywords } = input.dataset;
2022
+ const temp = componentLabels ? JSON.parse(componentLabels) : {};
2023
+ self.componentLabels = ObjectAssign(self.componentLabels, temp);
2024
+
2025
+ const translatedColorLabels = colorLabels && colorLabels.split(',').length === 17
2026
+ ? colorLabels.split(',') : colorNames$1;
2027
+
2028
+ // expose color labels to all methods
2029
+ colorNames$1.forEach((c, i) => { self.colorLabels[c] = translatedColorLabels[i]; });
2030
+
2031
+ // set colour presets
2032
+ if (keywords !== 'false') {
2033
+ self.keywords = keywords ? keywords.split(',') : nonColors;
2034
+ }
2035
+
2036
+ // bind events
2037
+ self.showPicker = self.showPicker.bind(self);
2038
+ self.togglePicker = self.togglePicker.bind(self);
2039
+ self.toggleMenu = self.toggleMenu.bind(self);
2040
+ self.menuClickHandler = self.menuClickHandler.bind(self);
2041
+ self.menuKeyHandler = self.menuKeyHandler.bind(self);
2042
+ self.pointerDown = self.pointerDown.bind(self);
2043
+ self.pointerMove = self.pointerMove.bind(self);
2044
+ self.pointerUp = self.pointerUp.bind(self);
2045
+ self.handleScroll = self.handleScroll.bind(self);
2046
+ self.handleFocusOut = self.handleFocusOut.bind(self);
2047
+ self.changeHandler = self.changeHandler.bind(self);
2048
+ self.handleDismiss = self.handleDismiss.bind(self);
2049
+ self.keyHandler = self.keyHandler.bind(self);
2050
+ self.handleKnobs = self.handleKnobs.bind(self);
2051
+
2052
+ // generate markup
2053
+ initCallback(self);
2054
+
2055
+ const { parent } = self;
2056
+ // set main elements
2057
+ /** @type {HTMLElement} */
2058
+ // @ts-ignore
2059
+ self.pickerToggle = querySelector('.picker-toggle', parent);
2060
+ /** @type {HTMLElement} */
2061
+ // @ts-ignore
2062
+ self.menuToggle = querySelector('.menu-toggle', parent);
2063
+ /** @type {HTMLElement} */
2064
+ // @ts-ignore
2065
+ self.colorMenu = querySelector('.color-dropdown.menu', parent);
2066
+ /** @type {HTMLElement} */
2067
+ // @ts-ignore
2068
+ self.colorPicker = querySelector('.color-dropdown.picker', parent);
2069
+ /** @type {HTMLElement} */
2070
+ // @ts-ignore
2071
+ self.controls = querySelector('.color-controls', parent);
2072
+ /** @type {HTMLInputElement[]} */
2073
+ // @ts-ignore
2074
+ self.inputs = [...querySelectorAll('.color-input', parent)];
2075
+ /** @type {(HTMLElement)[]} */
2076
+ // @ts-ignore
2077
+ self.controlKnobs = [...querySelectorAll('.knob', parent)];
2078
+ /** @type {HTMLCanvasElement[]} */
2079
+ // @ts-ignore
2080
+ self.visuals = [...querySelectorAll('canvas', self.controls)];
2081
+ /** @type {HTMLLabelElement[]} */
2082
+ // @ts-ignore
2083
+ self.knobLabels = [...querySelectorAll('.color-label', parent)];
2084
+ /** @type {HTMLLabelElement} */
2085
+ // @ts-ignore
2086
+ self.appearance = querySelector('.color-appearance', parent);
2087
+
2088
+ const [v1, v2, v3] = self.visuals;
2089
+ // set dimensions
2090
+ /** @type {number} */
2091
+ self.width1 = v1.width;
2092
+ /** @type {number} */
2093
+ self.height1 = v1.height;
2094
+ /** @type {number} */
2095
+ self.width2 = v2.width;
2096
+ /** @type {number} */
2097
+ self.height2 = v2.height;
2098
+ // set main controls
2099
+ /** @type {*} */
2100
+ self.ctx1 = v1.getContext('2d');
2101
+ /** @type {*} */
2102
+ self.ctx2 = v2.getContext('2d');
2103
+ self.ctx1.rect(0, 0, self.width1, self.height1);
2104
+ self.ctx2.rect(0, 0, self.width2, self.height2);
2105
+
2106
+ /** @type {number} */
2107
+ self.width3 = 0;
2108
+ /** @type {number} */
2109
+ self.height3 = 0;
2110
+
2111
+ // set alpha control except hex
2112
+ if (self.format !== 'hex') {
2113
+ self.width3 = v3.width;
2114
+ self.height3 = v3.height;
2115
+ /** @type {*} */
2116
+ this.ctx3 = v3.getContext('2d');
2117
+ self.ctx3.rect(0, 0, self.width3, self.height3);
2118
+ }
2119
+
2120
+ // update color picker controls, inputs and visuals
2121
+ this.setControlPositions();
2122
+ this.setColorAppearence();
2123
+ // don't trigger change at initialization
2124
+ this.updateInputs(true);
2125
+ this.updateControls();
2126
+ this.updateVisuals();
2127
+ // add main events listeners
2128
+ toggleEvents(self, true);
2129
+
2130
+ // set component data
2131
+ Data.set(input, colorPickerString, self);
2132
+ }
2133
+
2134
+ /** Returns the current color value */
2135
+ get value() { return this.input.value; }
2136
+
2137
+ /**
2138
+ * Sets a new color value.
2139
+ * @param {string} v new color value
2140
+ */
2141
+ set value(v) { this.input.value = v; }
2142
+
2143
+ /** Check if the input is required to have a valid value. */
2144
+ get required() { return hasAttribute(this.input, 'required'); }
2145
+
2146
+ /**
2147
+ * Returns the colour format.
2148
+ * @returns {CP.ColorFormats | string}
2149
+ */
2150
+ get format() { return getAttribute(this.input, 'format') || 'hex'; }
2151
+
2152
+ /** Returns the input name. */
2153
+ get name() { return getAttribute(this.input, 'name'); }
2154
+
2155
+ /**
2156
+ * Returns the label associated to the input.
2157
+ * @returns {HTMLLabelElement?}
2158
+ */
2159
+ // @ts-ignore
2160
+ get label() { return querySelector(`[for="${this.input.id}"]`); }
2161
+
2162
+ /** Check if the color presets include any non-color. */
2163
+ get includeNonColor() {
2164
+ return this.keywords instanceof Array
2165
+ && this.keywords.some((x) => nonColors.includes(x));
2166
+ }
2167
+
2168
+ /** Returns hexadecimal value of the current color. */
2169
+ get hex() { return this.color.toHex(); }
2170
+
2171
+ /** Returns the current color value in {h,s,v,a} object format. */
2172
+ get hsv() { return this.color.toHsv(); }
2173
+
2174
+ /** Returns the current color value in {h,s,l,a} object format. */
2175
+ get hsl() { return this.color.toHsl(); }
2176
+
2177
+ /** Returns the current color value in {r,g,b,a} object format. */
2178
+ get rgb() { return this.color.toRgb(); }
2179
+
2180
+ /** Returns the current color brightness. */
2181
+ get brightness() { return this.color.brightness; }
2182
+
2183
+ /** Returns the current color luminance. */
2184
+ get luminance() { return this.color.luminance; }
2185
+
2186
+ /** Checks if the current colour requires a light text color. */
2187
+ get isDark() {
2188
+ const { rgb, brightness } = this;
2189
+ return brightness < 120 && rgb.a > 0.33;
2190
+ }
2191
+
2192
+ /** Checks if the current input value is a valid color. */
2193
+ get isValid() {
2194
+ const inputValue = this.input.value;
2195
+ return inputValue !== '' && new Color(inputValue).isValid;
2196
+ }
2197
+
2198
+ /** Updates `ColorPicker` visuals. */
2199
+ updateVisuals() {
2200
+ const self = this;
2201
+ const {
2202
+ color, format, controlPositions,
2203
+ width1, width2, width3,
2204
+ height1, height2, height3,
2205
+ ctx1, ctx2, ctx3,
2206
+ } = self;
2207
+ const { r, g, b } = color;
2208
+
2209
+ if (format !== 'hsl') {
2210
+ const hue = Math.round((controlPositions.c2y / height2) * 360);
2211
+ ctx1.fillStyle = new Color(`hsl(${hue},100%,50%})`).toRgbString();
2212
+ ctx1.fillRect(0, 0, width1, height1);
2213
+
2214
+ const whiteGrad = ctx2.createLinearGradient(0, 0, width1, 0);
2215
+ whiteGrad.addColorStop(0, 'rgba(255,255,255,1)');
2216
+ whiteGrad.addColorStop(1, 'rgba(255,255,255,0)');
2217
+ ctx1.fillStyle = whiteGrad;
2218
+ ctx1.fillRect(0, 0, width1, height1);
2219
+
2220
+ const blackGrad = ctx2.createLinearGradient(0, 0, 0, height1);
2221
+ blackGrad.addColorStop(0, 'rgba(0,0,0,0)');
2222
+ blackGrad.addColorStop(1, 'rgba(0,0,0,1)');
2223
+ ctx1.fillStyle = blackGrad;
2224
+ ctx1.fillRect(0, 0, width1, height1);
2225
+
2226
+ const hueGrad = ctx2.createLinearGradient(0, 0, 0, height1);
2227
+ hueGrad.addColorStop(0, 'rgba(255,0,0,1)');
2228
+ hueGrad.addColorStop(0.17, 'rgba(255,255,0,1)');
2229
+ hueGrad.addColorStop(0.34, 'rgba(0,255,0,1)');
2230
+ hueGrad.addColorStop(0.51, 'rgba(0,255,255,1)');
2231
+ hueGrad.addColorStop(0.68, 'rgba(0,0,255,1)');
2232
+ hueGrad.addColorStop(0.85, 'rgba(255,0,255,1)');
2233
+ hueGrad.addColorStop(1, 'rgba(255,0,0,1)');
2234
+ ctx2.fillStyle = hueGrad;
2235
+ ctx2.fillRect(0, 0, width2, height2);
2236
+ } else {
2237
+ const hueGrad = ctx1.createLinearGradient(0, 0, width1, 0);
2238
+ const saturation = Math.round((1 - controlPositions.c2y / height2) * 100);
2239
+
2240
+ hueGrad.addColorStop(0, new Color('rgba(255,0,0,1)').desaturate(100 - saturation).toRgbString());
2241
+ hueGrad.addColorStop(0.17, new Color('rgba(255,255,0,1)').desaturate(100 - saturation).toRgbString());
2242
+ hueGrad.addColorStop(0.34, new Color('rgba(0,255,0,1)').desaturate(100 - saturation).toRgbString());
2243
+ hueGrad.addColorStop(0.51, new Color('rgba(0,255,255,1)').desaturate(100 - saturation).toRgbString());
2244
+ hueGrad.addColorStop(0.68, new Color('rgba(0,0,255,1)').desaturate(100 - saturation).toRgbString());
2245
+ hueGrad.addColorStop(0.85, new Color('rgba(255,0,255,1)').desaturate(100 - saturation).toRgbString());
2246
+ hueGrad.addColorStop(1, new Color('rgba(255,0,0,1)').desaturate(100 - saturation).toRgbString());
2247
+
2248
+ ctx1.fillStyle = hueGrad;
2249
+ ctx1.fillRect(0, 0, width1, height1);
2250
+
2251
+ const whiteGrad = ctx1.createLinearGradient(0, 0, 0, height1);
2252
+ whiteGrad.addColorStop(0, 'rgba(255,255,255,1)');
2253
+ whiteGrad.addColorStop(0.5, 'rgba(255,255,255,0)');
2254
+ ctx1.fillStyle = whiteGrad;
2255
+ ctx1.fillRect(0, 0, width1, height1);
2256
+
2257
+ const blackGrad = ctx1.createLinearGradient(0, 0, 0, height1);
2258
+ blackGrad.addColorStop(0.5, 'rgba(0,0,0,0)');
2259
+ blackGrad.addColorStop(1, 'rgba(0,0,0,1)');
2260
+ ctx1.fillStyle = blackGrad;
2261
+ ctx1.fillRect(0, 0, width1, height1);
2262
+
2263
+ const saturationGrad = ctx2.createLinearGradient(0, 0, 0, height2);
2264
+ const incolor = color.clone().greyscale().toRgb();
2265
+
2266
+ saturationGrad.addColorStop(0, `rgba(${r},${g},${b},1)`);
2267
+ saturationGrad.addColorStop(1, `rgba(${incolor.r},${incolor.g},${incolor.b},1)`);
2268
+
2269
+ ctx2.fillStyle = saturationGrad;
2270
+ ctx2.fillRect(0, 0, width3, height3);
2271
+ }
2272
+
2273
+ if (format !== 'hex') {
2274
+ ctx3.clearRect(0, 0, width3, height3);
2275
+ const alphaGrad = ctx3.createLinearGradient(0, 0, 0, height3);
2276
+ alphaGrad.addColorStop(0, `rgba(${r},${g},${b},1)`);
2277
+ alphaGrad.addColorStop(1, `rgba(${r},${g},${b},0)`);
2278
+ ctx3.fillStyle = alphaGrad;
2279
+ ctx3.fillRect(0, 0, width3, height3);
2280
+ }
2281
+ }
2282
+
2283
+ /**
2284
+ * Handles the `focusout` listener of the `ColorPicker`.
2285
+ * @param {FocusEvent} e
2286
+ * @this {ColorPicker}
2287
+ */
2288
+ handleFocusOut({ relatedTarget }) {
2289
+ // @ts-ignore
2290
+ if (relatedTarget && !this.parent.contains(relatedTarget)) {
2291
+ this.hide(true);
2292
+ }
2293
+ }
2294
+
2295
+ /**
2296
+ * Handles the `focusout` listener of the `ColorPicker`.
2297
+ * @param {KeyboardEvent} e
2298
+ * @this {ColorPicker}
2299
+ */
2300
+ handleDismiss({ code }) {
2301
+ const self = this;
2302
+ if (self.isOpen && code === keyEscape) {
2303
+ self.hide();
2304
+ }
2305
+ }
2306
+
2307
+ /**
2308
+ * Handles the `ColorPicker` scroll listener when open.
2309
+ * @param {Event} e
2310
+ * @this {ColorPicker}
2311
+ */
2312
+ handleScroll(e) {
2313
+ const self = this;
2314
+ /** @type {*} */
2315
+ const { activeElement } = document;
2316
+
2317
+ if ((isMobile && self.dragElement)
2318
+ || (activeElement && self.controlKnobs.includes(activeElement))) {
2319
+ e.stopPropagation();
2320
+ e.preventDefault();
2321
+ }
2322
+
2323
+ self.updateDropdownPosition();
2324
+ }
2325
+
2326
+ /**
2327
+ * Handles all `ColorPicker` click listeners.
2328
+ * @param {KeyboardEvent} e
2329
+ * @this {ColorPicker}
2330
+ */
2331
+ menuKeyHandler(e) {
2332
+ const { target, code } = e;
2333
+
2334
+ if ([keyArrowDown, keyArrowUp].includes(code)) {
2335
+ e.preventDefault();
2336
+ } else if ([keyEnter, keySpace].includes(code)) {
2337
+ this.menuClickHandler({ target });
2338
+ }
2339
+ }
2340
+
2341
+ /**
2342
+ * Handles all `ColorPicker` click listeners.
2343
+ * @param {Partial<Event>} e
2344
+ * @this {ColorPicker}
2345
+ */
2346
+ menuClickHandler(e) {
2347
+ const self = this;
2348
+ /** @type {*} */
2349
+ const { target } = e;
2350
+ const { format } = self;
2351
+ const newOption = (getAttribute(target, 'data-value') || '').trim();
2352
+ const currentActive = self.colorMenu.querySelector('li.active');
2353
+ const newColor = nonColors.includes(newOption) ? 'white' : newOption;
2354
+ self.color = new Color(newColor, { format });
2355
+ self.setControlPositions();
2356
+ self.setColorAppearence();
2357
+ self.updateInputs(true);
2358
+ self.updateControls();
2359
+ self.updateVisuals();
2360
+
2361
+ if (currentActive) {
2362
+ removeClass(currentActive, 'active');
2363
+ removeAttribute(currentActive, ariaSelected);
2364
+ }
2365
+
2366
+ if (currentActive !== target) {
2367
+ addClass(target, 'active');
2368
+ setAttribute(target, ariaSelected, 'true');
2369
+
2370
+ if (nonColors.includes(newOption)) {
2371
+ self.value = newOption;
2372
+ firePickerChange(self);
2373
+ }
2374
+ }
2375
+ }
2376
+
2377
+ /**
2378
+ * Handles the `ColorPicker` touchstart / mousedown events listeners.
2379
+ * @param {TouchEvent} e
2380
+ * @this {ColorPicker}
2381
+ */
2382
+ pointerDown(e) {
2383
+ const self = this;
2384
+ const {
2385
+ // @ts-ignore
2386
+ type, target, touches, pageX, pageY,
2387
+ } = e;
2388
+ const { visuals, controlKnobs, format } = self;
2389
+ const [v1, v2, v3] = visuals;
2390
+ const [c1, c2, c3] = controlKnobs;
2391
+ /** @type {HTMLCanvasElement} */
2392
+ // @ts-ignore
2393
+ const visual = target.tagName === 'canvas' // @ts-ignore
2394
+ ? target : querySelector('canvas', target.parentElement);
2395
+ const visualRect = getBoundingClientRect(visual);
2396
+ const X = type === 'touchstart' ? touches[0].pageX : pageX;
2397
+ const Y = type === 'touchstart' ? touches[0].pageY : pageY;
2398
+ const offsetX = X - window.pageXOffset - visualRect.left;
2399
+ const offsetY = Y - window.pageYOffset - visualRect.top;
2400
+
2401
+ if (target === v1 || target === c1) {
2402
+ self.dragElement = visual;
2403
+ self.changeControl1({ offsetX, offsetY });
2404
+ } else if (target === v2 || target === c2) {
2405
+ self.dragElement = visual;
2406
+ self.changeControl2({ offsetY });
2407
+ } else if (format !== 'hex' && (target === v3 || target === c3)) {
2408
+ self.dragElement = visual;
2409
+ self.changeAlpha({ offsetY });
2410
+ }
2411
+ e.preventDefault();
2412
+ }
2413
+
2414
+ /**
2415
+ * Handles the `ColorPicker` touchend / mouseup events listeners.
2416
+ * @param {TouchEvent} e
2417
+ * @this {ColorPicker}
2418
+ */
2419
+ pointerUp({ target }) {
2420
+ const self = this;
2421
+ const selection = document.getSelection();
2422
+ // @ts-ignore
2423
+ if (!self.dragElement && !selection.toString().length
2424
+ // @ts-ignore
2425
+ && !self.parent.contains(target)) {
2426
+ self.hide();
2427
+ }
2428
+
2429
+ self.dragElement = null;
2430
+ }
2431
+
2432
+ /**
2433
+ * Handles the `ColorPicker` touchmove / mousemove events listeners.
2434
+ * @param {TouchEvent} e
2435
+ */
2436
+ pointerMove(e) {
2437
+ const self = this;
2438
+ const { dragElement, visuals, format } = self;
2439
+ const [v1, v2, v3] = visuals;
2440
+ const {
2441
+ // @ts-ignore
2442
+ type, touches, pageX, pageY,
2443
+ } = e;
2444
+
2445
+ if (!dragElement) return;
2446
+
2447
+ const controlRect = getBoundingClientRect(dragElement);
2448
+ const X = type === 'touchmove' ? touches[0].pageX : pageX;
2449
+ const Y = type === 'touchmove' ? touches[0].pageY : pageY;
2450
+ const offsetX = X - window.pageXOffset - controlRect.left;
2451
+ const offsetY = Y - window.pageYOffset - controlRect.top;
2452
+
2453
+ if (dragElement === v1) {
2454
+ self.changeControl1({ offsetX, offsetY });
2455
+ }
2456
+
2457
+ if (dragElement === v2) {
2458
+ self.changeControl2({ offsetY });
2459
+ }
2460
+
2461
+ if (dragElement === v3 && format !== 'hex') {
2462
+ self.changeAlpha({ offsetY });
2463
+ }
2464
+ }
2465
+
2466
+ /**
2467
+ * Handles the `ColorPicker` events listeners associated with the color knobs.
2468
+ * @param {KeyboardEvent} e
2469
+ */
2470
+ handleKnobs(e) {
2471
+ const { target, code } = e;
2472
+ const self = this;
2473
+
2474
+ // only react to arrow buttons
2475
+ if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
2476
+ e.preventDefault();
2477
+
2478
+ const { activeElement } = document;
2479
+ const { controlKnobs } = self;
2480
+ const currentKnob = controlKnobs.find((x) => x === activeElement);
2481
+ const [c1, c2, c3] = controlKnobs;
2482
+
2483
+ if (currentKnob) {
2484
+ let offsetX = 0;
2485
+ let offsetY = 0;
2486
+ if (target === c1) {
2487
+ if ([keyArrowLeft, keyArrowRight].includes(code)) {
2488
+ self.controlPositions.c1x += code === keyArrowRight ? +1 : -1;
2489
+ } else if ([keyArrowUp, keyArrowDown].includes(code)) {
2490
+ self.controlPositions.c1y += code === keyArrowDown ? +1 : -1;
2491
+ }
2492
+
2493
+ offsetX = self.controlPositions.c1x;
2494
+ offsetY = self.controlPositions.c1y;
2495
+ self.changeControl1({ offsetX, offsetY });
2496
+ } else if (target === c2) {
2497
+ self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
2498
+ offsetY = self.controlPositions.c2y;
2499
+ self.changeControl2({ offsetY });
2500
+ } else if (target === c3) {
2501
+ self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
2502
+ offsetY = self.controlPositions.c3y;
2503
+ self.changeAlpha({ offsetY });
2504
+ }
2505
+
2506
+ self.setColorAppearence();
2507
+ self.updateInputs();
2508
+ self.updateControls();
2509
+ self.updateVisuals();
2510
+ self.handleScroll(e);
2511
+ }
2512
+ }
2513
+
2514
+ /** Handles the event listeners of the color form. */
2515
+ changeHandler() {
2516
+ const self = this;
2517
+ let colorSource;
2518
+ /** @type {HTMLInputElement} */
2519
+ // @ts-ignore
2520
+ const { activeElement } = document;
2521
+ const {
2522
+ inputs, format, value: currentValue, input,
2523
+ } = self;
2524
+ const [i1, i2, i3, i4] = inputs;
2525
+ const isNonColorValue = self.includeNonColor && nonColors.includes(currentValue);
2526
+
2527
+ if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
2528
+ if (activeElement === input) {
2529
+ if (isNonColorValue) {
2530
+ colorSource = 'white';
2531
+ } else {
2532
+ colorSource = currentValue;
2533
+ }
2534
+ } else if (format === 'hex') {
2535
+ colorSource = i1.value;
2536
+ } else if (format === 'hsl') {
2537
+ colorSource = `hsla(${i1.value},${i2.value}%,${i3.value}%,${i4.value})`;
2538
+ } else {
2539
+ colorSource = `rgba(${inputs.map((x) => x.value).join(',')})`;
2540
+ }
2541
+
2542
+ self.color = new Color(colorSource, { format });
2543
+ self.setControlPositions();
2544
+ self.setColorAppearence();
2545
+ self.updateInputs();
2546
+ self.updateControls();
2547
+ self.updateVisuals();
2548
+
2549
+ // set non-color keyword
2550
+ if (activeElement === input && isNonColorValue) {
2551
+ self.value = currentValue;
2552
+ }
2553
+ }
2554
+ }
2555
+
2556
+ /**
2557
+ * Updates `ColorPicker` first control:
2558
+ * * `lightness` and `saturation` for HEX/RGB;
2559
+ * * `lightness` and `hue` for HSL.
2560
+ *
2561
+ * @param {Record<string, number>} offsets
2562
+ */
2563
+ changeControl1(offsets) {
2564
+ const self = this;
2565
+ let [offsetX, offsetY] = [0, 0];
2566
+ const { offsetX: X, offsetY: Y } = offsets;
2567
+ const {
2568
+ format, controlPositions,
2569
+ height1, height2, height3, width1,
2570
+ } = self;
2571
+
2572
+ if (X > width1) {
2573
+ offsetX = width1;
2574
+ } else if (X >= 0) {
2575
+ offsetX = X;
2576
+ }
2577
+
2578
+ if (Y > height1) {
2579
+ offsetY = height1;
2580
+ } else if (Y >= 0) {
2581
+ offsetY = Y;
2582
+ }
2583
+
2584
+ const hue = format !== 'hsl'
2585
+ ? Math.round((controlPositions.c2y / height2) * 360)
2586
+ : Math.round((offsetX / width1) * 360);
2587
+
2588
+ const saturation = format !== 'hsl'
2589
+ ? Math.round((offsetX / width1) * 100)
2590
+ : Math.round((1 - controlPositions.c2y / height2) * 100);
2591
+
2592
+ const lightness = Math.round((1 - offsetY / height1) * 100);
2593
+ const alpha = format !== 'hex' ? Math.round((1 - controlPositions.c3y / height3) * 100) / 100 : 1;
2594
+ const tempFormat = format !== 'hsl' ? 'hsva' : 'hsla';
2595
+
2596
+ // new color
2597
+ self.color = new Color(`${tempFormat}(${hue},${saturation}%,${lightness}%,${alpha})`, { format });
2598
+ // new positions
2599
+ self.controlPositions.c1x = offsetX;
2600
+ self.controlPositions.c1y = offsetY;
2601
+
2602
+ // update color picker
2603
+ self.setColorAppearence();
2604
+ self.updateInputs();
2605
+ self.updateControls();
2606
+ self.updateVisuals();
2607
+ }
2608
+
2609
+ /**
2610
+ * Updates `ColorPicker` second control:
2611
+ * * `hue` for HEX/RGB;
2612
+ * * `saturation` for HSL.
2613
+ *
2614
+ * @param {Record<string, number>} offset
2615
+ */
2616
+ changeControl2(offset) {
2617
+ const self = this;
2618
+ const { offsetY: Y } = offset;
2619
+ const {
2620
+ format, width1, height1, height2, height3, controlPositions,
2621
+ } = self;
2622
+ let offsetY = 0;
2623
+
2624
+ if (Y > height2) {
2625
+ offsetY = height2;
2626
+ } else if (Y >= 0) {
2627
+ offsetY = Y;
2628
+ }
2629
+
2630
+ const hue = format !== 'hsl' ? Math.round((offsetY / height2) * 360) : Math.round((controlPositions.c1x / width1) * 360);
2631
+ const saturation = format !== 'hsl' ? Math.round((controlPositions.c1x / width1) * 100) : Math.round((1 - offsetY / height2) * 100);
2632
+ const lightness = Math.round((1 - controlPositions.c1y / height1) * 100);
2633
+ const alpha = format !== 'hex' ? Math.round((1 - controlPositions.c3y / height3) * 100) / 100 : 1;
2634
+ const colorFormat = format !== 'hsl' ? 'hsva' : 'hsla';
2635
+
2636
+ // new color
2637
+ self.color = new Color(`${colorFormat}(${hue},${saturation}%,${lightness}%,${alpha})`, { format });
2638
+ // new position
2639
+ self.controlPositions.c2y = offsetY;
2640
+ // update color picker
2641
+ self.setColorAppearence();
2642
+ self.updateInputs();
2643
+ self.updateControls();
2644
+ self.updateVisuals();
2645
+ }
2646
+
2647
+ /**
2648
+ * Updates `ColorPicker` last control,
2649
+ * the `alpha` channel for RGB/HSL.
2650
+ *
2651
+ * @param {Record<string, number>} offset
2652
+ */
2653
+ changeAlpha(offset) {
2654
+ const self = this;
2655
+ const { height3 } = self;
2656
+ const { offsetY: Y } = offset;
2657
+ let offsetY = 0;
2658
+
2659
+ if (Y > height3) {
2660
+ offsetY = height3;
2661
+ } else if (Y >= 0) {
2662
+ offsetY = Y;
2663
+ }
2664
+
2665
+ // update color alpha
2666
+ const alpha = Math.round((1 - offsetY / height3) * 100);
2667
+ self.color.setAlpha(alpha / 100);
2668
+ // update position
2669
+ self.controlPositions.c3y = offsetY;
2670
+ // update color picker
2671
+ self.updateInputs();
2672
+ self.updateControls();
2673
+ // alpha?
2674
+ self.updateVisuals();
2675
+ }
2676
+
2677
+ /** Update opened dropdown position on scroll. */
2678
+ updateDropdownPosition() {
2679
+ const self = this;
2680
+ const { input, colorPicker, colorMenu } = self;
2681
+ const elRect = getBoundingClientRect(input);
2682
+ const { offsetHeight: elHeight } = input;
2683
+ const windowHeight = document.documentElement.clientHeight;
2684
+ const isPicker = classToggle(colorPicker, true);
2685
+ const dropdown = isPicker ? colorPicker : colorMenu;
2686
+ const { offsetHeight: dropHeight } = dropdown;
2687
+ const distanceBottom = windowHeight - elRect.bottom;
2688
+ const distanceTop = elRect.top;
2689
+ const bottomExceed = elRect.top + dropHeight + elHeight > windowHeight; // show
2690
+ const topExceed = elRect.top - dropHeight < 0; // show-top
2691
+
2692
+ if (hasClass(dropdown, 'show') && distanceBottom < distanceTop && bottomExceed) {
2693
+ removeClass(dropdown, 'show');
2694
+ addClass(dropdown, 'show-top');
2695
+ }
2696
+ if (hasClass(dropdown, 'show-top') && distanceBottom > distanceTop && topExceed) {
2697
+ removeClass(dropdown, 'show-top');
2698
+ addClass(dropdown, 'show');
2699
+ }
2700
+ }
2701
+
2702
+ /** Update control knobs' positions. */
2703
+ setControlPositions() {
2704
+ const self = this;
2705
+ const {
2706
+ hsv, hsl, format, height1, height2, height3, width1,
2707
+ } = self;
2708
+ const hue = hsl.h;
2709
+ const saturation = format !== 'hsl' ? hsv.s : hsl.s;
2710
+ const lightness = format !== 'hsl' ? hsv.v : hsl.l;
2711
+ const alpha = hsv.a;
2712
+
2713
+ self.controlPositions.c1x = format !== 'hsl' ? saturation * width1 : (hue / 360) * width1;
2714
+ self.controlPositions.c1y = (1 - lightness) * height1;
2715
+ self.controlPositions.c2y = format !== 'hsl' ? (hue / 360) * height2 : (1 - saturation) * height2;
2716
+
2717
+ if (format !== 'hex') {
2718
+ self.controlPositions.c3y = (1 - alpha) * height3;
2719
+ }
2720
+ }
2721
+
2722
+ /** Update the visual appearance label. */
2723
+ setColorAppearence() {
2724
+ const self = this;
2725
+ const {
2726
+ componentLabels, colorLabels, hsl, hsv, hex, format, knobLabels,
2727
+ } = self;
2728
+ const {
2729
+ lightnessLabel, saturationLabel, hueLabel, alphaLabel, appearanceLabel, hexLabel,
2730
+ } = componentLabels;
2731
+ let { requiredLabel } = componentLabels;
2732
+ const [knob1Lbl, knob2Lbl, knob3Lbl] = knobLabels;
2733
+ const hue = Math.round(hsl.h);
2734
+ const alpha = hsv.a;
2735
+ const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
2736
+ const saturation = Math.round(saturationSource * 100);
2737
+ const lightness = Math.round(hsl.l * 100);
2738
+ const hsvl = hsv.v * 100;
2739
+ let colorName;
2740
+
2741
+ // determine color appearance
2742
+ if (lightness === 100 && saturation === 0) {
2743
+ colorName = colorLabels.white;
2744
+ } else if (lightness === 0) {
2745
+ colorName = colorLabels.black;
2746
+ } else if (saturation === 0) {
2747
+ colorName = colorLabels.grey;
2748
+ } else if (hue < 15 || hue >= 345) {
2749
+ colorName = colorLabels.red;
2750
+ } else if (hue >= 15 && hue < 45) {
2751
+ colorName = hsvl > 80 && saturation > 80 ? colorLabels.orange : colorLabels.brown;
2752
+ } else if (hue >= 45 && hue < 75) {
2753
+ const isGold = hue > 46 && hue < 54 && hsvl < 80 && saturation > 90;
2754
+ const isOlive = hue >= 54 && hue < 75 && hsvl < 80;
2755
+ colorName = isGold ? colorLabels.gold : colorLabels.yellow;
2756
+ colorName = isOlive ? colorLabels.olive : colorName;
2757
+ } else if (hue >= 75 && hue < 155) {
2758
+ colorName = hsvl < 68 ? colorLabels.green : colorLabels.lime;
2759
+ } else if (hue >= 155 && hue < 175) {
2760
+ colorName = colorLabels.teal;
2761
+ } else if (hue >= 175 && hue < 195) {
2762
+ colorName = colorLabels.cyan;
2763
+ } else if (hue >= 195 && hue < 255) {
2764
+ colorName = colorLabels.blue;
2765
+ } else if (hue >= 255 && hue < 270) {
2766
+ colorName = colorLabels.violet;
2767
+ } else if (hue >= 270 && hue < 295) {
2768
+ colorName = colorLabels.magenta;
2769
+ } else if (hue >= 295 && hue < 345) {
2770
+ colorName = colorLabels.pink;
2771
+ }
2772
+
2773
+ if (format === 'hsl') {
2774
+ knob1Lbl.innerText = `${hueLabel}: ${hue}°. ${lightnessLabel}: ${lightness}%`;
2775
+ knob2Lbl.innerText = `${saturationLabel}: ${saturation}%`;
2776
+ } else {
2777
+ knob1Lbl.innerText = `${lightnessLabel}: ${lightness}%. ${saturationLabel}: ${saturation}%`;
2778
+ knob2Lbl.innerText = `${hueLabel}: ${hue}°`;
2779
+ }
2780
+
2781
+ if (format !== 'hex') {
2782
+ const alphaValue = Math.round(alpha * 100);
2783
+ knob3Lbl.innerText = `${alphaLabel}: ${alphaValue}%`;
2784
+ }
2785
+
2786
+ // update color labels
2787
+ self.appearance.innerText = `${appearanceLabel}: ${colorName}.`;
2788
+ const colorLabel = format === 'hex'
2789
+ ? `${hexLabel} ${hex.split('').join(' ')}.`
2790
+ : self.value.toUpperCase();
2791
+
2792
+ if (self.label) {
2793
+ const fieldLabel = self.label.innerText.replace('*', '').trim();
2794
+ /** @type {HTMLSpanElement} */
2795
+ // @ts-ignore
2796
+ const [pickerBtnSpan] = self.pickerToggle.children;
2797
+ requiredLabel = self.required ? ` ${requiredLabel}` : '';
2798
+ pickerBtnSpan.innerText = `${fieldLabel}: ${colorLabel}${requiredLabel}`;
2799
+ }
2800
+ }
2801
+
2802
+ /** Updates the control knobs positions. */
2803
+ updateControls() {
2804
+ const { format, controlKnobs, controlPositions } = this;
2805
+ const [control1, control2, control3] = controlKnobs;
2806
+ control1.style.transform = `translate3d(${controlPositions.c1x - 3}px,${controlPositions.c1y - 3}px,0)`;
2807
+ control2.style.transform = `translate3d(0,${controlPositions.c2y - 3}px,0)`;
2808
+
2809
+ if (format !== 'hex') {
2810
+ control3.style.transform = `translate3d(0,${controlPositions.c3y - 3}px,0)`;
2811
+ }
2812
+ }
2813
+
2814
+ /**
2815
+ * Update all color form inputs.
2816
+ * @param {boolean=} isPrevented when `true`, the component original event is prevented
2817
+ */
2818
+ updateInputs(isPrevented) {
2819
+ const self = this;
2820
+ const {
2821
+ value: oldColor, rgb, hsl, hsv, format, parent, input, inputs,
2822
+ } = self;
2823
+ const [i1, i2, i3, i4] = inputs;
2824
+
2825
+ const alpha = hsl.a;
2826
+ const hue = Math.round(hsl.h);
2827
+ const saturation = Math.round(hsl.s * 100);
2828
+ const lightSource = format === 'hsl' ? hsl.l : hsv.v;
2829
+ const lightness = Math.round(lightSource * 100);
2830
+ let newColor;
2831
+
2832
+ if (format === 'hex') {
2833
+ newColor = self.color.toHexString();
2834
+ i1.value = self.hex;
2835
+ } else if (format === 'hsl') {
2836
+ newColor = self.color.toHslString();
2837
+ i1.value = `${hue}`;
2838
+ i2.value = `${saturation}`;
2839
+ i3.value = `${lightness}`;
2840
+ i4.value = `${alpha}`;
2841
+ } else if (format === 'rgb') {
2842
+ newColor = self.color.toRgbString();
2843
+ i1.value = `${rgb.r}`;
2844
+ i2.value = `${rgb.g}`;
2845
+ i3.value = `${rgb.b}`;
2846
+ i4.value = `${alpha}`;
2847
+ }
2848
+
2849
+ // update the color value
2850
+ self.value = `${newColor}`;
2851
+
2852
+ // update the input backgroundColor
2853
+ ObjectAssign(input.style, { backgroundColor: newColor });
2854
+
2855
+ // toggle dark/light classes will also style the placeholder
2856
+ // dark sets color white, light sets color black
2857
+ // isDark ? '#000' : '#fff'
2858
+ if (!self.isDark) {
2859
+ if (hasClass(parent, 'dark')) removeClass(parent, 'dark');
2860
+ if (!hasClass(parent, 'light')) addClass(parent, 'light');
2861
+ } else {
2862
+ if (hasClass(parent, 'light')) removeClass(parent, 'light');
2863
+ if (!hasClass(parent, 'dark')) addClass(parent, 'dark');
2864
+ }
2865
+
2866
+ // don't trigger the custom event unless it's really changed
2867
+ if (!isPrevented && newColor !== oldColor) {
2868
+ firePickerChange(self);
2869
+ }
2870
+ }
2871
+
2872
+ /**
2873
+ * Handles the `Space` and `Enter` keys inputs.
2874
+ * @param {KeyboardEvent} e
2875
+ * @this {ColorPicker}
2876
+ */
2877
+ keyHandler(e) {
2878
+ const self = this;
2879
+ const { menuToggle } = self;
2880
+ const { activeElement } = document;
2881
+ const { code } = e;
2882
+
2883
+ if ([keyEnter, keySpace].includes(code)) {
2884
+ if ((menuToggle && activeElement === menuToggle) || !activeElement) {
2885
+ e.preventDefault();
2886
+ if (!activeElement) {
2887
+ self.togglePicker(e);
2888
+ } else {
2889
+ self.toggleMenu();
2890
+ }
2891
+ }
2892
+ }
2893
+ }
2894
+
2895
+ /**
2896
+ * Toggle the `ColorPicker` dropdown visibility.
2897
+ * @param {Event} e
2898
+ * @this {ColorPicker}
2899
+ */
2900
+ togglePicker(e) {
2901
+ e.preventDefault();
2902
+ const self = this;
2903
+ const pickerIsOpen = classToggle(self.colorPicker, true);
2904
+
2905
+ if (self.isOpen && pickerIsOpen) {
2906
+ self.hide(true);
2907
+ } else {
2908
+ self.showPicker();
2909
+ }
2910
+ }
2911
+
2912
+ /** Shows the `ColorPicker` dropdown. */
2913
+ showPicker() {
2914
+ const self = this;
2915
+ classToggle(self.colorMenu);
2916
+ addClass(self.colorPicker, 'show');
2917
+ self.input.focus();
2918
+ self.show();
2919
+ setAttribute(self.pickerToggle, ariaExpanded, 'true');
2920
+ }
2921
+
2922
+ /** Toggles the visibility of the `ColorPicker` presets menu. */
2923
+ toggleMenu() {
2924
+ const self = this;
2925
+ const menuIsOpen = classToggle(self.colorMenu, true);
2926
+
2927
+ if (self.isOpen && menuIsOpen) {
2928
+ self.hide(true);
2929
+ } else {
2930
+ showMenu(self);
2931
+ }
2932
+ }
2933
+
2934
+ /** Show the dropdown. */
2935
+ show() {
2936
+ const self = this;
2937
+ if (!self.isOpen) {
2938
+ addClass(self.parent, 'open');
2939
+ toggleEventsOnShown(self, true);
2940
+ self.updateDropdownPosition();
2941
+ self.isOpen = true;
2942
+ }
2943
+ }
2944
+
2945
+ /**
2946
+ * Hides the currently opened dropdown.
2947
+ * @param {boolean=} focusPrevented
2948
+ */
2949
+ hide(focusPrevented) {
2950
+ const self = this;
2951
+ if (self.isOpen) {
2952
+ const { pickerToggle, colorMenu } = self;
2953
+ toggleEventsOnShown(self);
2954
+
2955
+ removeClass(self.parent, 'open');
2956
+
2957
+ classToggle(self.colorPicker);
2958
+ setAttribute(pickerToggle, ariaExpanded, 'false');
2959
+
2960
+ if (colorMenu) {
2961
+ classToggle(colorMenu);
2962
+ setAttribute(self.menuToggle, ariaExpanded, 'false');
2963
+ }
2964
+
2965
+ if (!self.isValid) {
2966
+ self.value = self.color.toString();
2967
+ }
2968
+
2969
+ self.isOpen = false;
2970
+
2971
+ if (!focusPrevented) {
2972
+ pickerToggle.focus();
2973
+ }
2974
+ }
2975
+ }
2976
+
2977
+ dispose() {
2978
+ const self = this;
2979
+ const { input, parent } = self;
2980
+ self.hide(true);
2981
+ toggleEvents(self);
2982
+ [...parent.children].forEach((el) => {
2983
+ if (el !== input) el.remove();
2984
+ });
2985
+ Data.remove(input, colorPickerString);
2986
+ }
2987
+ }
2988
+
2989
+ ObjectAssign(ColorPicker, {
2990
+ Color,
2991
+ getInstance: getColorPickerInstance,
2992
+ init: initColorPicker,
2993
+ selector: colorPickerSelector,
2994
+ });
2995
+
2996
+ /**
2997
+ * `ColorPickerElement` Web Component.
2998
+ * @example
2999
+ * <color-picker>
3000
+ * <input type="text">
3001
+ * </color-picker>
3002
+ */
3003
+ class ColorPickerElement extends HTMLElement {
3004
+ constructor() {
3005
+ super();
3006
+ /** @type {ColorPicker?} */
3007
+ this.colorPicker = null;
3008
+ /** @type {HTMLInputElement} */
3009
+ // @ts-ignore - `HTMLInputElement` is also `HTMLElement`
3010
+ this.input = querySelector('input', this);
3011
+ /** @type {boolean} */
3012
+ this.isDisconnected = true;
3013
+ this.attachShadow({ mode: 'open' });
3014
+ }
3015
+
3016
+ get value() { return this.input.value; }
3017
+
3018
+ get color() { return this.colorPicker && this.colorPicker.color; }
3019
+
3020
+ connectedCallback() {
3021
+ if (this.colorPicker) {
3022
+ if (this.isDisconnected) {
3023
+ this.isDisconnected = false;
3024
+ }
3025
+ return;
3026
+ }
3027
+
3028
+ this.colorPicker = new ColorPicker(this.input);
3029
+ this.isDisconnected = false;
3030
+
3031
+ if (this.shadowRoot) {
3032
+ this.shadowRoot.append(createElement('slot'));
3033
+ }
3034
+ }
3035
+
3036
+ disconnectedCallback() {
3037
+ if (this.colorPicker) this.colorPicker.dispose();
3038
+ this.isDisconnected = true;
3039
+ }
3040
+ }
3041
+
3042
+ ObjectAssign(ColorPickerElement, {
3043
+ Color,
3044
+ ColorPicker,
3045
+ });
3046
+
3047
+ customElements.define('color-picker', ColorPickerElement);
3048
+
3049
+ return ColorPickerElement;
3050
+
3051
+ })));