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