@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,3006 @@
1
+ /*!
2
+ * ColorPicker 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.ColorPicker = 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 shortcut for `(document|Element).querySelectorAll`.
31
+ *
32
+ * @param {string} selector the input selector
33
+ * @param {(HTMLElement | Element | Document | Node)=} parent optional node to look into
34
+ * @return {NodeListOf<HTMLElement | Element>} the query result
35
+ */
36
+ function querySelectorAll(selector, parent) {
37
+ const lookUp = parent && parentNodes
38
+ .some((x) => parent instanceof x) ? parent : getDocument();
39
+ // @ts-ignore -- `ShadowRoot` is also a node
40
+ return lookUp.querySelectorAll(selector);
41
+ }
42
+
43
+ /** @type {Record<string, any>} */
44
+ const EventRegistry = {};
45
+
46
+ /**
47
+ * The global event listener.
48
+ *
49
+ * @this {Element | HTMLElement | Window | Document}
50
+ * @param {Event} e
51
+ * @returns {void}
52
+ */
53
+ function globalListener(e) {
54
+ const that = this;
55
+ const { type } = e;
56
+ const oneEvMap = EventRegistry[type] ? [...EventRegistry[type]] : [];
57
+
58
+ oneEvMap.forEach((elementsMap) => {
59
+ const [element, listenersMap] = elementsMap;
60
+ [...listenersMap].forEach((listenerMap) => {
61
+ if (element === that) {
62
+ const [listener, options] = listenerMap;
63
+ listener.apply(element, [e]);
64
+
65
+ if (options && options.once) {
66
+ removeListener(element, type, listener, options);
67
+ }
68
+ }
69
+ });
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Register a new listener with its options and attach the `globalListener`
75
+ * to the target if this is the first listener.
76
+ *
77
+ * @param {Element | HTMLElement | Window | Document} element
78
+ * @param {string} eventType
79
+ * @param {EventListenerObject['handleEvent']} listener
80
+ * @param {AddEventListenerOptions=} options
81
+ */
82
+ const addListener = (element, eventType, listener, options) => {
83
+ // get element listeners first
84
+ if (!EventRegistry[eventType]) {
85
+ EventRegistry[eventType] = new Map();
86
+ }
87
+ const oneEventMap = EventRegistry[eventType];
88
+
89
+ if (!oneEventMap.has(element)) {
90
+ oneEventMap.set(element, new Map());
91
+ }
92
+ const oneElementMap = oneEventMap.get(element);
93
+
94
+ // get listeners size
95
+ const { size } = oneElementMap;
96
+
97
+ // register listener with its options
98
+ if (oneElementMap) {
99
+ oneElementMap.set(listener, options);
100
+ }
101
+
102
+ // add listener last
103
+ if (!size) {
104
+ element.addEventListener(eventType, globalListener, options);
105
+ }
106
+ };
107
+
108
+ /**
109
+ * Remove a listener from registry and detach the `globalListener`
110
+ * if no listeners are found in the registry.
111
+ *
112
+ * @param {Element | HTMLElement | Window | Document} element
113
+ * @param {string} eventType
114
+ * @param {EventListenerObject['handleEvent']} listener
115
+ * @param {AddEventListenerOptions=} options
116
+ */
117
+ const removeListener = (element, eventType, listener, options) => {
118
+ // get listener first
119
+ const oneEventMap = EventRegistry[eventType];
120
+ const oneElementMap = oneEventMap && oneEventMap.get(element);
121
+ const savedOptions = oneElementMap && oneElementMap.get(listener);
122
+
123
+ // also recover initial options
124
+ const { options: eventOptions } = savedOptions !== undefined
125
+ ? savedOptions
126
+ : { options };
127
+
128
+ // unsubscribe second, remove from registry
129
+ if (oneElementMap && oneElementMap.has(listener)) oneElementMap.delete(listener);
130
+ if (oneEventMap && (!oneElementMap || !oneElementMap.size)) oneEventMap.delete(element);
131
+ if (!oneEventMap || !oneEventMap.size) delete EventRegistry[eventType];
132
+
133
+ // remove listener last
134
+ if (!oneElementMap || !oneElementMap.size) {
135
+ element.removeEventListener(eventType, globalListener, eventOptions);
136
+ }
137
+ };
138
+
139
+ /**
140
+ * A global namespace for aria-selected.
141
+ * @type {string}
142
+ */
143
+ const ariaSelected = 'aria-selected';
144
+
145
+ /**
146
+ * A global namespace for aria-expanded.
147
+ * @type {string}
148
+ */
149
+ const ariaExpanded = 'aria-expanded';
150
+
151
+ /**
152
+ * A global namespace for aria-hidden.
153
+ * @type {string}
154
+ */
155
+ const ariaHidden = 'aria-hidden';
156
+
157
+ /**
158
+ * A global namespace for aria-labelledby.
159
+ * @type {string}
160
+ */
161
+ const ariaLabelledBy = 'aria-labelledby';
162
+
163
+ /**
164
+ * A global namespace for `ArrowDown` key.
165
+ * @type {string} e.which = 40 equivalent
166
+ */
167
+ const keyArrowDown = 'ArrowDown';
168
+
169
+ /**
170
+ * A global namespace for `ArrowUp` key.
171
+ * @type {string} e.which = 38 equivalent
172
+ */
173
+ const keyArrowUp = 'ArrowUp';
174
+
175
+ /**
176
+ * A global namespace for `ArrowLeft` key.
177
+ * @type {string} e.which = 37 equivalent
178
+ */
179
+ const keyArrowLeft = 'ArrowLeft';
180
+
181
+ /**
182
+ * A global namespace for `ArrowRight` key.
183
+ * @type {string} e.which = 39 equivalent
184
+ */
185
+ const keyArrowRight = 'ArrowRight';
186
+
187
+ /**
188
+ * A global namespace for `Enter` key.
189
+ * @type {string} e.which = 13 equivalent
190
+ */
191
+ const keyEnter = 'Enter';
192
+
193
+ /**
194
+ * A global namespace for `Space` key.
195
+ * @type {string} e.which = 32 equivalent
196
+ */
197
+ const keySpace = 'Space';
198
+
199
+ /**
200
+ * A global namespace for `Escape` key.
201
+ * @type {string} e.which = 27 equivalent
202
+ */
203
+ const keyEscape = 'Escape';
204
+
205
+ // @ts-ignore
206
+ const { userAgentData: uaDATA } = navigator;
207
+
208
+ /**
209
+ * A global namespace for `userAgentData` object.
210
+ */
211
+ const userAgentData = uaDATA;
212
+
213
+ const { userAgent: userAgentString } = navigator;
214
+
215
+ /**
216
+ * A global namespace for `navigator.userAgent` string.
217
+ */
218
+ const userAgent = userAgentString;
219
+
220
+ const mobileBrands = /iPhone|iPad|iPod|Android/i;
221
+ let isMobileCheck = false;
222
+
223
+ if (userAgentData) {
224
+ isMobileCheck = userAgentData.brands
225
+ .some((/** @type {Record<String, any>} */x) => mobileBrands.test(x.brand));
226
+ } else {
227
+ isMobileCheck = mobileBrands.test(userAgent);
228
+ }
229
+
230
+ /**
231
+ * A global `boolean` for mobile detection.
232
+ * @type {boolean}
233
+ */
234
+ const isMobile = isMobileCheck;
235
+
236
+ let elementUID = 1;
237
+ const elementIDMap = new Map();
238
+
239
+ /**
240
+ * Returns a unique identifier for popover, tooltip, scrollspy.
241
+ *
242
+ * @param {HTMLElement | Element} element target element
243
+ * @param {string=} key predefined key
244
+ * @returns {number} an existing or new unique ID
245
+ */
246
+ function getUID(element, key) {
247
+ elementUID += 1;
248
+ let elMap = elementIDMap.get(element);
249
+ let result = elementUID;
250
+
251
+ if (key && key.length) {
252
+ if (elMap) {
253
+ const elMapId = elMap.get(key);
254
+ if (!Number.isNaN(elMapId)) {
255
+ result = elMapId;
256
+ } else {
257
+ elMap.set(key, result);
258
+ }
259
+ } else {
260
+ elementIDMap.set(element, new Map());
261
+ elMap = elementIDMap.get(element);
262
+ elMap.set(key, result);
263
+ }
264
+ } else if (!Number.isNaN(elMap)) {
265
+ result = elMap;
266
+ } else {
267
+ elementIDMap.set(element, result);
268
+ }
269
+ return result;
270
+ }
271
+
272
+ /**
273
+ * Returns the bounding client rect of a target `HTMLElement`.
274
+ *
275
+ * @see https://github.com/floating-ui/floating-ui
276
+ *
277
+ * @param {HTMLElement | Element} element event.target
278
+ * @param {boolean=} includeScale when *true*, the target scale is also computed
279
+ * @returns {SHORTER.BoundingClientRect} the bounding client rect object
280
+ */
281
+ function getBoundingClientRect(element, includeScale) {
282
+ const {
283
+ width, height, top, right, bottom, left,
284
+ } = element.getBoundingClientRect();
285
+ let scaleX = 1;
286
+ let scaleY = 1;
287
+
288
+ if (includeScale && element instanceof HTMLElement) {
289
+ const { offsetWidth, offsetHeight } = element;
290
+ scaleX = offsetWidth > 0 ? Math.round(width) / offsetWidth || 1 : 1;
291
+ scaleY = offsetHeight > 0 ? Math.round(height) / offsetHeight || 1 : 1;
292
+ }
293
+
294
+ return {
295
+ width: width / scaleX,
296
+ height: height / scaleY,
297
+ top: top / scaleY,
298
+ right: right / scaleX,
299
+ bottom: bottom / scaleY,
300
+ left: left / scaleX,
301
+ x: left / scaleX,
302
+ y: top / scaleY,
303
+ };
304
+ }
305
+
306
+ /**
307
+ * A global array with `Element` | `HTMLElement`.
308
+ */
309
+ const elementNodes = [Element, HTMLElement];
310
+
311
+ /**
312
+ * Utility to check if target is typeof `HTMLElement`, `Element`, `Node`
313
+ * or find one that matches a selector.
314
+ *
315
+ * @param {HTMLElement | Element | string} selector the input selector or target element
316
+ * @param {(HTMLElement | Element | Document)=} parent optional node to look into
317
+ * @return {(HTMLElement | Element)?} the `HTMLElement` or `querySelector` result
318
+ */
319
+ function querySelector(selector, parent) {
320
+ const lookUp = parentNodes.some((x) => parent instanceof x)
321
+ ? parent : getDocument();
322
+
323
+ // @ts-ignore
324
+ return elementNodes.some((x) => selector instanceof x)
325
+ // @ts-ignore
326
+ ? selector : lookUp.querySelector(selector);
327
+ }
328
+
329
+ /**
330
+ * Shortcut for `HTMLElement.closest` method which also works
331
+ * with children of `ShadowRoot`. The order of the parameters
332
+ * is intentional since they're both required.
333
+ *
334
+ * @see https://stackoverflow.com/q/54520554/803358
335
+ *
336
+ * @param {HTMLElement | Element} element Element to look into
337
+ * @param {string} selector the selector name
338
+ * @return {(HTMLElement | Element)?} the query result
339
+ */
340
+ function closest(element, selector) {
341
+ return element ? (element.closest(selector)
342
+ // @ts-ignore -- break out of `ShadowRoot`
343
+ || closest(element.getRootNode().host, selector)) : null;
344
+ }
345
+
346
+ /**
347
+ * Shortcut for `Object.assign()` static method.
348
+ * @param {Record<string, any>} obj a target object
349
+ * @param {Record<string, any>} source a source object
350
+ */
351
+ const ObjectAssign = (obj, source) => Object.assign(obj, source);
352
+
353
+ /**
354
+ * This is a shortie for `document.createElement` method
355
+ * which allows you to create a new `HTMLElement` for a given `tagName`
356
+ * or based on an object with specific non-readonly attributes:
357
+ * `id`, `className`, `textContent`, `style`, etc.
358
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
359
+ *
360
+ * @param {Record<string, string> | string} param `tagName` or object
361
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
362
+ */
363
+ function createElement(param) {
364
+ if (typeof param === 'string') {
365
+ return getDocument().createElement(param);
366
+ }
367
+
368
+ const { tagName } = param;
369
+ const attr = { ...param };
370
+ const newElement = createElement(tagName);
371
+ delete attr.tagName;
372
+ ObjectAssign(newElement, attr);
373
+ return newElement;
374
+ }
375
+
376
+ /**
377
+ * This is a shortie for `document.createElementNS` method
378
+ * which allows you to create a new `HTMLElement` for a given `tagName`
379
+ * or based on an object with specific non-readonly attributes:
380
+ * `id`, `className`, `textContent`, `style`, etc.
381
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
382
+ *
383
+ * @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
384
+ * @param {Record<string, string> | string} param `tagName` or object
385
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
386
+ */
387
+ function createElementNS(namespace, param) {
388
+ if (typeof param === 'string') {
389
+ return getDocument().createElementNS(namespace, param);
390
+ }
391
+
392
+ const { tagName } = param;
393
+ const attr = { ...param };
394
+ const newElement = createElementNS(namespace, tagName);
395
+ delete attr.tagName;
396
+ ObjectAssign(newElement, attr);
397
+ return newElement;
398
+ }
399
+
400
+ /**
401
+ * Shortcut for the `Element.dispatchEvent(Event)` method.
402
+ *
403
+ * @param {HTMLElement | Element} element is the target
404
+ * @param {Event} event is the `Event` object
405
+ */
406
+ const dispatchEvent = (element, event) => element.dispatchEvent(event);
407
+
408
+ /** @type {Map<string, Map<HTMLElement | Element, Record<string, any>>>} */
409
+ const componentData = new Map();
410
+ /**
411
+ * An interface for web components background data.
412
+ * @see https://github.com/thednp/bootstrap.native/blob/master/src/components/base-component.js
413
+ */
414
+ const Data = {
415
+ /**
416
+ * Sets web components data.
417
+ * @param {HTMLElement | Element | string} target target element
418
+ * @param {string} component the component's name or a unique key
419
+ * @param {Record<string, any>} instance the component instance
420
+ */
421
+ set: (target, component, instance) => {
422
+ const element = querySelector(target);
423
+ if (!element) return;
424
+
425
+ if (!componentData.has(component)) {
426
+ componentData.set(component, new Map());
427
+ }
428
+
429
+ const instanceMap = componentData.get(component);
430
+ // @ts-ignore - not undefined, but defined right above
431
+ instanceMap.set(element, instance);
432
+ },
433
+
434
+ /**
435
+ * Returns all instances for specified component.
436
+ * @param {string} component the component's name or a unique key
437
+ * @returns {Map<HTMLElement | Element, Record<string, any>>?} all the component instances
438
+ */
439
+ getAllFor: (component) => {
440
+ const instanceMap = componentData.get(component);
441
+
442
+ return instanceMap || null;
443
+ },
444
+
445
+ /**
446
+ * Returns the instance associated with the target.
447
+ * @param {HTMLElement | Element | string} target target element
448
+ * @param {string} component the component's name or a unique key
449
+ * @returns {Record<string, any>?} the instance
450
+ */
451
+ get: (target, component) => {
452
+ const element = querySelector(target);
453
+ const allForC = Data.getAllFor(component);
454
+ const instance = element && allForC && allForC.get(element);
455
+
456
+ return instance || null;
457
+ },
458
+
459
+ /**
460
+ * Removes web components data.
461
+ * @param {HTMLElement | Element | string} target target element
462
+ * @param {string} component the component's name or a unique key
463
+ */
464
+ remove: (target, component) => {
465
+ const element = querySelector(target);
466
+ const instanceMap = componentData.get(component);
467
+ if (!instanceMap || !element) return;
468
+
469
+ instanceMap.delete(element);
470
+
471
+ if (instanceMap.size === 0) {
472
+ componentData.delete(component);
473
+ }
474
+ },
475
+ };
476
+
477
+ /**
478
+ * An alias for `Data.get()`.
479
+ * @type {SHORTER.getInstance<any>}
480
+ */
481
+ const getInstance = (target, component) => Data.get(target, component);
482
+
483
+ /**
484
+ * Check class in `HTMLElement.classList`.
485
+ *
486
+ * @param {HTMLElement | Element} element target
487
+ * @param {string} classNAME to check
488
+ * @returns {boolean}
489
+ */
490
+ function hasClass(element, classNAME) {
491
+ return element.classList.contains(classNAME);
492
+ }
493
+
494
+ /**
495
+ * Add class to `HTMLElement.classList`.
496
+ *
497
+ * @param {HTMLElement | Element} element target
498
+ * @param {string} classNAME to add
499
+ * @returns {void}
500
+ */
501
+ function addClass(element, classNAME) {
502
+ element.classList.add(classNAME);
503
+ }
504
+
505
+ /**
506
+ * Remove class from `HTMLElement.classList`.
507
+ *
508
+ * @param {HTMLElement | Element} element target
509
+ * @param {string} classNAME to remove
510
+ * @returns {void}
511
+ */
512
+ function removeClass(element, classNAME) {
513
+ element.classList.remove(classNAME);
514
+ }
515
+
516
+ /**
517
+ * Shortcut for `HTMLElement.hasAttribute()` method.
518
+ * @param {HTMLElement | Element} element target element
519
+ * @param {string} attribute attribute name
520
+ * @returns {boolean} the query result
521
+ */
522
+ const hasAttribute = (element, attribute) => element.hasAttribute(attribute);
523
+
524
+ /**
525
+ * Shortcut for `HTMLElement.setAttribute()` method.
526
+ * @param {HTMLElement | Element} element target element
527
+ * @param {string} attribute attribute name
528
+ * @param {string} value attribute value
529
+ * @returns {void}
530
+ */
531
+ const setAttribute = (element, attribute, value) => element.setAttribute(attribute, value);
532
+
533
+ /**
534
+ * Shortcut for `HTMLElement.getAttribute()` method.
535
+ * @param {HTMLElement | Element} element target element
536
+ * @param {string} attribute attribute name
537
+ * @returns {string?} attribute value
538
+ */
539
+ const getAttribute = (element, attribute) => element.getAttribute(attribute);
540
+
541
+ /**
542
+ * Shortcut for `HTMLElement.removeAttribute()` method.
543
+ * @param {HTMLElement | Element} element target element
544
+ * @param {string} attribute attribute name
545
+ * @returns {void}
546
+ */
547
+ const removeAttribute = (element, attribute) => element.removeAttribute(attribute);
548
+
549
+ /**
550
+ * Shortcut for `String.toUpperCase()`.
551
+ *
552
+ * @param {string} source input string
553
+ * @returns {string} uppercase output string
554
+ */
555
+ const toUpperCase = (source) => source.toUpperCase();
556
+
557
+ const vHidden = 'v-hidden';
558
+
559
+ /**
560
+ * Returns the color form.
561
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
562
+ * @returns {HTMLElement | Element}
563
+ */
564
+ function getColorForm(self) {
565
+ const { format, id } = self;
566
+ const colorForm = createElement({
567
+ tagName: 'div',
568
+ className: `color-form ${format}`,
569
+ });
570
+
571
+ let components = ['hex'];
572
+ if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
573
+ else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
574
+
575
+ components.forEach((c) => {
576
+ const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
577
+ const cID = `color_${format}_${c}_${id}`;
578
+ const cInputLabel = createElement({ tagName: 'label' });
579
+ setAttribute(cInputLabel, 'for', cID);
580
+ cInputLabel.append(
581
+ createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
582
+ createElement({ tagName: 'span', className: vHidden, innerText: `${c}` }),
583
+ );
584
+ const cInput = createElement({
585
+ tagName: 'input',
586
+ id: cID, // name: cID,
587
+ type: format === 'hex' ? 'text' : 'number',
588
+ value: c === 'alpha' ? '1' : '0',
589
+ className: `color-input ${c}`,
590
+ autocomplete: 'off',
591
+ spellcheck: 'false',
592
+ });
593
+ if (format !== 'hex') {
594
+ // alpha
595
+ let max = '1';
596
+ let step = '0.01';
597
+ if (c !== 'alpha') {
598
+ if (format === 'rgb') { max = '255'; step = '1'; } else if (c === 'hue') { max = '360'; step = '1'; } else { max = '100'; step = '1'; }
599
+ }
600
+ ObjectAssign(cInput, {
601
+ min: '0',
602
+ max,
603
+ step,
604
+ });
605
+ }
606
+ colorForm.append(cInputLabel, cInput);
607
+ });
608
+ return colorForm;
609
+ }
610
+
611
+ /**
612
+ * Returns a new color control `HTMLElement`.
613
+ * @param {number} iteration
614
+ * @param {number} id
615
+ * @param {number} width
616
+ * @param {number} height
617
+ * @param {string=} labelledby
618
+ * @returns {HTMLElement | Element}
619
+ */
620
+ function getColorControl(iteration, id, width, height, labelledby) {
621
+ const labelID = `appearance${iteration}_${id}`;
622
+ const knobClass = iteration === 1 ? 'color-pointer' : 'color-slider';
623
+ const control = createElement({
624
+ tagName: 'div',
625
+ className: 'color-control',
626
+ });
627
+ setAttribute(control, 'role', 'presentation');
628
+
629
+ control.append(
630
+ createElement({
631
+ id: labelID,
632
+ tagName: 'label',
633
+ className: `color-label ${vHidden}`,
634
+ ariaLive: 'polite',
635
+ }),
636
+ createElement({
637
+ tagName: 'canvas',
638
+ className: `visual-control${iteration}`,
639
+ ariaHidden: 'true',
640
+ width: `${width}`,
641
+ height: `${height}`,
642
+ }),
643
+ );
644
+
645
+ const knob = createElement({
646
+ tagName: 'div',
647
+ className: `${knobClass} knob`,
648
+ });
649
+ setAttribute(knob, ariaLabelledBy, labelledby || labelID);
650
+ setAttribute(knob, 'tabindex', '0');
651
+ control.append(knob);
652
+ return control;
653
+ }
654
+
655
+ /**
656
+ * Returns the `document.head` or the `<head>` element.
657
+ *
658
+ * @param {(Node | HTMLElement | Element | globalThis)=} node
659
+ * @returns {HTMLElement | HTMLHeadElement}
660
+ */
661
+ function getDocumentHead(node) {
662
+ return getDocument(node).head;
663
+ }
664
+
665
+ /**
666
+ * Shortcut for `window.getComputedStyle(element).propertyName`
667
+ * static method.
668
+ *
669
+ * * If `element` parameter is not an `HTMLElement`, `getComputedStyle`
670
+ * throws a `ReferenceError`.
671
+ *
672
+ * @param {HTMLElement | Element} element target
673
+ * @param {string} property the css property
674
+ * @return {string} the css property value
675
+ */
676
+ function getElementStyle(element, property) {
677
+ const computedStyle = getComputedStyle(element);
678
+
679
+ // @ts-ignore -- must use camelcase strings,
680
+ // or non-camelcase strings with `getPropertyValue`
681
+ return property in computedStyle ? computedStyle[property] : '';
682
+ }
683
+
684
+ /**
685
+ * Shortcut for multiple uses of `HTMLElement.style.propertyName` method.
686
+ * @param {HTMLElement | Element} element target element
687
+ * @param {Partial<CSSStyleDeclaration>} styles attribute value
688
+ */
689
+ // @ts-ignore
690
+ const setElementStyle = (element, styles) => { ObjectAssign(element.style, styles); };
691
+
692
+ /**
693
+ * A complete list of web safe colors.
694
+ * @see https://github.com/bahamas10/css-color-names/blob/master/css-color-names.json
695
+ * @type {string[]}
696
+ */
697
+ const colorNames = [
698
+ 'aliceblue',
699
+ 'antiquewhite',
700
+ 'aqua',
701
+ 'aquamarine',
702
+ 'azure',
703
+ 'beige',
704
+ 'bisque',
705
+ 'black',
706
+ 'blanchedalmond',
707
+ 'blue',
708
+ 'blueviolet',
709
+ 'brown',
710
+ 'burlywood',
711
+ 'cadetblue',
712
+ 'chartreuse',
713
+ 'chocolate',
714
+ 'coral',
715
+ 'cornflowerblue',
716
+ 'cornsilk',
717
+ 'crimson',
718
+ 'cyan',
719
+ 'darkblue',
720
+ 'darkcyan',
721
+ 'darkgoldenrod',
722
+ 'darkgray',
723
+ 'darkgreen',
724
+ 'darkgrey',
725
+ 'darkkhaki',
726
+ 'darkmagenta',
727
+ 'darkolivegreen',
728
+ 'darkorange',
729
+ 'darkorchid',
730
+ 'darkred',
731
+ 'darksalmon',
732
+ 'darkseagreen',
733
+ 'darkslateblue',
734
+ 'darkslategray',
735
+ 'darkslategrey',
736
+ 'darkturquoise',
737
+ 'darkviolet',
738
+ 'deeppink',
739
+ 'deepskyblue',
740
+ 'dimgray',
741
+ 'dimgrey',
742
+ 'dodgerblue',
743
+ 'firebrick',
744
+ 'floralwhite',
745
+ 'forestgreen',
746
+ 'fuchsia',
747
+ 'gainsboro',
748
+ 'ghostwhite',
749
+ 'goldenrod',
750
+ 'gold',
751
+ 'gray',
752
+ 'green',
753
+ 'greenyellow',
754
+ 'grey',
755
+ 'honeydew',
756
+ 'hotpink',
757
+ 'indianred',
758
+ 'indigo',
759
+ 'ivory',
760
+ 'khaki',
761
+ 'lavenderblush',
762
+ 'lavender',
763
+ 'lawngreen',
764
+ 'lemonchiffon',
765
+ 'lightblue',
766
+ 'lightcoral',
767
+ 'lightcyan',
768
+ 'lightgoldenrodyellow',
769
+ 'lightgray',
770
+ 'lightgreen',
771
+ 'lightgrey',
772
+ 'lightpink',
773
+ 'lightsalmon',
774
+ 'lightseagreen',
775
+ 'lightskyblue',
776
+ 'lightslategray',
777
+ 'lightslategrey',
778
+ 'lightsteelblue',
779
+ 'lightyellow',
780
+ 'lime',
781
+ 'limegreen',
782
+ 'linen',
783
+ 'magenta',
784
+ 'maroon',
785
+ 'mediumaquamarine',
786
+ 'mediumblue',
787
+ 'mediumorchid',
788
+ 'mediumpurple',
789
+ 'mediumseagreen',
790
+ 'mediumslateblue',
791
+ 'mediumspringgreen',
792
+ 'mediumturquoise',
793
+ 'mediumvioletred',
794
+ 'midnightblue',
795
+ 'mintcream',
796
+ 'mistyrose',
797
+ 'moccasin',
798
+ 'navajowhite',
799
+ 'navy',
800
+ 'oldlace',
801
+ 'olive',
802
+ 'olivedrab',
803
+ 'orange',
804
+ 'orangered',
805
+ 'orchid',
806
+ 'palegoldenrod',
807
+ 'palegreen',
808
+ 'paleturquoise',
809
+ 'palevioletred',
810
+ 'papayawhip',
811
+ 'peachpuff',
812
+ 'peru',
813
+ 'pink',
814
+ 'plum',
815
+ 'powderblue',
816
+ 'purple',
817
+ 'rebeccapurple',
818
+ 'red',
819
+ 'rosybrown',
820
+ 'royalblue',
821
+ 'saddlebrown',
822
+ 'salmon',
823
+ 'sandybrown',
824
+ 'seagreen',
825
+ 'seashell',
826
+ 'sienna',
827
+ 'silver',
828
+ 'skyblue',
829
+ 'slateblue',
830
+ 'slategray',
831
+ 'slategrey',
832
+ 'snow',
833
+ 'springgreen',
834
+ 'steelblue',
835
+ 'tan',
836
+ 'teal',
837
+ 'thistle',
838
+ 'tomato',
839
+ 'turquoise',
840
+ 'violet',
841
+ 'wheat',
842
+ 'white',
843
+ 'whitesmoke',
844
+ 'yellow',
845
+ 'yellowgreen',
846
+ ];
847
+
848
+ // <http://www.w3.org/TR/css3-values/#integers>
849
+ const CSS_INTEGER = '[-\\+]?\\d+%?';
850
+
851
+ // <http://www.w3.org/TR/css3-values/#number-value>
852
+ const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
853
+
854
+ // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
855
+ const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
856
+
857
+ // Actual matching.
858
+ // Parentheses and commas are optional, but not required.
859
+ // Whitespace can take the place of commas or opening paren
860
+ const PERMISSIVE_MATCH3 = `[\\s|\\(]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})\\s*\\)?`;
861
+ const PERMISSIVE_MATCH4 = `[\\s|\\(]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})\\s*\\)?`;
862
+
863
+ const matchers = {
864
+ CSS_UNIT: new RegExp(CSS_UNIT),
865
+ rgb: new RegExp(`rgb${PERMISSIVE_MATCH3}`),
866
+ rgba: new RegExp(`rgba${PERMISSIVE_MATCH4}`),
867
+ hsl: new RegExp(`hsl${PERMISSIVE_MATCH3}`),
868
+ hsla: new RegExp(`hsla${PERMISSIVE_MATCH4}`),
869
+ hsv: new RegExp(`hsv${PERMISSIVE_MATCH3}`),
870
+ hsva: new RegExp(`hsva${PERMISSIVE_MATCH4}`),
871
+ hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
872
+ hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
873
+ hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
874
+ hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
875
+ };
876
+
877
+ /**
878
+ * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
879
+ * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
880
+ * @param {string} n
881
+ * @returns {boolean}
882
+ */
883
+ function isOnePointZero(n) {
884
+ return typeof n === 'string' && n.includes('.') && parseFloat(n) === 1;
885
+ }
886
+
887
+ /**
888
+ * Check to see if string passed in is a percentage
889
+ * @param {string} n
890
+ * @returns {boolean}
891
+ */
892
+ function isPercentage(n) {
893
+ return typeof n === 'string' && n.includes('%');
894
+ }
895
+
896
+ /**
897
+ * Check to see if it looks like a CSS unit
898
+ * (see `matchers` above for definition).
899
+ * @param {string | number} color
900
+ * @returns {boolean}
901
+ */
902
+ function isValidCSSUnit(color) {
903
+ return Boolean(matchers.CSS_UNIT.exec(String(color)));
904
+ }
905
+
906
+ /**
907
+ * Take input from [0, n] and return it as [0, 1]
908
+ * @param {*} n
909
+ * @param {number} max
910
+ * @returns {number}
911
+ */
912
+ function bound01(n, max) {
913
+ let N = n;
914
+ if (isOnePointZero(n)) N = '100%';
915
+
916
+ N = max === 360 ? N : Math.min(max, Math.max(0, parseFloat(N)));
917
+
918
+ // Automatically convert percentage into number
919
+ if (isPercentage(N)) {
920
+ N = parseInt(String(N * max), 10) / 100;
921
+ }
922
+ // Handle floating point rounding errors
923
+ if (Math.abs(N - max) < 0.000001) {
924
+ return 1;
925
+ }
926
+ // Convert into [0, 1] range if it isn't already
927
+ if (max === 360) {
928
+ // If n is a hue given in degrees,
929
+ // wrap around out-of-range values into [0, 360] range
930
+ // then convert into [0, 1].
931
+ N = (N < 0 ? (N % max) + max : N % max) / parseFloat(String(max));
932
+ } else {
933
+ // If n not a hue given in degrees
934
+ // Convert into [0, 1] range if it isn't already.
935
+ N = (N % max) / parseFloat(String(max));
936
+ }
937
+ return N;
938
+ }
939
+
940
+ /**
941
+ * Return a valid alpha value [0,1] with all invalid values being set to 1.
942
+ * @param {string | number} a
943
+ * @returns {number}
944
+ */
945
+ function boundAlpha(a) {
946
+ // @ts-ignore
947
+ let na = parseFloat(a);
948
+
949
+ if (Number.isNaN(na) || na < 0 || na > 1) {
950
+ na = 1;
951
+ }
952
+
953
+ return na;
954
+ }
955
+
956
+ /**
957
+ * Force a number between 0 and 1
958
+ * @param {number} val
959
+ * @returns {number}
960
+ */
961
+ function clamp01(val) {
962
+ return Math.min(1, Math.max(0, val));
963
+ }
964
+
965
+ /**
966
+ * Returns the hexadecimal value of a web safe colour.
967
+ * @param {string} name
968
+ * @returns {string}
969
+ */
970
+ function getHexFromColorName(name) {
971
+ const documentHead = getDocumentHead();
972
+ setElementStyle(documentHead, { color: name });
973
+ const colorName = getElementStyle(documentHead, 'color');
974
+ setElementStyle(documentHead, { color: '' });
975
+ return colorName;
976
+ }
977
+
978
+ /**
979
+ * Replace a decimal with it's percentage value
980
+ * @param {number | string} n
981
+ * @return {string | number}
982
+ */
983
+ function convertToPercentage(n) {
984
+ if (n <= 1) {
985
+ return `${Number(n) * 100}%`;
986
+ }
987
+ return n;
988
+ }
989
+
990
+ /**
991
+ * Force a hex value to have 2 characters
992
+ * @param {string} c
993
+ * @returns {string}
994
+ */
995
+ function pad2(c) {
996
+ return c.length === 1 ? `0${c}` : String(c);
997
+ }
998
+
999
+ // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
1000
+ // <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
1001
+ /**
1002
+ * Handle bounds / percentage checking to conform to CSS color spec
1003
+ * * *Assumes:* r, g, b in [0, 255] or [0, 1]
1004
+ * * *Returns:* { r, g, b } in [0, 255]
1005
+ * @see http://www.w3.org/TR/css3-color/
1006
+ * @param {number | string} r
1007
+ * @param {number | string} g
1008
+ * @param {number | string} b
1009
+ * @returns {CP.RGB}
1010
+ */
1011
+ function rgbToRgb(r, g, b) {
1012
+ return {
1013
+ r: bound01(r, 255) * 255,
1014
+ g: bound01(g, 255) * 255,
1015
+ b: bound01(b, 255) * 255,
1016
+ };
1017
+ }
1018
+
1019
+ /**
1020
+ * Converts an RGB color value to HSL.
1021
+ * *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
1022
+ * *Returns:* { h, s, l } in [0,1]
1023
+ * @param {number} R
1024
+ * @param {number} G
1025
+ * @param {number} B
1026
+ * @returns {CP.HSL}
1027
+ */
1028
+ function rgbToHsl(R, G, B) {
1029
+ const r = bound01(R, 255);
1030
+ const g = bound01(G, 255);
1031
+ const b = bound01(B, 255);
1032
+ const max = Math.max(r, g, b);
1033
+ const min = Math.min(r, g, b);
1034
+ let h = 0;
1035
+ let s = 0;
1036
+ const l = (max + min) / 2;
1037
+ if (max === min) {
1038
+ s = 0;
1039
+ h = 0; // achromatic
1040
+ } else {
1041
+ const d = max - min;
1042
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
1043
+ switch (max) {
1044
+ case r:
1045
+ h = (g - b) / d + (g < b ? 6 : 0);
1046
+ break;
1047
+ case g:
1048
+ h = (b - r) / d + 2;
1049
+ break;
1050
+ case b:
1051
+ h = (r - g) / d + 4;
1052
+ break;
1053
+ }
1054
+ h /= 6;
1055
+ }
1056
+ return { h, s, l };
1057
+ }
1058
+
1059
+ /**
1060
+ * Returns a normalized RGB component value.
1061
+ * @param {number} P
1062
+ * @param {number} Q
1063
+ * @param {number} T
1064
+ * @returns {number}
1065
+ */
1066
+ function hue2rgb(P, Q, T) {
1067
+ const p = P;
1068
+ const q = Q;
1069
+ let t = T;
1070
+ if (t < 0) {
1071
+ t += 1;
1072
+ }
1073
+ if (t > 1) {
1074
+ t -= 1;
1075
+ }
1076
+ if (t < 1 / 6) {
1077
+ return p + (q - p) * (6 * t);
1078
+ }
1079
+ if (t < 1 / 2) {
1080
+ return q;
1081
+ }
1082
+ if (t < 2 / 3) {
1083
+ return p + (q - p) * (2 / 3 - t) * 6;
1084
+ }
1085
+ return p;
1086
+ }
1087
+
1088
+ /**
1089
+ * Converts an HSL colour value to RGB.
1090
+ *
1091
+ * * *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
1092
+ * * *Returns:* { r, g, b } in the set [0, 255]
1093
+ * @param {number | string} H
1094
+ * @param {number | string} S
1095
+ * @param {number | string} L
1096
+ * @returns {CP.RGB}
1097
+ */
1098
+ function hslToRgb(H, S, L) {
1099
+ let r = 0;
1100
+ let g = 0;
1101
+ let b = 0;
1102
+ const h = bound01(H, 360);
1103
+ const s = bound01(S, 100);
1104
+ const l = bound01(L, 100);
1105
+
1106
+ if (s === 0) {
1107
+ // achromatic
1108
+ g = l;
1109
+ b = l;
1110
+ r = l;
1111
+ } else {
1112
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1113
+ const p = 2 * l - q;
1114
+ r = hue2rgb(p, q, h + 1 / 3);
1115
+ g = hue2rgb(p, q, h);
1116
+ b = hue2rgb(p, q, h - 1 / 3);
1117
+ }
1118
+ return { r: r * 255, g: g * 255, b: b * 255 };
1119
+ }
1120
+
1121
+ /**
1122
+ * Converts an RGB colour value to HSV.
1123
+ *
1124
+ * *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
1125
+ * *Returns:* { h, s, v } in [0,1]
1126
+ * @param {number | string} R
1127
+ * @param {number | string} G
1128
+ * @param {number | string} B
1129
+ * @returns {CP.HSV}
1130
+ */
1131
+ function rgbToHsv(R, G, B) {
1132
+ const r = bound01(R, 255);
1133
+ const g = bound01(G, 255);
1134
+ const b = bound01(B, 255);
1135
+ const max = Math.max(r, g, b);
1136
+ const min = Math.min(r, g, b);
1137
+ let h = 0;
1138
+ const v = max;
1139
+ const d = max - min;
1140
+ const s = max === 0 ? 0 : d / max;
1141
+ if (max === min) {
1142
+ h = 0; // achromatic
1143
+ } else {
1144
+ switch (max) {
1145
+ case r:
1146
+ h = (g - b) / d + (g < b ? 6 : 0);
1147
+ break;
1148
+ case g:
1149
+ h = (b - r) / d + 2;
1150
+ break;
1151
+ case b:
1152
+ h = (r - g) / d + 4;
1153
+ break;
1154
+ }
1155
+ h /= 6;
1156
+ }
1157
+ return { h, s, v };
1158
+ }
1159
+
1160
+ /**
1161
+ * Converts an HSV color value to RGB.
1162
+ *
1163
+ * *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
1164
+ * *Returns:* { r, g, b } in the set [0, 255]
1165
+ * @param {number | string} H
1166
+ * @param {number | string} S
1167
+ * @param {number | string} V
1168
+ * @returns {CP.RGB}
1169
+ */
1170
+ function hsvToRgb(H, S, V) {
1171
+ const h = bound01(H, 360) * 6;
1172
+ const s = bound01(S, 100);
1173
+ const v = bound01(V, 100);
1174
+ const i = Math.floor(h);
1175
+ const f = h - i;
1176
+ const p = v * (1 - s);
1177
+ const q = v * (1 - f * s);
1178
+ const t = v * (1 - (1 - f) * s);
1179
+ const mod = i % 6;
1180
+ const r = [v, q, p, p, t, v][mod];
1181
+ const g = [t, v, v, q, p, p][mod];
1182
+ const b = [p, p, t, v, v, q][mod];
1183
+ return { r: r * 255, g: g * 255, b: b * 255 };
1184
+ }
1185
+
1186
+ /**
1187
+ * Converts an RGB color to hex
1188
+ *
1189
+ * Assumes r, g, and b are contained in the set [0, 255]
1190
+ * Returns a 3 or 6 character hex
1191
+ * @param {number} r
1192
+ * @param {number} g
1193
+ * @param {number} b
1194
+ * @returns {string}
1195
+ */
1196
+ function rgbToHex(r, g, b) {
1197
+ const hex = [
1198
+ pad2(Math.round(r).toString(16)),
1199
+ pad2(Math.round(g).toString(16)),
1200
+ pad2(Math.round(b).toString(16)),
1201
+ ];
1202
+
1203
+ return hex.join('');
1204
+ }
1205
+
1206
+ /**
1207
+ * Converts a hex value to a decimal.
1208
+ * @param {string} h
1209
+ * @returns {number}
1210
+ */
1211
+ function convertHexToDecimal(h) {
1212
+ return parseIntFromHex(h) / 255;
1213
+ }
1214
+
1215
+ /**
1216
+ * Parse a base-16 hex value into a base-10 integer.
1217
+ * @param {string} val
1218
+ * @returns {number}
1219
+ */
1220
+ function parseIntFromHex(val) {
1221
+ return parseInt(val, 16);
1222
+ }
1223
+
1224
+ /**
1225
+ * Returns an `{r,g,b}` color object corresponding to a given number.
1226
+ * @param {number} color
1227
+ * @returns {CP.RGB}
1228
+ */
1229
+ function numberInputToObject(color) {
1230
+ /* eslint-disable no-bitwise */
1231
+ return {
1232
+ r: color >> 16,
1233
+ g: (color & 0xff00) >> 8,
1234
+ b: color & 0xff,
1235
+ };
1236
+ /* eslint-enable no-bitwise */
1237
+ }
1238
+
1239
+ /**
1240
+ * Permissive string parsing. Take in a number of formats, and output an object
1241
+ * based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
1242
+ * @param {string} input
1243
+ * @returns {Record<string, (number | string)> | false}
1244
+ */
1245
+ function stringInputToObject(input) {
1246
+ let color = input.trim().toLowerCase();
1247
+ if (color.length === 0) {
1248
+ return {
1249
+ r: 0, g: 0, b: 0, a: 0,
1250
+ };
1251
+ }
1252
+ let named = false;
1253
+ if (colorNames.includes(color)) {
1254
+ color = getHexFromColorName(color);
1255
+ named = true;
1256
+ } else if (color === 'transparent') {
1257
+ return {
1258
+ r: 0, g: 0, b: 0, a: 0, format: 'name',
1259
+ };
1260
+ }
1261
+
1262
+ // Try to match string input using regular expressions.
1263
+ // Keep most of the number bounding out of this function,
1264
+ // don't worry about [0,1] or [0,100] or [0,360]
1265
+ // Just return an object and let the conversion functions handle that.
1266
+ // This way the result will be the same whether Color is initialized with string or object.
1267
+ let match = matchers.rgb.exec(color);
1268
+ if (match) {
1269
+ return { r: match[1], g: match[2], b: match[3] };
1270
+ }
1271
+ match = matchers.rgba.exec(color);
1272
+ if (match) {
1273
+ return {
1274
+ r: match[1], g: match[2], b: match[3], a: match[4],
1275
+ };
1276
+ }
1277
+ match = matchers.hsl.exec(color);
1278
+ if (match) {
1279
+ return { h: match[1], s: match[2], l: match[3] };
1280
+ }
1281
+ match = matchers.hsla.exec(color);
1282
+ if (match) {
1283
+ return {
1284
+ h: match[1], s: match[2], l: match[3], a: match[4],
1285
+ };
1286
+ }
1287
+ match = matchers.hsv.exec(color);
1288
+ if (match) {
1289
+ return { h: match[1], s: match[2], v: match[3] };
1290
+ }
1291
+ match = matchers.hsva.exec(color);
1292
+ if (match) {
1293
+ return {
1294
+ h: match[1], s: match[2], v: match[3], a: match[4],
1295
+ };
1296
+ }
1297
+ match = matchers.hex8.exec(color);
1298
+ if (match) {
1299
+ return {
1300
+ r: parseIntFromHex(match[1]),
1301
+ g: parseIntFromHex(match[2]),
1302
+ b: parseIntFromHex(match[3]),
1303
+ a: convertHexToDecimal(match[4]),
1304
+ format: named ? 'name' : 'hex8',
1305
+ };
1306
+ }
1307
+ match = matchers.hex6.exec(color);
1308
+ if (match) {
1309
+ return {
1310
+ r: parseIntFromHex(match[1]),
1311
+ g: parseIntFromHex(match[2]),
1312
+ b: parseIntFromHex(match[3]),
1313
+ format: named ? 'name' : 'hex',
1314
+ };
1315
+ }
1316
+ match = matchers.hex4.exec(color);
1317
+ if (match) {
1318
+ return {
1319
+ r: parseIntFromHex(match[1] + match[1]),
1320
+ g: parseIntFromHex(match[2] + match[2]),
1321
+ b: parseIntFromHex(match[3] + match[3]),
1322
+ a: convertHexToDecimal(match[4] + match[4]),
1323
+ format: named ? 'name' : 'hex8',
1324
+ };
1325
+ }
1326
+ match = matchers.hex3.exec(color);
1327
+ if (match) {
1328
+ return {
1329
+ r: parseIntFromHex(match[1] + match[1]),
1330
+ g: parseIntFromHex(match[2] + match[2]),
1331
+ b: parseIntFromHex(match[3] + match[3]),
1332
+ format: named ? 'name' : 'hex',
1333
+ };
1334
+ }
1335
+ return false;
1336
+ }
1337
+
1338
+ /**
1339
+ * Given a string or object, convert that input to RGB
1340
+ *
1341
+ * Possible string inputs:
1342
+ * ```
1343
+ * "red"
1344
+ * "#f00" or "f00"
1345
+ * "#ff0000" or "ff0000"
1346
+ * "#ff000000" or "ff000000"
1347
+ * "rgb 255 0 0" or "rgb (255, 0, 0)"
1348
+ * "rgb 1.0 0 0" or "rgb (1, 0, 0)"
1349
+ * "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
1350
+ * "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
1351
+ * "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
1352
+ * "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
1353
+ * "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
1354
+ * ```
1355
+ * @param {string | Record<string, any>} input
1356
+ * @returns {CP.ColorObject}
1357
+ */
1358
+ function inputToRGB(input) {
1359
+ /** @type {CP.RGB} */
1360
+ let rgb = { r: 0, g: 0, b: 0 };
1361
+ let color = input;
1362
+ let a;
1363
+ let s = null;
1364
+ let v = null;
1365
+ let l = null;
1366
+ let ok = false;
1367
+ let format = 'hex';
1368
+
1369
+ if (typeof input === 'string') {
1370
+ // @ts-ignore -- this now is converted to object
1371
+ color = stringInputToObject(input);
1372
+ if (color) ok = true;
1373
+ }
1374
+ if (typeof color === 'object') {
1375
+ if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
1376
+ rgb = rgbToRgb(color.r, color.g, color.b);
1377
+ ok = true;
1378
+ format = `${color.r}`.slice(-1) === '%' ? 'prgb' : 'rgb';
1379
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
1380
+ s = convertToPercentage(color.s);
1381
+ v = convertToPercentage(color.v);
1382
+ rgb = hsvToRgb(color.h, s, v);
1383
+ ok = true;
1384
+ format = 'hsv';
1385
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
1386
+ s = convertToPercentage(color.s);
1387
+ l = convertToPercentage(color.l);
1388
+ rgb = hslToRgb(color.h, s, l);
1389
+ ok = true;
1390
+ format = 'hsl';
1391
+ }
1392
+ if ('a' in color) a = color.a;
1393
+ }
1394
+
1395
+ return {
1396
+ ok, // @ts-ignore
1397
+ format: color.format || format,
1398
+ r: Math.min(255, Math.max(rgb.r, 0)),
1399
+ g: Math.min(255, Math.max(rgb.g, 0)),
1400
+ b: Math.min(255, Math.max(rgb.b, 0)),
1401
+ a: boundAlpha(a),
1402
+ };
1403
+ }
1404
+
1405
+ /** @type {CP.ColorOptions} */
1406
+ const colorPickerDefaults = {
1407
+ format: 'hex',
1408
+ };
1409
+
1410
+ /**
1411
+ * Returns a new `Color` instance.
1412
+ * @see https://github.com/bgrins/TinyColor
1413
+ * @class
1414
+ */
1415
+ class Color {
1416
+ /**
1417
+ * @constructor
1418
+ * @param {CP.ColorInput} input
1419
+ * @param {CP.ColorOptions=} config
1420
+ */
1421
+ constructor(input, config) {
1422
+ let color = input;
1423
+ const opts = typeof config === 'object'
1424
+ ? ObjectAssign(colorPickerDefaults, config)
1425
+ : ObjectAssign({}, colorPickerDefaults);
1426
+
1427
+ // If input is already a `Color`, return itself
1428
+ if (color instanceof Color) {
1429
+ color = inputToRGB(color);
1430
+ }
1431
+ if (typeof color === 'number') {
1432
+ color = numberInputToObject(color);
1433
+ }
1434
+ const {
1435
+ r, g, b, a, ok, format,
1436
+ } = inputToRGB(color);
1437
+
1438
+ /** @type {CP.ColorInput} */
1439
+ this.originalInput = color;
1440
+ /** @type {number} */
1441
+ this.r = r;
1442
+ /** @type {number} */
1443
+ this.g = g;
1444
+ /** @type {number} */
1445
+ this.b = b;
1446
+ /** @type {number} */
1447
+ this.a = a;
1448
+ /** @type {boolean} */
1449
+ this.ok = ok;
1450
+ /** @type {number} */
1451
+ this.roundA = Math.round(100 * this.a) / 100;
1452
+ /** @type {CP.ColorFormats} */
1453
+ this.format = opts.format || format;
1454
+
1455
+ // Don't let the range of [0,255] come back in [0,1].
1456
+ // Potentially lose a little bit of precision here, but will fix issues where
1457
+ // .5 gets interpreted as half of the total, instead of half of 1
1458
+ // If it was supposed to be 128, this was already taken care of by `inputToRgb`
1459
+ if (this.r < 1) {
1460
+ this.r = Math.round(this.r);
1461
+ }
1462
+ if (this.g < 1) {
1463
+ this.g = Math.round(this.g);
1464
+ }
1465
+ if (this.b < 1) {
1466
+ this.b = Math.round(this.b);
1467
+ }
1468
+ }
1469
+
1470
+ /**
1471
+ * Checks if the current input value is a valid colour.
1472
+ * @returns {boolean} the query result
1473
+ */
1474
+ get isValid() {
1475
+ return this.ok;
1476
+ }
1477
+
1478
+ /**
1479
+ * Checks if the current colour requires a light text colour.
1480
+ * @returns {boolean} the query result
1481
+ */
1482
+ get isDark() {
1483
+ return this.brightness < 128;
1484
+ }
1485
+
1486
+ /**
1487
+ * Returns the perceived luminance of a color.
1488
+ * @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
1489
+ * @returns {number} a number in [0-1] range
1490
+ */
1491
+ get luminance() {
1492
+ const { r, g, b } = this;
1493
+ let R = 0;
1494
+ let G = 0;
1495
+ let B = 0;
1496
+ const RsRGB = r / 255;
1497
+ const GsRGB = g / 255;
1498
+ const BsRGB = b / 255;
1499
+
1500
+ if (RsRGB <= 0.03928) {
1501
+ R = RsRGB / 12.92;
1502
+ } else {
1503
+ R = ((RsRGB + 0.055) / 1.055) ** 2.4;
1504
+ }
1505
+ if (GsRGB <= 0.03928) {
1506
+ G = GsRGB / 12.92;
1507
+ } else {
1508
+ G = ((GsRGB + 0.055) / 1.055) ** 2.4;
1509
+ }
1510
+ if (BsRGB <= 0.03928) {
1511
+ B = BsRGB / 12.92;
1512
+ } else {
1513
+ B = ((BsRGB + 0.055) / 1.055) ** 2.4;
1514
+ }
1515
+ return 0.2126 * R + 0.7152 * G + 0.0722 * B;
1516
+ }
1517
+
1518
+ /**
1519
+ * Returns the perceived brightness of the color.
1520
+ * @returns {number} a number in [0-255] range
1521
+ */
1522
+ get brightness() {
1523
+ const { r, g, b } = this;
1524
+ return (r * 299 + g * 587 + b * 114) / 1000;
1525
+ }
1526
+
1527
+ /**
1528
+ * Returns the color as a RGBA object.
1529
+ * @returns {CP.RGBA}
1530
+ */
1531
+ toRgb() {
1532
+ return {
1533
+ r: Math.round(this.r),
1534
+ g: Math.round(this.g),
1535
+ b: Math.round(this.b),
1536
+ a: this.a,
1537
+ };
1538
+ }
1539
+
1540
+ /**
1541
+ * Returns the RGBA values concatenated into a string.
1542
+ * @returns {string} the CSS valid color in RGB/RGBA format
1543
+ */
1544
+ toRgbString() {
1545
+ const r = Math.round(this.r);
1546
+ const g = Math.round(this.g);
1547
+ const b = Math.round(this.b);
1548
+ return this.a === 1
1549
+ ? `rgb(${r},${g},${b})`
1550
+ : `rgba(${r},${g},${b},${this.roundA})`;
1551
+ }
1552
+
1553
+ /**
1554
+ * Returns the HEX value of the color.
1555
+ * @returns {string} the hexadecimal color format
1556
+ */
1557
+ toHex() {
1558
+ return rgbToHex(this.r, this.g, this.b);
1559
+ }
1560
+
1561
+ /**
1562
+ * Returns the HEX value of the color.
1563
+ * @returns {string} the CSS valid color in hexadecimal format
1564
+ */
1565
+ toHexString() {
1566
+ return `#${this.toHex()}`;
1567
+ }
1568
+
1569
+ /**
1570
+ * Returns the color as a HSVA object.
1571
+ * @returns {CP.HSVA} the `{h,s,v,a}` object
1572
+ */
1573
+ toHsv() {
1574
+ const { h, s, v } = rgbToHsv(this.r, this.g, this.b);
1575
+ return {
1576
+ h: h * 360, s, v, a: this.a,
1577
+ };
1578
+ }
1579
+
1580
+ /**
1581
+ * Returns the color as a HSLA object.
1582
+ * @returns {CP.HSLA}
1583
+ */
1584
+ toHsl() {
1585
+ const { h, s, l } = rgbToHsl(this.r, this.g, this.b);
1586
+ return {
1587
+ h: h * 360, s, l, a: this.a,
1588
+ };
1589
+ }
1590
+
1591
+ /**
1592
+ * Returns the HSLA values concatenated into a string.
1593
+ * @returns {string} the CSS valid color in HSL/HSLA format
1594
+ */
1595
+ toHslString() {
1596
+ const hsl = this.toHsl();
1597
+ const h = Math.round(hsl.h);
1598
+ const s = Math.round(hsl.s * 100);
1599
+ const l = Math.round(hsl.l * 100);
1600
+ return this.a === 1
1601
+ ? `hsl(${h},${s}%,${l}%)`
1602
+ : `hsla(${h},${s}%,${l}%,${this.roundA})`;
1603
+ }
1604
+
1605
+ /**
1606
+ * Sets the alpha value on the current color.
1607
+ * @param {number} alpha a new alpha value in [0-1] range.
1608
+ * @returns {Color} a new `Color` instance
1609
+ */
1610
+ setAlpha(alpha) {
1611
+ this.a = boundAlpha(alpha);
1612
+ this.roundA = Math.round(100 * this.a) / 100;
1613
+ return this;
1614
+ }
1615
+
1616
+ /**
1617
+ * Saturate the color with a given amount.
1618
+ * @param {number=} amount a value in [0-100] range
1619
+ * @returns {Color} a new `Color` instance
1620
+ */
1621
+ saturate(amount) {
1622
+ if (!amount) return this;
1623
+ const hsl = this.toHsl();
1624
+ hsl.s += amount / 100;
1625
+ hsl.s = clamp01(hsl.s);
1626
+ return new Color(hsl);
1627
+ }
1628
+
1629
+ /**
1630
+ * Desaturate the color with a given amount.
1631
+ * @param {number=} amount a value in [0-100] range
1632
+ * @returns {Color} a new `Color` instance
1633
+ */
1634
+ desaturate(amount) {
1635
+ return amount ? this.saturate(-amount) : this;
1636
+ }
1637
+
1638
+ /**
1639
+ * Completely desaturates a color into greyscale.
1640
+ * Same as calling `desaturate(100)`
1641
+ * @returns {Color} a new `Color` instance
1642
+ */
1643
+ greyscale() {
1644
+ return this.desaturate(100);
1645
+ }
1646
+
1647
+ /** Returns a clone of the current `Color` instance. */
1648
+ clone() {
1649
+ return new Color(this);
1650
+ }
1651
+
1652
+ /**
1653
+ * Returns the color value in CSS valid string format.
1654
+ * @returns {string}
1655
+ */
1656
+ toString() {
1657
+ const { format } = this;
1658
+
1659
+ if (format === 'rgb') {
1660
+ return this.toRgbString();
1661
+ }
1662
+ if (format === 'hsl') {
1663
+ return this.toHslString();
1664
+ }
1665
+ return this.toHexString();
1666
+ }
1667
+ }
1668
+
1669
+ ObjectAssign(Color, {
1670
+ colorNames,
1671
+ CSS_INTEGER,
1672
+ CSS_NUMBER,
1673
+ CSS_UNIT,
1674
+ PERMISSIVE_MATCH3,
1675
+ PERMISSIVE_MATCH4,
1676
+ matchers,
1677
+ isOnePointZero,
1678
+ isPercentage,
1679
+ isValidCSSUnit,
1680
+ bound01,
1681
+ boundAlpha,
1682
+ clamp01,
1683
+ getHexFromColorName,
1684
+ convertToPercentage,
1685
+ convertHexToDecimal,
1686
+ pad2,
1687
+ rgbToRgb,
1688
+ rgbToHsl,
1689
+ rgbToHex,
1690
+ rgbToHsv,
1691
+ hslToRgb,
1692
+ hsvToRgb,
1693
+ hue2rgb,
1694
+ parseIntFromHex,
1695
+ numberInputToObject,
1696
+ stringInputToObject,
1697
+ inputToRGB,
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
+ function initCallBack() {
2997
+ const { init, selector } = ColorPicker;
2998
+ [...querySelectorAll(selector)].forEach(init);
2999
+ }
3000
+
3001
+ if (document.body) initCallBack();
3002
+ else document.addEventListener('DOMContentLoaded', initCallBack, { once: true });
3003
+
3004
+ return ColorPicker;
3005
+
3006
+ })));