@thednp/color-picker 0.0.1-alpha1 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +63 -26
  3. package/dist/css/color-picker.css +504 -337
  4. package/dist/css/color-picker.min.css +2 -0
  5. package/dist/css/color-picker.rtl.css +529 -0
  6. package/dist/css/color-picker.rtl.min.css +2 -0
  7. package/dist/js/color-picker-element-esm.js +3851 -2
  8. package/dist/js/color-picker-element-esm.min.js +2 -0
  9. package/dist/js/color-picker-element.js +2086 -1278
  10. package/dist/js/color-picker-element.min.js +2 -2
  11. package/dist/js/color-picker-esm.js +3742 -0
  12. package/dist/js/color-picker-esm.min.js +2 -0
  13. package/dist/js/color-picker.js +2030 -1286
  14. package/dist/js/color-picker.min.js +2 -2
  15. package/package.json +18 -9
  16. package/src/js/color-palette.js +71 -0
  17. package/src/js/color-picker-element.js +62 -16
  18. package/src/js/color-picker.js +734 -618
  19. package/src/js/color.js +621 -358
  20. package/src/js/index.js +0 -9
  21. package/src/js/util/colorNames.js +2 -152
  22. package/src/js/util/colorPickerLabels.js +22 -0
  23. package/src/js/util/getColorControls.js +103 -0
  24. package/src/js/util/getColorForm.js +26 -19
  25. package/src/js/util/getColorMenu.js +88 -0
  26. package/src/js/util/isValidJSON.js +13 -0
  27. package/src/js/util/nonColors.js +5 -0
  28. package/src/js/util/roundPart.js +9 -0
  29. package/src/js/util/setCSSProperties.js +12 -0
  30. package/src/js/util/tabindex.js +3 -0
  31. package/src/js/util/templates.js +1 -0
  32. package/src/scss/color-picker.rtl.scss +23 -0
  33. package/src/scss/color-picker.scss +449 -0
  34. package/types/cp.d.ts +263 -162
  35. package/types/index.d.ts +9 -2
  36. package/types/source/source.ts +2 -1
  37. package/types/source/types.d.ts +28 -5
  38. package/dist/js/color-picker.esm.js +0 -2998
  39. package/dist/js/color-picker.esm.min.js +0 -2
  40. package/src/js/util/getColorControl.js +0 -49
  41. package/src/js/util/init.js +0 -14
@@ -0,0 +1,3742 @@
1
+ /*!
2
+ * ColorPicker v0.0.1 (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
+ /** @type {Record<string, any>} */
7
+ const EventRegistry = {};
8
+
9
+ /**
10
+ * The global event listener.
11
+ *
12
+ * @this {Element | HTMLElement | Window | Document}
13
+ * @param {Event} e
14
+ * @returns {void}
15
+ */
16
+ function globalListener(e) {
17
+ const that = this;
18
+ const { type } = e;
19
+ const oneEvMap = EventRegistry[type] ? [...EventRegistry[type]] : [];
20
+
21
+ oneEvMap.forEach((elementsMap) => {
22
+ const [element, listenersMap] = elementsMap;
23
+ [...listenersMap].forEach((listenerMap) => {
24
+ if (element === that) {
25
+ const [listener, options] = listenerMap;
26
+ listener.apply(element, [e]);
27
+
28
+ if (options && options.once) {
29
+ removeListener(element, type, listener, options);
30
+ }
31
+ }
32
+ });
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Register a new listener with its options and attach the `globalListener`
38
+ * to the target if this is the first listener.
39
+ *
40
+ * @param {Element | HTMLElement | Window | Document} element
41
+ * @param {string} eventType
42
+ * @param {EventListenerObject['handleEvent']} listener
43
+ * @param {AddEventListenerOptions=} options
44
+ */
45
+ const addListener = (element, eventType, listener, options) => {
46
+ // get element listeners first
47
+ if (!EventRegistry[eventType]) {
48
+ EventRegistry[eventType] = new Map();
49
+ }
50
+ const oneEventMap = EventRegistry[eventType];
51
+
52
+ if (!oneEventMap.has(element)) {
53
+ oneEventMap.set(element, new Map());
54
+ }
55
+ const oneElementMap = oneEventMap.get(element);
56
+
57
+ // get listeners size
58
+ const { size } = oneElementMap;
59
+
60
+ // register listener with its options
61
+ if (oneElementMap) {
62
+ oneElementMap.set(listener, options);
63
+ }
64
+
65
+ // add listener last
66
+ if (!size) {
67
+ element.addEventListener(eventType, globalListener, options);
68
+ }
69
+ };
70
+
71
+ /**
72
+ * Remove a listener from registry and detach the `globalListener`
73
+ * if no listeners are found in the registry.
74
+ *
75
+ * @param {Element | HTMLElement | Window | Document} element
76
+ * @param {string} eventType
77
+ * @param {EventListenerObject['handleEvent']} listener
78
+ * @param {AddEventListenerOptions=} options
79
+ */
80
+ const removeListener = (element, eventType, listener, options) => {
81
+ // get listener first
82
+ const oneEventMap = EventRegistry[eventType];
83
+ const oneElementMap = oneEventMap && oneEventMap.get(element);
84
+ const savedOptions = oneElementMap && oneElementMap.get(listener);
85
+
86
+ // also recover initial options
87
+ const { options: eventOptions } = savedOptions !== undefined
88
+ ? savedOptions
89
+ : { options };
90
+
91
+ // unsubscribe second, remove from registry
92
+ if (oneElementMap && oneElementMap.has(listener)) oneElementMap.delete(listener);
93
+ if (oneEventMap && (!oneElementMap || !oneElementMap.size)) oneEventMap.delete(element);
94
+ if (!oneEventMap || !oneEventMap.size) delete EventRegistry[eventType];
95
+
96
+ // remove listener last
97
+ if (!oneElementMap || !oneElementMap.size) {
98
+ element.removeEventListener(eventType, globalListener, eventOptions);
99
+ }
100
+ };
101
+
102
+ /**
103
+ * A global namespace for aria-description.
104
+ * @type {string}
105
+ */
106
+ const ariaDescription = 'aria-description';
107
+
108
+ /**
109
+ * A global namespace for aria-selected.
110
+ * @type {string}
111
+ */
112
+ const ariaSelected = 'aria-selected';
113
+
114
+ /**
115
+ * A global namespace for aria-expanded.
116
+ * @type {string}
117
+ */
118
+ const ariaExpanded = 'aria-expanded';
119
+
120
+ /**
121
+ * A global namespace for aria-valuetext.
122
+ * @type {string}
123
+ */
124
+ const ariaValueText = 'aria-valuetext';
125
+
126
+ /**
127
+ * A global namespace for aria-valuenow.
128
+ * @type {string}
129
+ */
130
+ const ariaValueNow = 'aria-valuenow';
131
+
132
+ /**
133
+ * A global namespace for aria-haspopup.
134
+ * @type {string}
135
+ */
136
+ const ariaHasPopup = 'aria-haspopup';
137
+
138
+ /**
139
+ * A global namespace for aria-hidden.
140
+ * @type {string}
141
+ */
142
+ const ariaHidden = 'aria-hidden';
143
+
144
+ /**
145
+ * A global namespace for aria-labelledby.
146
+ * @type {string}
147
+ */
148
+ const ariaLabelledBy = 'aria-labelledby';
149
+
150
+ /**
151
+ * A global namespace for `ArrowDown` key.
152
+ * @type {string} e.which = 40 equivalent
153
+ */
154
+ const keyArrowDown = 'ArrowDown';
155
+
156
+ /**
157
+ * A global namespace for `ArrowUp` key.
158
+ * @type {string} e.which = 38 equivalent
159
+ */
160
+ const keyArrowUp = 'ArrowUp';
161
+
162
+ /**
163
+ * A global namespace for `ArrowLeft` key.
164
+ * @type {string} e.which = 37 equivalent
165
+ */
166
+ const keyArrowLeft = 'ArrowLeft';
167
+
168
+ /**
169
+ * A global namespace for `ArrowRight` key.
170
+ * @type {string} e.which = 39 equivalent
171
+ */
172
+ const keyArrowRight = 'ArrowRight';
173
+
174
+ /**
175
+ * A global namespace for `Enter` key.
176
+ * @type {string} e.which = 13 equivalent
177
+ */
178
+ const keyEnter = 'Enter';
179
+
180
+ /**
181
+ * A global namespace for `Space` key.
182
+ * @type {string} e.which = 32 equivalent
183
+ */
184
+ const keySpace = 'Space';
185
+
186
+ /**
187
+ * A global namespace for `Escape` key.
188
+ * @type {string} e.which = 27 equivalent
189
+ */
190
+ const keyEscape = 'Escape';
191
+
192
+ /**
193
+ * A global namespace for `focusin` event.
194
+ * @type {string}
195
+ */
196
+ const focusinEvent = 'focusin';
197
+
198
+ /**
199
+ * A global namespace for `click` event.
200
+ * @type {string}
201
+ */
202
+ const mouseclickEvent = 'click';
203
+
204
+ /**
205
+ * A global namespace for `keydown` event.
206
+ * @type {string}
207
+ */
208
+ const keydownEvent = 'keydown';
209
+
210
+ /**
211
+ * A global namespace for `change` event.
212
+ * @type {string}
213
+ */
214
+ const changeEvent = 'change';
215
+
216
+ /**
217
+ * A global namespace for `touchstart` event.
218
+ * @type {string}
219
+ */
220
+ const touchstartEvent = 'touchstart';
221
+
222
+ /**
223
+ * A global namespace for `touchmove` event.
224
+ * @type {string}
225
+ */
226
+ const touchmoveEvent = 'touchmove';
227
+
228
+ /**
229
+ * A global namespace for `touchend` event.
230
+ * @type {string}
231
+ */
232
+ const touchendEvent = 'touchend';
233
+
234
+ /**
235
+ * A global namespace for `mousedown` event.
236
+ * @type {string}
237
+ */
238
+ const mousedownEvent = 'mousedown';
239
+
240
+ /**
241
+ * A global namespace for `mousemove` event.
242
+ * @type {string}
243
+ */
244
+ const mousemoveEvent = 'mousemove';
245
+
246
+ /**
247
+ * A global namespace for `mouseup` event.
248
+ * @type {string}
249
+ */
250
+ const mouseupEvent = 'mouseup';
251
+
252
+ /**
253
+ * A global namespace for `scroll` event.
254
+ * @type {string}
255
+ */
256
+ const scrollEvent = 'scroll';
257
+
258
+ /**
259
+ * A global namespace for `keyup` event.
260
+ * @type {string}
261
+ */
262
+ const keyupEvent = 'keyup';
263
+
264
+ /**
265
+ * A global namespace for `resize` event.
266
+ * @type {string}
267
+ */
268
+ const resizeEvent = 'resize';
269
+
270
+ /**
271
+ * A global namespace for `focusout` event.
272
+ * @type {string}
273
+ */
274
+ const focusoutEvent = 'focusout';
275
+
276
+ // @ts-ignore
277
+ const { userAgentData: uaDATA } = navigator;
278
+
279
+ /**
280
+ * A global namespace for `userAgentData` object.
281
+ */
282
+ const userAgentData = uaDATA;
283
+
284
+ const { userAgent: userAgentString } = navigator;
285
+
286
+ /**
287
+ * A global namespace for `navigator.userAgent` string.
288
+ */
289
+ const userAgent = userAgentString;
290
+
291
+ const mobileBrands = /iPhone|iPad|iPod|Android/i;
292
+ let isMobileCheck = false;
293
+
294
+ if (userAgentData) {
295
+ isMobileCheck = userAgentData.brands
296
+ .some((/** @type {Record<String, any>} */x) => mobileBrands.test(x.brand));
297
+ } else {
298
+ isMobileCheck = mobileBrands.test(userAgent);
299
+ }
300
+
301
+ /**
302
+ * A global `boolean` for mobile detection.
303
+ * @type {boolean}
304
+ */
305
+ const isMobile = isMobileCheck;
306
+
307
+ /**
308
+ * Returns the `document` or the `#document` element.
309
+ * @see https://github.com/floating-ui/floating-ui
310
+ * @param {(Node | HTMLElement | Element | globalThis)=} node
311
+ * @returns {Document}
312
+ */
313
+ function getDocument(node) {
314
+ if (node instanceof HTMLElement) return node.ownerDocument;
315
+ if (node instanceof Window) return node.document;
316
+ return window.document;
317
+ }
318
+
319
+ /**
320
+ * Returns the `document.documentElement` or the `<html>` element.
321
+ *
322
+ * @param {(Node | HTMLElement | Element | globalThis)=} node
323
+ * @returns {HTMLElement | HTMLHtmlElement}
324
+ */
325
+ function getDocumentElement(node) {
326
+ return getDocument(node).documentElement;
327
+ }
328
+
329
+ /**
330
+ * Returns the `Window` object of a target node.
331
+ * @see https://github.com/floating-ui/floating-ui
332
+ *
333
+ * @param {(Node | HTMLElement | Element | Window)=} node target node
334
+ * @returns {globalThis}
335
+ */
336
+ function getWindow(node) {
337
+ if (node == null) {
338
+ return window;
339
+ }
340
+
341
+ if (!(node instanceof Window)) {
342
+ const { ownerDocument } = node;
343
+ return ownerDocument ? ownerDocument.defaultView || window : window;
344
+ }
345
+
346
+ // @ts-ignore
347
+ return node;
348
+ }
349
+
350
+ /**
351
+ * Shortcut for `window.getComputedStyle(element).propertyName`
352
+ * static method.
353
+ *
354
+ * * If `element` parameter is not an `HTMLElement`, `getComputedStyle`
355
+ * throws a `ReferenceError`.
356
+ *
357
+ * @param {HTMLElement | Element} element target
358
+ * @param {string} property the css property
359
+ * @return {string} the css property value
360
+ */
361
+ function getElementStyle(element, property) {
362
+ const computedStyle = getComputedStyle(element);
363
+
364
+ // @ts-ignore -- must use camelcase strings,
365
+ // or non-camelcase strings with `getPropertyValue`
366
+ return property in computedStyle ? computedStyle[property] : '';
367
+ }
368
+
369
+ let elementUID = 0;
370
+ let elementMapUID = 0;
371
+ const elementIDMap = new Map();
372
+
373
+ /**
374
+ * Returns a unique identifier for popover, tooltip, scrollspy.
375
+ *
376
+ * @param {HTMLElement | Element} element target element
377
+ * @param {string=} key predefined key
378
+ * @returns {number} an existing or new unique ID
379
+ */
380
+ function getUID(element, key) {
381
+ let result = key ? elementUID : elementMapUID;
382
+
383
+ if (key) {
384
+ const elID = getUID(element);
385
+ const elMap = elementIDMap.get(elID) || new Map();
386
+ if (!elementIDMap.has(elID)) {
387
+ elementIDMap.set(elID, elMap);
388
+ }
389
+ if (!elMap.has(key)) {
390
+ elMap.set(key, result);
391
+ elementUID += 1;
392
+ } else result = elMap.get(key);
393
+ } else {
394
+ const elkey = element.id || element;
395
+
396
+ if (!elementIDMap.has(elkey)) {
397
+ elementIDMap.set(elkey, result);
398
+ elementMapUID += 1;
399
+ } else result = elementIDMap.get(elkey);
400
+ }
401
+ return result;
402
+ }
403
+
404
+ /**
405
+ * Returns the bounding client rect of a target `HTMLElement`.
406
+ *
407
+ * @see https://github.com/floating-ui/floating-ui
408
+ *
409
+ * @param {HTMLElement | Element} element event.target
410
+ * @param {boolean=} includeScale when *true*, the target scale is also computed
411
+ * @returns {SHORTER.BoundingClientRect} the bounding client rect object
412
+ */
413
+ function getBoundingClientRect(element, includeScale) {
414
+ const {
415
+ width, height, top, right, bottom, left,
416
+ } = element.getBoundingClientRect();
417
+ let scaleX = 1;
418
+ let scaleY = 1;
419
+
420
+ if (includeScale && element instanceof HTMLElement) {
421
+ const { offsetWidth, offsetHeight } = element;
422
+ scaleX = offsetWidth > 0 ? Math.round(width) / offsetWidth || 1 : 1;
423
+ scaleY = offsetHeight > 0 ? Math.round(height) / offsetHeight || 1 : 1;
424
+ }
425
+
426
+ return {
427
+ width: width / scaleX,
428
+ height: height / scaleY,
429
+ top: top / scaleY,
430
+ right: right / scaleX,
431
+ bottom: bottom / scaleY,
432
+ left: left / scaleX,
433
+ x: left / scaleX,
434
+ y: top / scaleY,
435
+ };
436
+ }
437
+
438
+ /**
439
+ * A global namespace for 'transitionDuration' string.
440
+ * @type {string}
441
+ */
442
+ const transitionDuration = 'transitionDuration';
443
+
444
+ /**
445
+ * A global namespace for `transitionProperty` string for modern browsers.
446
+ *
447
+ * @type {string}
448
+ */
449
+ const transitionProperty = 'transitionProperty';
450
+
451
+ /**
452
+ * Utility to get the computed `transitionDuration`
453
+ * from Element in miliseconds.
454
+ *
455
+ * @param {HTMLElement | Element} element target
456
+ * @return {number} the value in miliseconds
457
+ */
458
+ function getElementTransitionDuration(element) {
459
+ const propertyValue = getElementStyle(element, transitionProperty);
460
+ const durationValue = getElementStyle(element, transitionDuration);
461
+ const durationScale = durationValue.includes('ms') ? 1 : 1000;
462
+ const duration = propertyValue && propertyValue !== 'none'
463
+ ? parseFloat(durationValue) * durationScale : 0;
464
+
465
+ return !Number.isNaN(duration) ? duration : 0;
466
+ }
467
+
468
+ /**
469
+ * A global array of possible `ParentNode`.
470
+ */
471
+ const parentNodes = [Document, Element, HTMLElement];
472
+
473
+ /**
474
+ * A global array with `Element` | `HTMLElement`.
475
+ */
476
+ const elementNodes = [Element, HTMLElement];
477
+
478
+ /**
479
+ * Utility to check if target is typeof `HTMLElement`, `Element`, `Node`
480
+ * or find one that matches a selector.
481
+ *
482
+ * @param {HTMLElement | Element | string} selector the input selector or target element
483
+ * @param {(HTMLElement | Element | Document)=} parent optional node to look into
484
+ * @return {(HTMLElement | Element)?} the `HTMLElement` or `querySelector` result
485
+ */
486
+ function querySelector(selector, parent) {
487
+ const lookUp = parentNodes.some((x) => parent instanceof x)
488
+ ? parent : getDocument();
489
+
490
+ // @ts-ignore
491
+ return elementNodes.some((x) => selector instanceof x)
492
+ // @ts-ignore
493
+ ? selector : lookUp.querySelector(selector);
494
+ }
495
+
496
+ /**
497
+ * Shortcut for `HTMLElement.closest` method which also works
498
+ * with children of `ShadowRoot`. The order of the parameters
499
+ * is intentional since they're both required.
500
+ *
501
+ * @see https://stackoverflow.com/q/54520554/803358
502
+ *
503
+ * @param {HTMLElement | Element} element Element to look into
504
+ * @param {string} selector the selector name
505
+ * @return {(HTMLElement | Element)?} the query result
506
+ */
507
+ function closest(element, selector) {
508
+ return element ? (element.closest(selector)
509
+ // @ts-ignore -- break out of `ShadowRoot`
510
+ || closest(element.getRootNode().host, selector)) : null;
511
+ }
512
+
513
+ /**
514
+ * Shortcut for `HTMLElement.getElementsByClassName` method. Some `Node` elements
515
+ * like `ShadowRoot` do not support `getElementsByClassName`.
516
+ *
517
+ * @param {string} selector the class name
518
+ * @param {(HTMLElement | Element | Document)=} parent optional Element to look into
519
+ * @return {HTMLCollectionOf<HTMLElement | Element>} the 'HTMLCollection'
520
+ */
521
+ function getElementsByClassName(selector, parent) {
522
+ const lookUp = parent && parentNodes.some((x) => parent instanceof x)
523
+ ? parent : getDocument();
524
+ return lookUp.getElementsByClassName(selector);
525
+ }
526
+
527
+ /**
528
+ * Shortcut for `Object.assign()` static method.
529
+ * @param {Record<string, any>} obj a target object
530
+ * @param {Record<string, any>} source a source object
531
+ */
532
+ const ObjectAssign = (obj, source) => Object.assign(obj, source);
533
+
534
+ /**
535
+ * This is a shortie for `document.createElement` method
536
+ * which allows you to create a new `HTMLElement` for a given `tagName`
537
+ * or based on an object with specific non-readonly attributes:
538
+ * `id`, `className`, `textContent`, `style`, etc.
539
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
540
+ *
541
+ * @param {Record<string, string> | string} param `tagName` or object
542
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
543
+ */
544
+ function createElement(param) {
545
+ if (typeof param === 'string') {
546
+ return getDocument().createElement(param);
547
+ }
548
+
549
+ const { tagName } = param;
550
+ const attr = { ...param };
551
+ const newElement = createElement(tagName);
552
+ delete attr.tagName;
553
+ ObjectAssign(newElement, attr);
554
+ return newElement;
555
+ }
556
+
557
+ /**
558
+ * This is a shortie for `document.createElementNS` method
559
+ * which allows you to create a new `HTMLElement` for a given `tagName`
560
+ * or based on an object with specific non-readonly attributes:
561
+ * `id`, `className`, `textContent`, `style`, etc.
562
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
563
+ *
564
+ * @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
565
+ * @param {Record<string, string> | string} param `tagName` or object
566
+ * @return {HTMLElement | Element} a new `HTMLElement` or `Element`
567
+ */
568
+ function createElementNS(namespace, param) {
569
+ if (typeof param === 'string') {
570
+ return getDocument().createElementNS(namespace, param);
571
+ }
572
+
573
+ const { tagName } = param;
574
+ const attr = { ...param };
575
+ const newElement = createElementNS(namespace, tagName);
576
+ delete attr.tagName;
577
+ ObjectAssign(newElement, attr);
578
+ return newElement;
579
+ }
580
+
581
+ /**
582
+ * Shortcut for the `Element.dispatchEvent(Event)` method.
583
+ *
584
+ * @param {HTMLElement | Element} element is the target
585
+ * @param {Event} event is the `Event` object
586
+ */
587
+ const dispatchEvent = (element, event) => element.dispatchEvent(event);
588
+
589
+ /** @type {Map<string, Map<HTMLElement | Element, Record<string, any>>>} */
590
+ const componentData = new Map();
591
+ /**
592
+ * An interface for web components background data.
593
+ * @see https://github.com/thednp/bootstrap.native/blob/master/src/components/base-component.js
594
+ */
595
+ const Data = {
596
+ /**
597
+ * Sets web components data.
598
+ * @param {HTMLElement | Element | string} target target element
599
+ * @param {string} component the component's name or a unique key
600
+ * @param {Record<string, any>} instance the component instance
601
+ */
602
+ set: (target, component, instance) => {
603
+ const element = querySelector(target);
604
+ if (!element) return;
605
+
606
+ if (!componentData.has(component)) {
607
+ componentData.set(component, new Map());
608
+ }
609
+
610
+ const instanceMap = componentData.get(component);
611
+ // @ts-ignore - not undefined, but defined right above
612
+ instanceMap.set(element, instance);
613
+ },
614
+
615
+ /**
616
+ * Returns all instances for specified component.
617
+ * @param {string} component the component's name or a unique key
618
+ * @returns {Map<HTMLElement | Element, Record<string, any>>?} all the component instances
619
+ */
620
+ getAllFor: (component) => {
621
+ const instanceMap = componentData.get(component);
622
+
623
+ return instanceMap || null;
624
+ },
625
+
626
+ /**
627
+ * Returns the instance associated with the target.
628
+ * @param {HTMLElement | Element | string} target target element
629
+ * @param {string} component the component's name or a unique key
630
+ * @returns {Record<string, any>?} the instance
631
+ */
632
+ get: (target, component) => {
633
+ const element = querySelector(target);
634
+ const allForC = Data.getAllFor(component);
635
+ const instance = element && allForC && allForC.get(element);
636
+
637
+ return instance || null;
638
+ },
639
+
640
+ /**
641
+ * Removes web components data.
642
+ * @param {HTMLElement | Element | string} target target element
643
+ * @param {string} component the component's name or a unique key
644
+ */
645
+ remove: (target, component) => {
646
+ const element = querySelector(target);
647
+ const instanceMap = componentData.get(component);
648
+ if (!instanceMap || !element) return;
649
+
650
+ instanceMap.delete(element);
651
+
652
+ if (instanceMap.size === 0) {
653
+ componentData.delete(component);
654
+ }
655
+ },
656
+ };
657
+
658
+ /**
659
+ * An alias for `Data.get()`.
660
+ * @type {SHORTER.getInstance<any>}
661
+ */
662
+ const getInstance = (target, component) => Data.get(target, component);
663
+
664
+ /**
665
+ * Shortcut for `Object.keys()` static method.
666
+ * @param {Record<string, any>} obj a target object
667
+ * @returns {string[]}
668
+ */
669
+ const ObjectKeys = (obj) => Object.keys(obj);
670
+
671
+ /**
672
+ * Shortcut for multiple uses of `HTMLElement.style.propertyName` method.
673
+ * @param {HTMLElement | Element} element target element
674
+ * @param {Partial<CSSStyleDeclaration>} styles attribute value
675
+ */
676
+ // @ts-ignore
677
+ const setElementStyle = (element, styles) => ObjectAssign(element.style, styles);
678
+
679
+ /**
680
+ * Shortcut for `HTMLElement.getAttribute()` method.
681
+ * @param {HTMLElement | Element} element target element
682
+ * @param {string} attribute attribute name
683
+ * @returns {string?} attribute value
684
+ */
685
+ const getAttribute = (element, attribute) => element.getAttribute(attribute);
686
+
687
+ /**
688
+ * The raw value or a given component option.
689
+ *
690
+ * @typedef {string | HTMLElement | Function | number | boolean | null} niceValue
691
+ */
692
+
693
+ /**
694
+ * Utility to normalize component options
695
+ *
696
+ * @param {any} value the input value
697
+ * @return {niceValue} the normalized value
698
+ */
699
+ function normalizeValue(value) {
700
+ if (value === 'true') { // boolean
701
+ return true;
702
+ }
703
+
704
+ if (value === 'false') { // boolean
705
+ return false;
706
+ }
707
+
708
+ if (!Number.isNaN(+value)) { // number
709
+ return +value;
710
+ }
711
+
712
+ if (value === '' || value === 'null') { // null
713
+ return null;
714
+ }
715
+
716
+ // string / function / HTMLElement / object
717
+ return value;
718
+ }
719
+
720
+ /**
721
+ * Shortcut for `String.toLowerCase()`.
722
+ *
723
+ * @param {string} source input string
724
+ * @returns {string} lowercase output string
725
+ */
726
+ const toLowerCase = (source) => source.toLowerCase();
727
+
728
+ /**
729
+ * Utility to normalize component options.
730
+ *
731
+ * @param {HTMLElement | Element} element target
732
+ * @param {Record<string, any>} defaultOps component default options
733
+ * @param {Record<string, any>} inputOps component instance options
734
+ * @param {string=} ns component namespace
735
+ * @return {Record<string, any>} normalized component options object
736
+ */
737
+ function normalizeOptions(element, defaultOps, inputOps, ns) {
738
+ // @ts-ignore -- our targets are always `HTMLElement`
739
+ const data = { ...element.dataset };
740
+ /** @type {Record<string, any>} */
741
+ const normalOps = {};
742
+ /** @type {Record<string, any>} */
743
+ const dataOps = {};
744
+ const title = 'title';
745
+
746
+ ObjectKeys(data).forEach((k) => {
747
+ const key = ns && k.includes(ns)
748
+ ? k.replace(ns, '').replace(/[A-Z]/, (match) => toLowerCase(match))
749
+ : k;
750
+
751
+ dataOps[key] = normalizeValue(data[k]);
752
+ });
753
+
754
+ ObjectKeys(inputOps).forEach((k) => {
755
+ inputOps[k] = normalizeValue(inputOps[k]);
756
+ });
757
+
758
+ ObjectKeys(defaultOps).forEach((k) => {
759
+ if (k in inputOps) {
760
+ normalOps[k] = inputOps[k];
761
+ } else if (k in dataOps) {
762
+ normalOps[k] = dataOps[k];
763
+ } else {
764
+ normalOps[k] = k === title
765
+ ? getAttribute(element, title)
766
+ : defaultOps[k];
767
+ }
768
+ });
769
+
770
+ return normalOps;
771
+ }
772
+
773
+ /**
774
+ * Utility to force re-paint of an `HTMLElement` target.
775
+ *
776
+ * @param {HTMLElement | Element} element is the target
777
+ * @return {number} the `Element.offsetHeight` value
778
+ */
779
+ // @ts-ignore
780
+ const reflow = (element) => element.offsetHeight;
781
+
782
+ /**
783
+ * Utility to focus an `HTMLElement` target.
784
+ *
785
+ * @param {HTMLElement | Element} element is the target
786
+ */
787
+ // @ts-ignore -- `Element`s resulted from querySelector can focus too
788
+ const focus = (element) => element.focus();
789
+
790
+ /**
791
+ * Check class in `HTMLElement.classList`.
792
+ *
793
+ * @param {HTMLElement | Element} element target
794
+ * @param {string} classNAME to check
795
+ * @returns {boolean}
796
+ */
797
+ function hasClass(element, classNAME) {
798
+ return element.classList.contains(classNAME);
799
+ }
800
+
801
+ /**
802
+ * Add class to `HTMLElement.classList`.
803
+ *
804
+ * @param {HTMLElement | Element} element target
805
+ * @param {string} classNAME to add
806
+ * @returns {void}
807
+ */
808
+ function addClass(element, classNAME) {
809
+ element.classList.add(classNAME);
810
+ }
811
+
812
+ /**
813
+ * Remove class from `HTMLElement.classList`.
814
+ *
815
+ * @param {HTMLElement | Element} element target
816
+ * @param {string} classNAME to remove
817
+ * @returns {void}
818
+ */
819
+ function removeClass(element, classNAME) {
820
+ element.classList.remove(classNAME);
821
+ }
822
+
823
+ /**
824
+ * Shortcut for `HTMLElement.setAttribute()` method.
825
+ * @param {HTMLElement | Element} element target element
826
+ * @param {string} attribute attribute name
827
+ * @param {string} value attribute value
828
+ * @returns {void}
829
+ */
830
+ const setAttribute = (element, attribute, value) => element.setAttribute(attribute, value);
831
+
832
+ /**
833
+ * Shortcut for `HTMLElement.removeAttribute()` method.
834
+ * @param {HTMLElement | Element} element target element
835
+ * @param {string} attribute attribute name
836
+ * @returns {void}
837
+ */
838
+ const removeAttribute = (element, attribute) => element.removeAttribute(attribute);
839
+
840
+ /** @type {Record<string, string>} */
841
+ const colorPickerLabels = {
842
+ pickerLabel: 'Colour Picker',
843
+ appearanceLabel: 'Colour Appearance',
844
+ valueLabel: 'Colour Value',
845
+ toggleLabel: 'Select Colour',
846
+ presetsLabel: 'Colour Presets',
847
+ defaultsLabel: 'Colour Defaults',
848
+ formatLabel: 'Format',
849
+ alphaLabel: 'Alpha',
850
+ hexLabel: 'Hexadecimal',
851
+ hueLabel: 'Hue',
852
+ whitenessLabel: 'Whiteness',
853
+ blacknessLabel: 'Blackness',
854
+ saturationLabel: 'Saturation',
855
+ lightnessLabel: 'Lightness',
856
+ redLabel: 'Red',
857
+ greenLabel: 'Green',
858
+ blueLabel: 'Blue',
859
+ };
860
+
861
+ /**
862
+ * A list of 17 color names used for WAI-ARIA compliance.
863
+ * @type {string[]}
864
+ */
865
+ const colorNames = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
866
+
867
+ /**
868
+ * A list of explicit default non-color values.
869
+ */
870
+ const nonColors = ['transparent', 'currentColor', 'inherit', 'revert', 'initial'];
871
+
872
+ /**
873
+ * Shortcut for `String.toUpperCase()`.
874
+ *
875
+ * @param {string} source input string
876
+ * @returns {string} uppercase output string
877
+ */
878
+ const toUpperCase = (source) => source.toUpperCase();
879
+
880
+ const vHidden = 'v-hidden';
881
+
882
+ /**
883
+ * Returns the color form for `ColorPicker`.
884
+ *
885
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
886
+ * @returns {HTMLElement | Element} a new `<div>` element with color component `<input>`
887
+ */
888
+ function getColorForm(self) {
889
+ const { format, id, componentLabels } = self;
890
+ const colorForm = createElement({
891
+ tagName: 'div',
892
+ className: `color-form ${format}`,
893
+ });
894
+
895
+ let components = ['hex'];
896
+ if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
897
+ else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
898
+ else if (format === 'hwb') components = ['hue', 'whiteness', 'blackness', 'alpha'];
899
+
900
+ components.forEach((c) => {
901
+ const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
902
+ const cID = `color_${format}_${c}_${id}`;
903
+ const formatLabel = componentLabels[`${c}Label`];
904
+ const cInputLabel = createElement({ tagName: 'label' });
905
+ setAttribute(cInputLabel, 'for', cID);
906
+ cInputLabel.append(
907
+ createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
908
+ createElement({ tagName: 'span', className: vHidden, innerText: formatLabel }),
909
+ );
910
+ const cInput = createElement({
911
+ tagName: 'input',
912
+ id: cID,
913
+ // name: cID, - prevent saving the value to a form
914
+ type: format === 'hex' ? 'text' : 'number',
915
+ value: c === 'alpha' ? '100' : '0',
916
+ className: `color-input ${c}`,
917
+ });
918
+ setAttribute(cInput, 'autocomplete', 'off');
919
+ setAttribute(cInput, 'spellcheck', 'false');
920
+
921
+ // alpha
922
+ let max = '100';
923
+ let step = '1';
924
+ if (c !== 'alpha') {
925
+ if (format === 'rgb') {
926
+ max = '255'; step = '1';
927
+ } else if (c === 'hue') {
928
+ max = '360'; step = '1';
929
+ }
930
+ }
931
+ ObjectAssign(cInput, {
932
+ min: '0',
933
+ max,
934
+ step,
935
+ });
936
+ colorForm.append(cInputLabel, cInput);
937
+ });
938
+ return colorForm;
939
+ }
940
+
941
+ /**
942
+ * A global namespace for aria-label.
943
+ * @type {string}
944
+ */
945
+ const ariaLabel = 'aria-label';
946
+
947
+ /**
948
+ * A global namespace for aria-valuemin.
949
+ * @type {string}
950
+ */
951
+ const ariaValueMin = 'aria-valuemin';
952
+
953
+ /**
954
+ * A global namespace for aria-valuemax.
955
+ * @type {string}
956
+ */
957
+ const ariaValueMax = 'aria-valuemax';
958
+
959
+ const tabIndex = 'tabindex';
960
+
961
+ /**
962
+ * Returns all color controls for `ColorPicker`.
963
+ *
964
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
965
+ * @returns {HTMLElement | Element} color controls
966
+ */
967
+ function getColorControls(self) {
968
+ const { format, componentLabels } = self;
969
+ const {
970
+ hueLabel, alphaLabel, lightnessLabel, saturationLabel,
971
+ whitenessLabel, blacknessLabel,
972
+ } = componentLabels;
973
+
974
+ const max1 = format === 'hsl' ? 360 : 100;
975
+ const max2 = format === 'hsl' ? 100 : 360;
976
+ const max3 = 100;
977
+
978
+ let ctrl1Label = format === 'hsl'
979
+ ? `${hueLabel} & ${lightnessLabel}`
980
+ : `${lightnessLabel} & ${saturationLabel}`;
981
+
982
+ ctrl1Label = format === 'hwb'
983
+ ? `${whitenessLabel} & ${blacknessLabel}`
984
+ : ctrl1Label;
985
+
986
+ const ctrl2Label = format === 'hsl'
987
+ ? `${saturationLabel}`
988
+ : `${hueLabel}`;
989
+
990
+ const colorControls = createElement({
991
+ tagName: 'div',
992
+ className: `color-controls ${format}`,
993
+ });
994
+
995
+ const colorPointer = 'color-pointer';
996
+ const colorSlider = 'color-slider';
997
+
998
+ const controls = [
999
+ {
1000
+ i: 1,
1001
+ c: colorPointer,
1002
+ l: ctrl1Label,
1003
+ min: 0,
1004
+ max: max1,
1005
+ },
1006
+ {
1007
+ i: 2,
1008
+ c: colorSlider,
1009
+ l: ctrl2Label,
1010
+ min: 0,
1011
+ max: max2,
1012
+ },
1013
+ {
1014
+ i: 3,
1015
+ c: colorSlider,
1016
+ l: alphaLabel,
1017
+ min: 0,
1018
+ max: max3,
1019
+ },
1020
+ ];
1021
+
1022
+ controls.forEach((template) => {
1023
+ const {
1024
+ i, c, l, min, max,
1025
+ } = template;
1026
+ const control = createElement({
1027
+ tagName: 'div',
1028
+ className: 'color-control',
1029
+ });
1030
+ setAttribute(control, 'role', 'presentation');
1031
+
1032
+ control.append(
1033
+ createElement({
1034
+ tagName: 'div',
1035
+ className: `visual-control visual-control${i}`,
1036
+ }),
1037
+ );
1038
+
1039
+ const knob = createElement({
1040
+ tagName: 'div',
1041
+ className: `${c} knob`,
1042
+ ariaLive: 'polite',
1043
+ });
1044
+
1045
+ setAttribute(knob, ariaLabel, l);
1046
+ setAttribute(knob, 'role', 'slider');
1047
+ setAttribute(knob, tabIndex, '0');
1048
+ setAttribute(knob, ariaValueMin, `${min}`);
1049
+ setAttribute(knob, ariaValueMax, `${max}`);
1050
+ control.append(knob);
1051
+ colorControls.append(control);
1052
+ });
1053
+
1054
+ return colorControls;
1055
+ }
1056
+
1057
+ /**
1058
+ * Helps setting CSS variables to the color-menu.
1059
+ * @param {HTMLElement} element
1060
+ * @param {Record<string,any>} props
1061
+ */
1062
+ function setCSSProperties(element, props) {
1063
+ ObjectKeys(props).forEach((prop) => {
1064
+ element.style.setProperty(prop, props[prop]);
1065
+ });
1066
+ }
1067
+
1068
+ /**
1069
+ * Returns the `document.head` or the `<head>` element.
1070
+ *
1071
+ * @param {(Node | HTMLElement | Element | globalThis)=} node
1072
+ * @returns {HTMLElement | HTMLHeadElement}
1073
+ */
1074
+ function getDocumentHead(node) {
1075
+ return getDocument(node).head;
1076
+ }
1077
+
1078
+ /**
1079
+ * Round colour components, for all formats except HEX.
1080
+ * @param {number} v one of the colour components
1081
+ * @returns {number} the rounded number
1082
+ */
1083
+ function roundPart(v) {
1084
+ const floor = Math.floor(v);
1085
+ return v - floor < 0.5 ? floor : Math.round(v);
1086
+ }
1087
+
1088
+ // Color supported formats
1089
+ const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsb', 'hwb'];
1090
+
1091
+ // Hue angles
1092
+ const ANGLES = 'deg|rad|grad|turn';
1093
+
1094
+ // <http://www.w3.org/TR/css3-values/#integers>
1095
+ const CSS_INTEGER = '[-\\+]?\\d+%?';
1096
+
1097
+ // Include CSS3 Module
1098
+ // <http://www.w3.org/TR/css3-values/#number-value>
1099
+ const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
1100
+
1101
+ // Include CSS4 Module Hue degrees unit
1102
+ // <https://www.w3.org/TR/css3-values/#angle-value>
1103
+ const CSS_ANGLE = `[-\\+]?\\d*\\.?\\d+(?:${ANGLES})?`;
1104
+
1105
+ // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
1106
+ const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
1107
+
1108
+ // Add angles to the mix
1109
+ const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
1110
+
1111
+ // Actual matching.
1112
+ // Parentheses and commas are optional, but not required.
1113
+ // Whitespace can take the place of commas or opening paren
1114
+ const PERMISSIVE_MATCH = `[\\s|\\(]+(${CSS_UNIT2})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s|\\/\\s]*(${CSS_UNIT})?\\s*\\)?`;
1115
+
1116
+ const matchers = {
1117
+ CSS_UNIT: new RegExp(CSS_UNIT2),
1118
+ hwb: new RegExp(`hwb${PERMISSIVE_MATCH}`),
1119
+ rgb: new RegExp(`rgb(?:a)?${PERMISSIVE_MATCH}`),
1120
+ hsl: new RegExp(`hsl(?:a)?${PERMISSIVE_MATCH}`),
1121
+ hsv: new RegExp(`hsv(?:a)?${PERMISSIVE_MATCH}`),
1122
+ hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1123
+ hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
1124
+ hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1125
+ hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
1126
+ };
1127
+
1128
+ /**
1129
+ * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
1130
+ * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
1131
+ * @param {string} n testing number
1132
+ * @returns {boolean} the query result
1133
+ */
1134
+ function isOnePointZero(n) {
1135
+ return `${n}`.includes('.') && parseFloat(n) === 1;
1136
+ }
1137
+
1138
+ /**
1139
+ * Check to see if string passed in is a percentage
1140
+ * @param {string} n testing number
1141
+ * @returns {boolean} the query result
1142
+ */
1143
+ function isPercentage(n) {
1144
+ return `${n}`.includes('%');
1145
+ }
1146
+
1147
+ /**
1148
+ * Check to see if string passed in is an angle
1149
+ * @param {string} n testing string
1150
+ * @returns {boolean} the query result
1151
+ */
1152
+ function isAngle(n) {
1153
+ return ANGLES.split('|').some((a) => `${n}`.includes(a));
1154
+ }
1155
+
1156
+ /**
1157
+ * Check to see if string passed is a web safe colour.
1158
+ * @param {string} color a colour name, EG: *red*
1159
+ * @returns {boolean} the query result
1160
+ */
1161
+ function isColorName(color) {
1162
+ return !['#', ...COLOR_FORMAT].some((s) => color.includes(s))
1163
+ && !/[0-9]/.test(color);
1164
+ }
1165
+
1166
+ /**
1167
+ * Check to see if it looks like a CSS unit
1168
+ * (see `matchers` above for definition).
1169
+ * @param {string | number} color testing value
1170
+ * @returns {boolean} the query result
1171
+ */
1172
+ function isValidCSSUnit(color) {
1173
+ return Boolean(matchers.CSS_UNIT.exec(String(color)));
1174
+ }
1175
+
1176
+ /**
1177
+ * Take input from [0, n] and return it as [0, 1]
1178
+ * @param {*} N the input number
1179
+ * @param {number} max the number maximum value
1180
+ * @returns {number} the number in [0, 1] value range
1181
+ */
1182
+ function bound01(N, max) {
1183
+ let n = N;
1184
+ if (isOnePointZero(n)) n = '100%';
1185
+
1186
+ n = max === 360 ? n : Math.min(max, Math.max(0, parseFloat(n)));
1187
+
1188
+ // Handle hue angles
1189
+ if (isAngle(N)) n = N.replace(new RegExp(ANGLES), '');
1190
+
1191
+ // Automatically convert percentage into number
1192
+ if (isPercentage(n)) n = parseInt(String(n * max), 10) / 100;
1193
+
1194
+ // Handle floating point rounding errors
1195
+ if (Math.abs(n - max) < 0.000001) {
1196
+ return 1;
1197
+ }
1198
+ // Convert into [0, 1] range if it isn't already
1199
+ if (max === 360) {
1200
+ // If n is a hue given in degrees,
1201
+ // wrap around out-of-range values into [0, 360] range
1202
+ // then convert into [0, 1].
1203
+ n = (n < 0 ? (n % max) + max : n % max) / parseFloat(String(max));
1204
+ } else {
1205
+ // If n not a hue given in degrees
1206
+ // Convert into [0, 1] range if it isn't already.
1207
+ n = (n % max) / parseFloat(String(max));
1208
+ }
1209
+ return n;
1210
+ }
1211
+
1212
+ /**
1213
+ * Return a valid alpha value [0,1] with all invalid values being set to 1.
1214
+ * @param {string | number} a transparency value
1215
+ * @returns {number} a transparency value in the [0, 1] range
1216
+ */
1217
+ function boundAlpha(a) {
1218
+ let na = parseFloat(`${a}`);
1219
+
1220
+ if (Number.isNaN(na) || na < 0 || na > 1) {
1221
+ na = 1;
1222
+ }
1223
+
1224
+ return na;
1225
+ }
1226
+
1227
+ /**
1228
+ * Force a number between 0 and 1.
1229
+ * @param {number} v the float number
1230
+ * @returns {number} - the resulting number
1231
+ */
1232
+ function clamp01(v) {
1233
+ return Math.min(1, Math.max(0, v));
1234
+ }
1235
+
1236
+ /**
1237
+ * Returns the hexadecimal value of a web safe colour.
1238
+ * @param {string} name
1239
+ * @returns {string}
1240
+ */
1241
+ function getRGBFromName(name) {
1242
+ const documentHead = getDocumentHead();
1243
+ setElementStyle(documentHead, { color: name });
1244
+ const colorName = getElementStyle(documentHead, 'color');
1245
+ setElementStyle(documentHead, { color: '' });
1246
+ return colorName;
1247
+ }
1248
+
1249
+ /**
1250
+ * Converts a decimal value to hexadecimal.
1251
+ * @param {number} d the input number
1252
+ * @returns {string} - the hexadecimal value
1253
+ */
1254
+ function convertDecimalToHex(d) {
1255
+ return roundPart(d * 255).toString(16);
1256
+ }
1257
+
1258
+ /**
1259
+ * Converts a hexadecimal value to decimal.
1260
+ * @param {string} h hexadecimal value
1261
+ * @returns {number} number in decimal format
1262
+ */
1263
+ function convertHexToDecimal(h) {
1264
+ return parseIntFromHex(h) / 255;
1265
+ }
1266
+
1267
+ /**
1268
+ * Converts a base-16 hexadecimal value into a base-10 integer.
1269
+ * @param {string} val
1270
+ * @returns {number}
1271
+ */
1272
+ function parseIntFromHex(val) {
1273
+ return parseInt(val, 16);
1274
+ }
1275
+
1276
+ /**
1277
+ * Force a hexadecimal value to have 2 characters.
1278
+ * @param {string} c string with [0-9A-F] ranged values
1279
+ * @returns {string} 0 => 00, a => 0a
1280
+ */
1281
+ function pad2(c) {
1282
+ return c.length === 1 ? `0${c}` : String(c);
1283
+ }
1284
+
1285
+ /**
1286
+ * Converts an RGB colour value to HSL.
1287
+ *
1288
+ * @param {number} R Red component [0, 255]
1289
+ * @param {number} G Green component [0, 255]
1290
+ * @param {number} B Blue component [0, 255]
1291
+ * @returns {CP.HSL} {h,s,l} object with [0, 1] ranged values
1292
+ */
1293
+ function rgbToHsl(R, G, B) {
1294
+ const r = R / 255;
1295
+ const g = G / 255;
1296
+ const b = B / 255;
1297
+ const max = Math.max(r, g, b);
1298
+ const min = Math.min(r, g, b);
1299
+ let h = 0;
1300
+ let s = 0;
1301
+ const l = (max + min) / 2;
1302
+ if (max === min) {
1303
+ s = 0;
1304
+ h = 0; // achromatic
1305
+ } else {
1306
+ const d = max - min;
1307
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
1308
+ switch (max) {
1309
+ case r:
1310
+ h = (g - b) / d + (g < b ? 6 : 0);
1311
+ break;
1312
+ case g:
1313
+ h = (b - r) / d + 2;
1314
+ break;
1315
+ case b:
1316
+ h = (r - g) / d + 4;
1317
+ break;
1318
+ }
1319
+ h /= 6;
1320
+ }
1321
+ return { h, s, l };
1322
+ }
1323
+
1324
+ /**
1325
+ * Returns a normalized RGB component value.
1326
+ * @param {number} p
1327
+ * @param {number} q
1328
+ * @param {number} t
1329
+ * @returns {number}
1330
+ */
1331
+ function hueToRgb(p, q, t) {
1332
+ let T = t;
1333
+ if (T < 0) T += 1;
1334
+ if (T > 1) T -= 1;
1335
+ if (T < 1 / 6) return p + (q - p) * (6 * T);
1336
+ if (T < 1 / 2) return q;
1337
+ if (T < 2 / 3) return p + (q - p) * (2 / 3 - T) * 6;
1338
+ return p;
1339
+ }
1340
+
1341
+ /**
1342
+ * Returns an HWB colour object from an RGB colour object.
1343
+ * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
1344
+ * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
1345
+ *
1346
+ * @param {number} R Red component [0, 255]
1347
+ * @param {number} G Green [0, 255]
1348
+ * @param {number} B Blue [0, 255]
1349
+ * @return {CP.HWB} {h,w,b} object with [0, 1] ranged values
1350
+ */
1351
+ function rgbToHwb(R, G, B) {
1352
+ const r = R / 255;
1353
+ const g = G / 255;
1354
+ const b = B / 255;
1355
+
1356
+ let f = 0;
1357
+ let i = 0;
1358
+ const whiteness = Math.min(r, g, b);
1359
+ const max = Math.max(r, g, b);
1360
+ const black = 1 - max;
1361
+
1362
+ if (max === whiteness) return { h: 0, w: whiteness, b: black };
1363
+ if (r === whiteness) {
1364
+ f = g - b;
1365
+ i = 3;
1366
+ } else {
1367
+ f = g === whiteness ? b - r : r - g;
1368
+ i = g === whiteness ? 5 : 1;
1369
+ }
1370
+
1371
+ const h = (i - f / (max - whiteness)) / 6;
1372
+ return {
1373
+ h: h === 1 ? 0 : h,
1374
+ w: whiteness,
1375
+ b: black,
1376
+ };
1377
+ }
1378
+
1379
+ /**
1380
+ * Returns an RGB colour object from an HWB colour.
1381
+ *
1382
+ * @param {number} H Hue Angle [0, 1]
1383
+ * @param {number} W Whiteness [0, 1]
1384
+ * @param {number} B Blackness [0, 1]
1385
+ * @return {CP.RGB} {r,g,b} object with [0, 255] ranged values
1386
+ *
1387
+ * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
1388
+ * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
1389
+ */
1390
+ function hwbToRgb(H, W, B) {
1391
+ if (W + B >= 1) {
1392
+ const gray = (W / (W + B)) * 255;
1393
+ return { r: gray, g: gray, b: gray };
1394
+ }
1395
+ let { r, g, b } = hslToRgb(H, 1, 0.5);
1396
+ [r, g, b] = [r, g, b]
1397
+ .map((v) => (v / 255) * (1 - W - B) + W)
1398
+ .map((v) => v * 255);
1399
+
1400
+ return { r, g, b };
1401
+ }
1402
+
1403
+ /**
1404
+ * Converts an HSL colour value to RGB.
1405
+ *
1406
+ * @param {number} h Hue Angle [0, 1]
1407
+ * @param {number} s Saturation [0, 1]
1408
+ * @param {number} l Lightness Angle [0, 1]
1409
+ * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1410
+ */
1411
+ function hslToRgb(h, s, l) {
1412
+ let r = 0;
1413
+ let g = 0;
1414
+ let b = 0;
1415
+
1416
+ if (s === 0) {
1417
+ // achromatic
1418
+ g = l;
1419
+ b = l;
1420
+ r = l;
1421
+ } else {
1422
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1423
+ const p = 2 * l - q;
1424
+ r = hueToRgb(p, q, h + 1 / 3);
1425
+ g = hueToRgb(p, q, h);
1426
+ b = hueToRgb(p, q, h - 1 / 3);
1427
+ }
1428
+ [r, g, b] = [r, g, b].map((x) => x * 255);
1429
+
1430
+ return { r, g, b };
1431
+ }
1432
+
1433
+ /**
1434
+ * Converts an RGB colour value to HSV.
1435
+ *
1436
+ * @param {number} R Red component [0, 255]
1437
+ * @param {number} G Green [0, 255]
1438
+ * @param {number} B Blue [0, 255]
1439
+ * @returns {CP.HSV} {h,s,v} object with [0, 1] ranged values
1440
+ */
1441
+ function rgbToHsv(R, G, B) {
1442
+ const r = R / 255;
1443
+ const g = G / 255;
1444
+ const b = B / 255;
1445
+ const max = Math.max(r, g, b);
1446
+ const min = Math.min(r, g, b);
1447
+ let h = 0;
1448
+ const v = max;
1449
+ const d = max - min;
1450
+ const s = max === 0 ? 0 : d / max;
1451
+ if (max === min) {
1452
+ h = 0; // achromatic
1453
+ } else {
1454
+ switch (max) {
1455
+ case r:
1456
+ h = (g - b) / d + (g < b ? 6 : 0);
1457
+ break;
1458
+ case g:
1459
+ h = (b - r) / d + 2;
1460
+ break;
1461
+ case b:
1462
+ h = (r - g) / d + 4;
1463
+ break;
1464
+ }
1465
+ h /= 6;
1466
+ }
1467
+ return { h, s, v };
1468
+ }
1469
+
1470
+ /**
1471
+ * Converts an HSV colour value to RGB.
1472
+ *
1473
+ * @param {number} H Hue Angle [0, 1]
1474
+ * @param {number} S Saturation [0, 1]
1475
+ * @param {number} V Brightness Angle [0, 1]
1476
+ * @returns {CP.RGB} {r,g,b} object with [0, 1] ranged values
1477
+ */
1478
+ function hsvToRgb(H, S, V) {
1479
+ const h = H * 6;
1480
+ const s = S;
1481
+ const v = V;
1482
+ const i = Math.floor(h);
1483
+ const f = h - i;
1484
+ const p = v * (1 - s);
1485
+ const q = v * (1 - f * s);
1486
+ const t = v * (1 - (1 - f) * s);
1487
+ const mod = i % 6;
1488
+ const r = [v, q, p, p, t, v][mod];
1489
+ const g = [t, v, v, q, p, p][mod];
1490
+ const b = [p, p, t, v, v, q][mod];
1491
+ return { r: r * 255, g: g * 255, b: b * 255 };
1492
+ }
1493
+
1494
+ /**
1495
+ * Converts an RGB colour to hex
1496
+ *
1497
+ * Assumes r, g, and b are contained in the set [0, 255]
1498
+ * Returns a 3 or 6 character hex
1499
+ * @param {number} r Red component [0, 255]
1500
+ * @param {number} g Green [0, 255]
1501
+ * @param {number} b Blue [0, 255]
1502
+ * @param {boolean=} allow3Char
1503
+ * @returns {string}
1504
+ */
1505
+ function rgbToHex(r, g, b, allow3Char) {
1506
+ const hex = [
1507
+ pad2(roundPart(r).toString(16)),
1508
+ pad2(roundPart(g).toString(16)),
1509
+ pad2(roundPart(b).toString(16)),
1510
+ ];
1511
+
1512
+ // Return a 3 character hex if possible
1513
+ if (allow3Char && hex[0].charAt(0) === hex[0].charAt(1)
1514
+ && hex[1].charAt(0) === hex[1].charAt(1)
1515
+ && hex[2].charAt(0) === hex[2].charAt(1)) {
1516
+ return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
1517
+ }
1518
+
1519
+ return hex.join('');
1520
+ }
1521
+
1522
+ /**
1523
+ * Converts an RGBA color plus alpha transparency to hex8.
1524
+ *
1525
+ * @param {number} r Red component [0, 255]
1526
+ * @param {number} g Green [0, 255]
1527
+ * @param {number} b Blue [0, 255]
1528
+ * @param {number} a Alpha transparency [0, 1]
1529
+ * @param {boolean=} allow4Char when *true* it will also find hex shorthand
1530
+ * @returns {string} a hexadecimal value with alpha transparency
1531
+ */
1532
+ function rgbaToHex(r, g, b, a, allow4Char) {
1533
+ const hex = [
1534
+ pad2(roundPart(r).toString(16)),
1535
+ pad2(roundPart(g).toString(16)),
1536
+ pad2(roundPart(b).toString(16)),
1537
+ pad2(convertDecimalToHex(a)),
1538
+ ];
1539
+
1540
+ // Return a 4 character hex if possible
1541
+ if (allow4Char && hex[0].charAt(0) === hex[0].charAt(1)
1542
+ && hex[1].charAt(0) === hex[1].charAt(1)
1543
+ && hex[2].charAt(0) === hex[2].charAt(1)
1544
+ && hex[3].charAt(0) === hex[3].charAt(1)) {
1545
+ return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
1546
+ }
1547
+ return hex.join('');
1548
+ }
1549
+
1550
+ /**
1551
+ * Returns a colour object corresponding to a given number.
1552
+ * @param {number} color input number
1553
+ * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1554
+ */
1555
+ function numberInputToObject(color) {
1556
+ /* eslint-disable no-bitwise */
1557
+ return {
1558
+ r: color >> 16,
1559
+ g: (color & 0xff00) >> 8,
1560
+ b: color & 0xff,
1561
+ };
1562
+ /* eslint-enable no-bitwise */
1563
+ }
1564
+
1565
+ /**
1566
+ * Permissive string parsing. Take in a number of formats, and output an object
1567
+ * based on detected format. Returns {r,g,b} or {h,s,l} or {h,s,v}
1568
+ * @param {string} input colour value in any format
1569
+ * @returns {Record<string, (number | string)> | false} an object matching the RegExp
1570
+ */
1571
+ function stringInputToObject(input) {
1572
+ let color = input.trim().toLowerCase();
1573
+ if (color.length === 0) {
1574
+ return {
1575
+ r: 0, g: 0, b: 0, a: 0,
1576
+ };
1577
+ }
1578
+ let named = false;
1579
+ if (isColorName(color)) {
1580
+ color = getRGBFromName(color);
1581
+ named = true;
1582
+ } else if (nonColors.includes(color)) {
1583
+ const isTransparent = color === 'transparent';
1584
+ const rgb = isTransparent ? 0 : 255;
1585
+ const a = isTransparent ? 0 : 1;
1586
+ return {
1587
+ r: rgb, g: rgb, b: rgb, a, format: 'rgb',
1588
+ };
1589
+ }
1590
+
1591
+ // Try to match string input using regular expressions.
1592
+ // Keep most of the number bounding out of this function,
1593
+ // don't worry about [0,1] or [0,100] or [0,360]
1594
+ // Just return an object and let the conversion functions handle that.
1595
+ // This way the result will be the same whether Color is initialized with string or object.
1596
+ let [, m1, m2, m3, m4] = matchers.rgb.exec(color) || [];
1597
+ if (m1 && m2 && m3/* && m4 */) {
1598
+ return {
1599
+ r: m1, g: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'rgb',
1600
+ };
1601
+ }
1602
+ [, m1, m2, m3, m4] = matchers.hsl.exec(color) || [];
1603
+ if (m1 && m2 && m3/* && m4 */) {
1604
+ return {
1605
+ h: m1, s: m2, l: m3, a: m4 !== undefined ? m4 : 1, format: 'hsl',
1606
+ };
1607
+ }
1608
+ [, m1, m2, m3, m4] = matchers.hsv.exec(color) || [];
1609
+ if (m1 && m2 && m3/* && m4 */) {
1610
+ return {
1611
+ h: m1, s: m2, v: m3, a: m4 !== undefined ? m4 : 1, format: 'hsv',
1612
+ };
1613
+ }
1614
+ [, m1, m2, m3, m4] = matchers.hwb.exec(color) || [];
1615
+ if (m1 && m2 && m3) {
1616
+ return {
1617
+ h: m1, w: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'hwb',
1618
+ };
1619
+ }
1620
+ [, m1, m2, m3, m4] = matchers.hex8.exec(color) || [];
1621
+ if (m1 && m2 && m3 && m4) {
1622
+ return {
1623
+ r: parseIntFromHex(m1),
1624
+ g: parseIntFromHex(m2),
1625
+ b: parseIntFromHex(m3),
1626
+ a: convertHexToDecimal(m4),
1627
+ // format: named ? 'rgb' : 'hex8',
1628
+ format: named ? 'rgb' : 'hex',
1629
+ };
1630
+ }
1631
+ [, m1, m2, m3] = matchers.hex6.exec(color) || [];
1632
+ if (m1 && m2 && m3) {
1633
+ return {
1634
+ r: parseIntFromHex(m1),
1635
+ g: parseIntFromHex(m2),
1636
+ b: parseIntFromHex(m3),
1637
+ format: named ? 'rgb' : 'hex',
1638
+ };
1639
+ }
1640
+ [, m1, m2, m3, m4] = matchers.hex4.exec(color) || [];
1641
+ if (m1 && m2 && m3 && m4) {
1642
+ return {
1643
+ r: parseIntFromHex(m1 + m1),
1644
+ g: parseIntFromHex(m2 + m2),
1645
+ b: parseIntFromHex(m3 + m3),
1646
+ a: convertHexToDecimal(m4 + m4),
1647
+ // format: named ? 'rgb' : 'hex8',
1648
+ format: named ? 'rgb' : 'hex',
1649
+ };
1650
+ }
1651
+ [, m1, m2, m3] = matchers.hex3.exec(color) || [];
1652
+ if (m1 && m2 && m3) {
1653
+ return {
1654
+ r: parseIntFromHex(m1 + m1),
1655
+ g: parseIntFromHex(m2 + m2),
1656
+ b: parseIntFromHex(m3 + m3),
1657
+ format: named ? 'rgb' : 'hex',
1658
+ };
1659
+ }
1660
+ return false;
1661
+ }
1662
+
1663
+ /**
1664
+ * Given a string or object, convert that input to RGB
1665
+ *
1666
+ * Possible string inputs:
1667
+ * ```
1668
+ * "red"
1669
+ * "#f00" or "f00"
1670
+ * "#ff0000" or "ff0000"
1671
+ * "#ff000000" or "ff000000" // CSS4 Module
1672
+ * "rgb 255 0 0" or "rgb (255, 0, 0)"
1673
+ * "rgb 1.0 0 0" or "rgb (1, 0, 0)"
1674
+ * "rgba(255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
1675
+ * "rgba(1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
1676
+ * "rgb(255 0 0 / 10%)" or "rgb 255 0 0 0.1" // CSS4 Module
1677
+ * "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
1678
+ * "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
1679
+ * "hsl(0deg 100% 50% / 50%)" or "hsl 0 100 50 50" // CSS4 Module
1680
+ * "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
1681
+ * "hsva(0, 100%, 100%, 0.1)" or "hsva 0 100% 100% 0.1"
1682
+ * "hsv(0deg 100% 100% / 10%)" or "hsv 0 100 100 0.1" // CSS4 Module
1683
+ * "hwb(0deg, 100%, 100%, 100%)" or "hwb 0 100% 100% 0.1" // CSS4 Module
1684
+ * ```
1685
+ * @param {string | Record<string, any>} input
1686
+ * @returns {CP.ColorObject}
1687
+ */
1688
+ function inputToRGB(input) {
1689
+ let rgb = { r: 0, g: 0, b: 0 };
1690
+ let color = input;
1691
+ let a = 1;
1692
+ let s = null;
1693
+ let v = null;
1694
+ let l = null;
1695
+ let w = null;
1696
+ let b = null;
1697
+ let h = null;
1698
+ let r = null;
1699
+ let g = null;
1700
+ let ok = false;
1701
+ let format = 'hex';
1702
+
1703
+ if (typeof input === 'string') {
1704
+ // @ts-ignore -- this now is converted to object
1705
+ color = stringInputToObject(input);
1706
+ if (color) ok = true;
1707
+ }
1708
+ if (typeof color === 'object') {
1709
+ if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
1710
+ ({ r, g, b } = color);
1711
+ // RGB values now are all in [0, 255] range
1712
+ [r, g, b] = [r, g, b].map((n) => bound01(n, isPercentage(n) ? 100 : 255) * 255);
1713
+ rgb = { r, g, b };
1714
+ ok = true;
1715
+ format = 'rgb';
1716
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
1717
+ ({ h, s, v } = color);
1718
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1719
+ s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1720
+ v = typeof v === 'number' ? v : bound01(v, 100); // brightness can be `5%` or a [0, 1] value
1721
+ rgb = hsvToRgb(h, s, v);
1722
+ ok = true;
1723
+ format = 'hsv';
1724
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
1725
+ ({ h, s, l } = color);
1726
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1727
+ s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1728
+ l = typeof l === 'number' ? l : bound01(l, 100); // lightness can be `5%` or a [0, 1] value
1729
+ rgb = hslToRgb(h, s, l);
1730
+ ok = true;
1731
+ format = 'hsl';
1732
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.w) && isValidCSSUnit(color.b)) {
1733
+ ({ h, w, b } = color);
1734
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1735
+ w = typeof w === 'number' ? w : bound01(w, 100); // whiteness can be `5%` or a [0, 1] value
1736
+ b = typeof b === 'number' ? b : bound01(b, 100); // blackness can be `5%` or a [0, 1] value
1737
+ rgb = hwbToRgb(h, w, b);
1738
+ ok = true;
1739
+ format = 'hwb';
1740
+ }
1741
+ if (isValidCSSUnit(color.a)) {
1742
+ a = color.a;
1743
+ a = isPercentage(`${a}`) ? bound01(a, 100) : a;
1744
+ }
1745
+ }
1746
+
1747
+ return {
1748
+ ok, // @ts-ignore
1749
+ format: color.format || format,
1750
+ r: Math.min(255, Math.max(rgb.r, 0)),
1751
+ g: Math.min(255, Math.max(rgb.g, 0)),
1752
+ b: Math.min(255, Math.max(rgb.b, 0)),
1753
+ a: boundAlpha(a),
1754
+ };
1755
+ }
1756
+
1757
+ /**
1758
+ * @class
1759
+ * Returns a new `Color` instance.
1760
+ * @see https://github.com/bgrins/TinyColor
1761
+ */
1762
+ class Color {
1763
+ /**
1764
+ * @constructor
1765
+ * @param {CP.ColorInput} input the given colour value
1766
+ * @param {CP.ColorFormats=} config the given format
1767
+ */
1768
+ constructor(input, config) {
1769
+ let color = input;
1770
+ const configFormat = config && COLOR_FORMAT.includes(config)
1771
+ ? config : 'rgb';
1772
+
1773
+ // If input is already a `Color`, return itself
1774
+ if (color instanceof Color) {
1775
+ color = inputToRGB(color);
1776
+ }
1777
+ if (typeof color === 'number') {
1778
+ color = numberInputToObject(color);
1779
+ }
1780
+ const {
1781
+ r, g, b, a, ok, format,
1782
+ } = inputToRGB(color);
1783
+
1784
+ // bind
1785
+ const self = this;
1786
+
1787
+ /** @type {CP.ColorInput} */
1788
+ self.originalInput = color;
1789
+ /** @type {number} */
1790
+ self.r = r;
1791
+ /** @type {number} */
1792
+ self.g = g;
1793
+ /** @type {number} */
1794
+ self.b = b;
1795
+ /** @type {number} */
1796
+ self.a = a;
1797
+ /** @type {boolean} */
1798
+ self.ok = ok;
1799
+ /** @type {CP.ColorFormats} */
1800
+ self.format = configFormat || format;
1801
+ }
1802
+
1803
+ /**
1804
+ * Checks if the current input value is a valid colour.
1805
+ * @returns {boolean} the query result
1806
+ */
1807
+ get isValid() {
1808
+ return this.ok;
1809
+ }
1810
+
1811
+ /**
1812
+ * Checks if the current colour requires a light text colour.
1813
+ * @returns {boolean} the query result
1814
+ */
1815
+ get isDark() {
1816
+ return this.brightness < 120;
1817
+ }
1818
+
1819
+ /**
1820
+ * Returns the perceived luminance of a colour.
1821
+ * @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
1822
+ * @returns {number} a number in the [0, 1] range
1823
+ */
1824
+ get luminance() {
1825
+ const { r, g, b } = this;
1826
+ let R = 0;
1827
+ let G = 0;
1828
+ let B = 0;
1829
+ const rp = r / 255;
1830
+ const rg = g / 255;
1831
+ const rb = b / 255;
1832
+
1833
+ if (rp <= 0.03928) {
1834
+ R = rp / 12.92;
1835
+ } else {
1836
+ R = ((rp + 0.055) / 1.055) ** 2.4;
1837
+ }
1838
+ if (rg <= 0.03928) {
1839
+ G = rg / 12.92;
1840
+ } else {
1841
+ G = ((rg + 0.055) / 1.055) ** 2.4;
1842
+ }
1843
+ if (rb <= 0.03928) {
1844
+ B = rb / 12.92;
1845
+ } else {
1846
+ B = ((rb + 0.055) / 1.055) ** 2.4;
1847
+ }
1848
+ return 0.2126 * R + 0.7152 * G + 0.0722 * B;
1849
+ }
1850
+
1851
+ /**
1852
+ * Returns the perceived brightness of the colour.
1853
+ * @returns {number} a number in the [0, 255] range
1854
+ */
1855
+ get brightness() {
1856
+ const { r, g, b } = this;
1857
+ return (r * 299 + g * 587 + b * 114) / 1000;
1858
+ }
1859
+
1860
+ /**
1861
+ * Returns the colour as an RGBA object.
1862
+ * @returns {CP.RGBA} an {r,g,b,a} object with [0, 255] ranged values
1863
+ */
1864
+ toRgb() {
1865
+ const {
1866
+ r, g, b, a,
1867
+ } = this;
1868
+
1869
+ return {
1870
+ r, g, b, a: roundPart(a * 100) / 100,
1871
+ };
1872
+ }
1873
+
1874
+ /**
1875
+ * Returns the RGBA values concatenated into a CSS3 Module string format.
1876
+ * * rgb(255,255,255)
1877
+ * * rgba(255,255,255,0.5)
1878
+ * @returns {string} the CSS valid colour in RGB/RGBA format
1879
+ */
1880
+ toRgbString() {
1881
+ const {
1882
+ r, g, b, a,
1883
+ } = this.toRgb();
1884
+ const [R, G, B] = [r, g, b].map(roundPart);
1885
+
1886
+ return a === 1
1887
+ ? `rgb(${R}, ${G}, ${B})`
1888
+ : `rgba(${R}, ${G}, ${B}, ${a})`;
1889
+ }
1890
+
1891
+ /**
1892
+ * Returns the RGBA values concatenated into a CSS4 Module string format.
1893
+ * * rgb(255 255 255)
1894
+ * * rgb(255 255 255 / 50%)
1895
+ * @returns {string} the CSS valid colour in CSS4 RGB format
1896
+ */
1897
+ toRgbCSS4String() {
1898
+ const {
1899
+ r, g, b, a,
1900
+ } = this.toRgb();
1901
+ const [R, G, B] = [r, g, b].map(roundPart);
1902
+ const A = a === 1 ? '' : ` / ${roundPart(a * 100)}%`;
1903
+
1904
+ return `rgb(${R} ${G} ${B}${A})`;
1905
+ }
1906
+
1907
+ /**
1908
+ * Returns the hexadecimal value of the colour. When the parameter is *true*
1909
+ * it will find a 3 characters shorthand of the decimal value.
1910
+ *
1911
+ * @param {boolean=} allow3Char when `true` returns shorthand HEX
1912
+ * @returns {string} the hexadecimal colour format
1913
+ */
1914
+ toHex(allow3Char) {
1915
+ const {
1916
+ r, g, b, a,
1917
+ } = this.toRgb();
1918
+
1919
+ return a === 1
1920
+ ? rgbToHex(r, g, b, allow3Char)
1921
+ : rgbaToHex(r, g, b, a, allow3Char);
1922
+ }
1923
+
1924
+ /**
1925
+ * Returns the CSS valid hexadecimal vaue of the colour. When the parameter is *true*
1926
+ * it will find a 3 characters shorthand of the value.
1927
+ *
1928
+ * @param {boolean=} allow3Char when `true` returns shorthand HEX
1929
+ * @returns {string} the CSS valid colour in hexadecimal format
1930
+ */
1931
+ toHexString(allow3Char) {
1932
+ return `#${this.toHex(allow3Char)}`;
1933
+ }
1934
+
1935
+ /**
1936
+ * Returns the HEX8 value of the colour.
1937
+ * @param {boolean=} allow4Char when `true` returns shorthand HEX
1938
+ * @returns {string} the CSS valid colour in hexadecimal format
1939
+ */
1940
+ toHex8(allow4Char) {
1941
+ const {
1942
+ r, g, b, a,
1943
+ } = this.toRgb();
1944
+
1945
+ return rgbaToHex(r, g, b, a, allow4Char);
1946
+ }
1947
+
1948
+ /**
1949
+ * Returns the HEX8 value of the colour.
1950
+ * @param {boolean=} allow4Char when `true` returns shorthand HEX
1951
+ * @returns {string} the CSS valid colour in hexadecimal format
1952
+ */
1953
+ toHex8String(allow4Char) {
1954
+ return `#${this.toHex8(allow4Char)}`;
1955
+ }
1956
+
1957
+ /**
1958
+ * Returns the colour as a HSVA object.
1959
+ * @returns {CP.HSVA} the `{h,s,v,a}` object with [0, 1] ranged values
1960
+ */
1961
+ toHsv() {
1962
+ const {
1963
+ r, g, b, a,
1964
+ } = this.toRgb();
1965
+ const { h, s, v } = rgbToHsv(r, g, b);
1966
+
1967
+ return {
1968
+ h, s, v, a,
1969
+ };
1970
+ }
1971
+
1972
+ /**
1973
+ * Returns the colour as an HSLA object.
1974
+ * @returns {CP.HSLA} the `{h,s,l,a}` object with [0, 1] ranged values
1975
+ */
1976
+ toHsl() {
1977
+ const {
1978
+ r, g, b, a,
1979
+ } = this.toRgb();
1980
+ const { h, s, l } = rgbToHsl(r, g, b);
1981
+
1982
+ return {
1983
+ h, s, l, a,
1984
+ };
1985
+ }
1986
+
1987
+ /**
1988
+ * Returns the HSLA values concatenated into a CSS3 Module format string.
1989
+ * * `hsl(150, 100%, 50%)`
1990
+ * * `hsla(150, 100%, 50%, 0.5)`
1991
+ * @returns {string} the CSS valid colour in HSL/HSLA format
1992
+ */
1993
+ toHslString() {
1994
+ let {
1995
+ h, s, l, a,
1996
+ } = this.toHsl();
1997
+ h = roundPart(h * 360);
1998
+ s = roundPart(s * 100);
1999
+ l = roundPart(l * 100);
2000
+ a = roundPart(a * 100) / 100;
2001
+
2002
+ return a === 1
2003
+ ? `hsl(${h}, ${s}%, ${l}%)`
2004
+ : `hsla(${h}, ${s}%, ${l}%, ${a})`;
2005
+ }
2006
+
2007
+ /**
2008
+ * Returns the HSLA values concatenated into a CSS4 Module format string.
2009
+ * * `hsl(150deg 100% 50%)`
2010
+ * * `hsl(150deg 100% 50% / 50%)`
2011
+ * @returns {string} the CSS valid colour in CSS4 HSL format
2012
+ */
2013
+ toHslCSS4String() {
2014
+ let {
2015
+ h, s, l, a,
2016
+ } = this.toHsl();
2017
+ h = roundPart(h * 360);
2018
+ s = roundPart(s * 100);
2019
+ l = roundPart(l * 100);
2020
+ a = roundPart(a * 100);
2021
+ const A = a < 100 ? ` / ${roundPart(a)}%` : '';
2022
+
2023
+ return `hsl(${h}deg ${s}% ${l}%${A})`;
2024
+ }
2025
+
2026
+ /**
2027
+ * Returns the colour as an HWBA object.
2028
+ * @returns {CP.HWBA} the `{h,w,b,a}` object with [0, 1] ranged values
2029
+ */
2030
+ toHwb() {
2031
+ const {
2032
+ r, g, b, a,
2033
+ } = this;
2034
+ const { h, w, b: bl } = rgbToHwb(r, g, b);
2035
+ return {
2036
+ h, w, b: bl, a,
2037
+ };
2038
+ }
2039
+
2040
+ /**
2041
+ * Returns the HWBA values concatenated into a string.
2042
+ * @returns {string} the CSS valid colour in HWB format
2043
+ */
2044
+ toHwbString() {
2045
+ let {
2046
+ h, w, b, a,
2047
+ } = this.toHwb();
2048
+ h = roundPart(h * 360);
2049
+ w = roundPart(w * 100);
2050
+ b = roundPart(b * 100);
2051
+ a = roundPart(a * 100);
2052
+ const A = a < 100 ? ` / ${roundPart(a)}%` : '';
2053
+
2054
+ return `hwb(${h}deg ${w}% ${b}%${A})`;
2055
+ }
2056
+
2057
+ /**
2058
+ * Sets the alpha value of the current colour.
2059
+ * @param {number} alpha a new alpha value in the [0, 1] range.
2060
+ * @returns {Color} the `Color` instance
2061
+ */
2062
+ setAlpha(alpha) {
2063
+ const self = this;
2064
+ self.a = boundAlpha(alpha);
2065
+ return self;
2066
+ }
2067
+
2068
+ /**
2069
+ * Saturate the colour with a given amount.
2070
+ * @param {number=} amount a value in the [0, 100] range
2071
+ * @returns {Color} the `Color` instance
2072
+ */
2073
+ saturate(amount) {
2074
+ const self = this;
2075
+ if (typeof amount !== 'number') return self;
2076
+ const { h, s, l } = self.toHsl();
2077
+ const { r, g, b } = hslToRgb(h, clamp01(s + amount / 100), l);
2078
+
2079
+ ObjectAssign(self, { r, g, b });
2080
+ return self;
2081
+ }
2082
+
2083
+ /**
2084
+ * Desaturate the colour with a given amount.
2085
+ * @param {number=} amount a value in the [0, 100] range
2086
+ * @returns {Color} the `Color` instance
2087
+ */
2088
+ desaturate(amount) {
2089
+ return typeof amount === 'number' ? this.saturate(-amount) : this;
2090
+ }
2091
+
2092
+ /**
2093
+ * Completely desaturates a colour into greyscale.
2094
+ * Same as calling `desaturate(100)`
2095
+ * @returns {Color} the `Color` instance
2096
+ */
2097
+ greyscale() {
2098
+ return this.saturate(-100);
2099
+ }
2100
+
2101
+ /**
2102
+ * Increase the colour lightness with a given amount.
2103
+ * @param {number=} amount a value in the [0, 100] range
2104
+ * @returns {Color} the `Color` instance
2105
+ */
2106
+ lighten(amount) {
2107
+ const self = this;
2108
+ if (typeof amount !== 'number') return self;
2109
+
2110
+ const { h, s, l } = self.toHsl();
2111
+ const { r, g, b } = hslToRgb(h, s, clamp01(l + amount / 100));
2112
+
2113
+ ObjectAssign(self, { r, g, b });
2114
+ return self;
2115
+ }
2116
+
2117
+ /**
2118
+ * Decrease the colour lightness with a given amount.
2119
+ * @param {number=} amount a value in the [0, 100] range
2120
+ * @returns {Color} the `Color` instance
2121
+ */
2122
+ darken(amount) {
2123
+ return typeof amount === 'number' ? this.lighten(-amount) : this;
2124
+ }
2125
+
2126
+ /**
2127
+ * Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
2128
+ * Values outside of this range will be wrapped into this range.
2129
+ *
2130
+ * @param {number=} amount a value in the [0, 100] range
2131
+ * @returns {Color} the `Color` instance
2132
+ */
2133
+ spin(amount) {
2134
+ const self = this;
2135
+ if (typeof amount !== 'number') return self;
2136
+
2137
+ const { h, s, l } = self.toHsl();
2138
+ const { r, g, b } = hslToRgb(clamp01(((h * 360 + amount) % 360) / 360), s, l);
2139
+
2140
+ ObjectAssign(self, { r, g, b });
2141
+ return self;
2142
+ }
2143
+
2144
+ /** Returns a clone of the current `Color` instance. */
2145
+ clone() {
2146
+ return new Color(this);
2147
+ }
2148
+
2149
+ /**
2150
+ * Returns the colour value in CSS valid string format.
2151
+ * @param {boolean=} allowShort when *true*, HEX values can be shorthand
2152
+ * @returns {string} the CSS valid colour in the configured format
2153
+ */
2154
+ toString(allowShort) {
2155
+ const self = this;
2156
+ const { format } = self;
2157
+
2158
+ if (format === 'hex') return self.toHexString(allowShort);
2159
+ if (format === 'hsl') return self.toHslString();
2160
+ if (format === 'hwb') return self.toHwbString();
2161
+
2162
+ return self.toRgbString();
2163
+ }
2164
+ }
2165
+
2166
+ ObjectAssign(Color, {
2167
+ ANGLES,
2168
+ CSS_ANGLE,
2169
+ CSS_INTEGER,
2170
+ CSS_NUMBER,
2171
+ CSS_UNIT,
2172
+ CSS_UNIT2,
2173
+ PERMISSIVE_MATCH,
2174
+ matchers,
2175
+ isOnePointZero,
2176
+ isPercentage,
2177
+ isValidCSSUnit,
2178
+ pad2,
2179
+ clamp01,
2180
+ bound01,
2181
+ boundAlpha,
2182
+ getRGBFromName,
2183
+ convertHexToDecimal,
2184
+ convertDecimalToHex,
2185
+ rgbToHsl,
2186
+ rgbToHex,
2187
+ rgbToHsv,
2188
+ rgbToHwb,
2189
+ rgbaToHex,
2190
+ hslToRgb,
2191
+ hsvToRgb,
2192
+ hueToRgb,
2193
+ hwbToRgb,
2194
+ parseIntFromHex,
2195
+ numberInputToObject,
2196
+ stringInputToObject,
2197
+ inputToRGB,
2198
+ roundPart,
2199
+ ObjectAssign,
2200
+ });
2201
+
2202
+ /**
2203
+ * @class
2204
+ * Returns a color palette with a given set of parameters.
2205
+ * @example
2206
+ * new ColorPalette(0, 12, 10);
2207
+ * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: array }
2208
+ */
2209
+ class ColorPalette {
2210
+ /**
2211
+ * The `hue` parameter is optional, which would be set to 0.
2212
+ * @param {number[]} args represeinting hue, hueSteps, lightSteps
2213
+ * * `args.hue` the starting Hue [0, 360]
2214
+ * * `args.hueSteps` Hue Steps Count [5, 24]
2215
+ * * `args.lightSteps` Lightness Steps Count [5, 12]
2216
+ */
2217
+ constructor(...args) {
2218
+ let hue = 0;
2219
+ let hueSteps = 12;
2220
+ let lightSteps = 10;
2221
+ let lightnessArray = [0.5];
2222
+
2223
+ if (args.length === 3) {
2224
+ [hue, hueSteps, lightSteps] = args;
2225
+ } else if (args.length === 2) {
2226
+ [hueSteps, lightSteps] = args;
2227
+ } else {
2228
+ throw TypeError('ColorPalette requires minimum 2 arguments');
2229
+ }
2230
+
2231
+ /** @type {string[]} */
2232
+ const colors = [];
2233
+
2234
+ const hueStep = 360 / hueSteps;
2235
+ const half = roundPart((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
2236
+ const estimatedStep = 100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100;
2237
+
2238
+ let lightStep = 0.25;
2239
+ lightStep = [4, 5].includes(lightSteps) ? 0.2 : lightStep;
2240
+ lightStep = [6, 7].includes(lightSteps) ? 0.15 : lightStep;
2241
+ lightStep = [8, 9].includes(lightSteps) ? 0.11 : lightStep;
2242
+ lightStep = [10, 11].includes(lightSteps) ? 0.09 : lightStep;
2243
+ lightStep = [12, 13].includes(lightSteps) ? 0.075 : lightStep;
2244
+ lightStep = lightSteps > 13 ? estimatedStep : lightStep;
2245
+
2246
+ // light tints
2247
+ for (let i = 1; i < half + 1; i += 1) {
2248
+ lightnessArray = [...lightnessArray, (0.5 + lightStep * (i))];
2249
+ }
2250
+
2251
+ // dark tints
2252
+ for (let i = 1; i < lightSteps - half; i += 1) {
2253
+ lightnessArray = [(0.5 - lightStep * (i)), ...lightnessArray];
2254
+ }
2255
+
2256
+ // feed `colors` Array
2257
+ for (let i = 0; i < hueSteps; i += 1) {
2258
+ const currentHue = ((hue + i * hueStep) % 360) / 360;
2259
+ lightnessArray.forEach((l) => {
2260
+ colors.push(new Color({ h: currentHue, s: 1, l }).toHexString());
2261
+ });
2262
+ }
2263
+
2264
+ this.hue = hue;
2265
+ this.hueSteps = hueSteps;
2266
+ this.lightSteps = lightSteps;
2267
+ this.colors = colors;
2268
+ }
2269
+ }
2270
+
2271
+ /**
2272
+ * Returns a color-defaults with given values and class.
2273
+ * @param {CP.ColorPicker} self
2274
+ * @param {CP.ColorPalette | string[]} colorsSource
2275
+ * @param {string} menuClass
2276
+ * @returns {HTMLElement | Element}
2277
+ */
2278
+ function getColorMenu(self, colorsSource, menuClass) {
2279
+ const { input, format, componentLabels } = self;
2280
+ const { defaultsLabel, presetsLabel } = componentLabels;
2281
+ const isOptionsMenu = menuClass === 'color-options';
2282
+ const isPalette = colorsSource instanceof ColorPalette;
2283
+ const menuLabel = isOptionsMenu ? presetsLabel : defaultsLabel;
2284
+ let colorsArray = isPalette ? colorsSource.colors : colorsSource;
2285
+ colorsArray = colorsArray instanceof Array ? colorsArray : [];
2286
+ const colorsCount = colorsArray.length;
2287
+ const { lightSteps } = isPalette ? colorsSource : { lightSteps: null };
2288
+ const fit = lightSteps || [9, 10].find((x) => colorsCount > x * 2 && !(colorsCount % x)) || 5;
2289
+ const isMultiLine = isOptionsMenu && colorsCount > fit;
2290
+ let rowCountHover = 2;
2291
+ rowCountHover = isMultiLine && colorsCount >= fit * 2 ? 3 : rowCountHover;
2292
+ rowCountHover = colorsCount >= fit * 3 ? 4 : rowCountHover;
2293
+ rowCountHover = colorsCount >= fit * 4 ? 5 : rowCountHover;
2294
+ const rowCount = rowCountHover - (colorsCount < fit * 3 ? 1 : 2);
2295
+ const isScrollable = isMultiLine && colorsCount > rowCount * fit;
2296
+ let finalClass = menuClass;
2297
+ finalClass += isScrollable ? ' scrollable' : '';
2298
+ finalClass += isMultiLine ? ' multiline' : '';
2299
+ const gap = isMultiLine ? '1px' : '0.25rem';
2300
+ let optionSize = isMultiLine ? 1.75 : 2;
2301
+ optionSize = fit > 5 && isMultiLine ? 1.5 : optionSize;
2302
+ const menuHeight = `${(rowCount || 1) * optionSize}rem`;
2303
+ const menuHeightHover = `calc(${rowCountHover} * ${optionSize}rem + ${rowCountHover - 1} * ${gap})`;
2304
+
2305
+ const menu = createElement({
2306
+ tagName: 'ul',
2307
+ className: finalClass,
2308
+ });
2309
+ setAttribute(menu, 'role', 'listbox');
2310
+ setAttribute(menu, ariaLabel, menuLabel);
2311
+
2312
+ if (isScrollable) { // @ts-ignore
2313
+ setCSSProperties(menu, {
2314
+ '--grid-item-size': `${optionSize}rem`,
2315
+ '--grid-fit': fit,
2316
+ '--grid-gap': gap,
2317
+ '--grid-height': menuHeight,
2318
+ '--grid-hover-height': menuHeightHover,
2319
+ });
2320
+ }
2321
+
2322
+ colorsArray.forEach((x) => {
2323
+ const [value, label] = x.trim().split(':');
2324
+ const xRealColor = new Color(value, format).toString();
2325
+ const isActive = xRealColor === getAttribute(input, 'value');
2326
+ const active = isActive ? ' active' : '';
2327
+
2328
+ const option = createElement({
2329
+ tagName: 'li',
2330
+ className: `color-option${active}`,
2331
+ innerText: `${label || x}`,
2332
+ });
2333
+
2334
+ setAttribute(option, tabIndex, '0');
2335
+ setAttribute(option, 'data-value', `${value}`);
2336
+ setAttribute(option, 'role', 'option');
2337
+ setAttribute(option, ariaSelected, isActive ? 'true' : 'false');
2338
+
2339
+ if (isOptionsMenu) {
2340
+ setElementStyle(option, { backgroundColor: x });
2341
+ }
2342
+
2343
+ menu.append(option);
2344
+ });
2345
+ return menu;
2346
+ }
2347
+
2348
+ /**
2349
+ * Check if a string is valid JSON string.
2350
+ * @param {string} str the string input
2351
+ * @returns {boolean} the query result
2352
+ */
2353
+ function isValidJSON(str) {
2354
+ try {
2355
+ JSON.parse(str);
2356
+ } catch (e) {
2357
+ return false;
2358
+ }
2359
+ return true;
2360
+ }
2361
+
2362
+ var version = "0.0.1";
2363
+
2364
+ // @ts-ignore
2365
+
2366
+ const Version = version;
2367
+
2368
+ // ColorPicker GC
2369
+ // ==============
2370
+ const colorPickerString = 'color-picker';
2371
+ const colorPickerSelector = `[data-function="${colorPickerString}"]`;
2372
+ const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2373
+ const colorPickerDefaults = {
2374
+ componentLabels: colorPickerLabels,
2375
+ colorLabels: colorNames,
2376
+ format: 'rgb',
2377
+ colorPresets: false,
2378
+ colorKeywords: false,
2379
+ };
2380
+
2381
+ // ColorPicker Static Methods
2382
+ // ==========================
2383
+
2384
+ /** @type {CP.GetInstance<ColorPicker>} */
2385
+ const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
2386
+
2387
+ /** @type {CP.InitCallback<ColorPicker>} */
2388
+ const initColorPicker = (element) => new ColorPicker(element);
2389
+
2390
+ // ColorPicker Private Methods
2391
+ // ===========================
2392
+
2393
+ /**
2394
+ * Generate HTML markup and update instance properties.
2395
+ * @param {ColorPicker} self
2396
+ */
2397
+ function initCallback(self) {
2398
+ const {
2399
+ input, parent, format, id, componentLabels, colorKeywords, colorPresets,
2400
+ } = self;
2401
+ const colorValue = getAttribute(input, 'value') || '#fff';
2402
+
2403
+ const {
2404
+ toggleLabel, pickerLabel, formatLabel, hexLabel,
2405
+ } = componentLabels;
2406
+
2407
+ // update color
2408
+ const color = nonColors.includes(colorValue) ? '#fff' : colorValue;
2409
+ self.color = new Color(color, format);
2410
+
2411
+ // set initial controls dimensions
2412
+ // make the controls smaller on mobile
2413
+ const dropClass = isMobile ? ' mobile' : '';
2414
+ const formatString = format === 'hex' ? hexLabel : format.toUpperCase();
2415
+
2416
+ const pickerBtn = createElement({
2417
+ id: `picker-btn-${id}`,
2418
+ tagName: 'button',
2419
+ className: 'picker-toggle btn-appearance',
2420
+ });
2421
+ setAttribute(pickerBtn, ariaExpanded, 'false');
2422
+ setAttribute(pickerBtn, ariaHasPopup, 'true');
2423
+ pickerBtn.append(createElement({
2424
+ tagName: 'span',
2425
+ className: vHidden,
2426
+ innerText: `${pickerLabel}. ${formatLabel}: ${formatString}`,
2427
+ }));
2428
+
2429
+ const pickerDropdown = createElement({
2430
+ tagName: 'div',
2431
+ className: `color-dropdown picker${dropClass}`,
2432
+ });
2433
+ setAttribute(pickerDropdown, ariaLabelledBy, `picker-btn-${id}`);
2434
+ setAttribute(pickerDropdown, 'role', 'group');
2435
+
2436
+ const colorControls = getColorControls(self);
2437
+ const colorForm = getColorForm(self);
2438
+
2439
+ pickerDropdown.append(colorControls, colorForm);
2440
+ input.before(pickerBtn);
2441
+ parent.append(pickerDropdown);
2442
+
2443
+ // set colour key menu template
2444
+ if (colorKeywords || colorPresets) {
2445
+ const presetsDropdown = createElement({
2446
+ tagName: 'div',
2447
+ className: `color-dropdown scrollable menu${dropClass}`,
2448
+ });
2449
+
2450
+ // color presets
2451
+ if ((colorPresets instanceof Array && colorPresets.length)
2452
+ || (colorPresets instanceof ColorPalette && colorPresets.colors)) {
2453
+ const presetsMenu = getColorMenu(self, colorPresets, 'color-options');
2454
+ presetsDropdown.append(presetsMenu);
2455
+ }
2456
+
2457
+ // explicit defaults [reset, initial, inherit, transparent, currentColor]
2458
+ if (colorKeywords && colorKeywords.length) {
2459
+ const keywordsMenu = getColorMenu(self, colorKeywords, 'color-defaults');
2460
+ presetsDropdown.append(keywordsMenu);
2461
+ }
2462
+
2463
+ const presetsBtn = createElement({
2464
+ tagName: 'button',
2465
+ className: 'menu-toggle btn-appearance',
2466
+ });
2467
+ setAttribute(presetsBtn, tabIndex, '-1');
2468
+ setAttribute(presetsBtn, ariaExpanded, 'false');
2469
+ setAttribute(presetsBtn, ariaHasPopup, 'true');
2470
+
2471
+ const xmlns = encodeURI('http://www.w3.org/2000/svg');
2472
+ const presetsIcon = createElementNS(xmlns, { tagName: 'svg' });
2473
+ setAttribute(presetsIcon, 'xmlns', xmlns);
2474
+ setAttribute(presetsIcon, 'viewBox', '0 0 512 512');
2475
+ setAttribute(presetsIcon, ariaHidden, 'true');
2476
+
2477
+ const path = createElementNS(xmlns, { tagName: 'path' });
2478
+ setAttribute(path, 'd', 'M98,158l157,156L411,158l27,27L255,368L71,185L98,158z');
2479
+ setAttribute(path, 'fill', '#fff');
2480
+ presetsIcon.append(path);
2481
+ presetsBtn.append(createElement({
2482
+ tagName: 'span',
2483
+ className: vHidden,
2484
+ innerText: `${toggleLabel}`,
2485
+ }), presetsIcon);
2486
+
2487
+ parent.append(presetsBtn, presetsDropdown);
2488
+ }
2489
+
2490
+ // solve non-colors after settings save
2491
+ if (colorKeywords && nonColors.includes(colorValue)) {
2492
+ self.value = colorValue;
2493
+ }
2494
+ setAttribute(input, tabIndex, '-1');
2495
+ }
2496
+
2497
+ /**
2498
+ * Add / remove `ColorPicker` main event listeners.
2499
+ * @param {ColorPicker} self
2500
+ * @param {boolean=} action
2501
+ */
2502
+ function toggleEvents(self, action) {
2503
+ const fn = action ? addListener : removeListener;
2504
+ const { input, pickerToggle, menuToggle } = self;
2505
+
2506
+ fn(input, focusinEvent, self.showPicker);
2507
+ fn(pickerToggle, mouseclickEvent, self.togglePicker);
2508
+
2509
+ fn(input, keydownEvent, self.keyToggle);
2510
+
2511
+ if (menuToggle) {
2512
+ fn(menuToggle, mouseclickEvent, self.toggleMenu);
2513
+ }
2514
+ }
2515
+
2516
+ /**
2517
+ * Add / remove `ColorPicker` event listeners active only when open.
2518
+ * @param {ColorPicker} self
2519
+ * @param {boolean=} action
2520
+ */
2521
+ function toggleEventsOnShown(self, action) {
2522
+ const fn = action ? addListener : removeListener;
2523
+ const { input, colorMenu, parent } = self;
2524
+ const doc = getDocument(input);
2525
+ const win = getWindow(input);
2526
+ const pointerEvents = `on${touchstartEvent}` in doc
2527
+ ? { down: touchstartEvent, move: touchmoveEvent, up: touchendEvent }
2528
+ : { down: mousedownEvent, move: mousemoveEvent, up: mouseupEvent };
2529
+
2530
+ fn(self.controls, pointerEvents.down, self.pointerDown);
2531
+ self.controlKnobs.forEach((x) => fn(x, keydownEvent, self.handleKnobs));
2532
+
2533
+ // @ts-ignore -- this is `Window`
2534
+ fn(win, scrollEvent, self.handleScroll);
2535
+ // @ts-ignore -- this is `Window`
2536
+ fn(win, resizeEvent, self.update);
2537
+
2538
+ [input, ...self.inputs].forEach((x) => fn(x, changeEvent, self.changeHandler));
2539
+
2540
+ if (colorMenu) {
2541
+ fn(colorMenu, mouseclickEvent, self.menuClickHandler);
2542
+ fn(colorMenu, keydownEvent, self.menuKeyHandler);
2543
+ }
2544
+
2545
+ fn(doc, pointerEvents.move, self.pointerMove);
2546
+ fn(doc, pointerEvents.up, self.pointerUp);
2547
+ fn(parent, focusoutEvent, self.handleFocusOut);
2548
+ // @ts-ignore -- this is `Window`
2549
+ fn(win, keyupEvent, self.handleDismiss);
2550
+ }
2551
+
2552
+ /**
2553
+ * Triggers the `ColorPicker` original event.
2554
+ * @param {ColorPicker} self
2555
+ */
2556
+ function firePickerChange(self) {
2557
+ dispatchEvent(self.input, new CustomEvent('colorpicker.change'));
2558
+ }
2559
+
2560
+ /**
2561
+ * Hides a visible dropdown.
2562
+ * @param {HTMLElement} element
2563
+ * @returns {void}
2564
+ */
2565
+ function removePosition(element) {
2566
+ if (element) {
2567
+ ['bottom', 'top'].forEach((x) => removeClass(element, x));
2568
+ }
2569
+ }
2570
+
2571
+ /**
2572
+ * Shows a `ColorPicker` dropdown and close the curent open dropdown.
2573
+ * @param {ColorPicker} self
2574
+ * @param {HTMLElement | Element} dropdown
2575
+ */
2576
+ function showDropdown(self, dropdown) {
2577
+ const {
2578
+ colorPicker, colorMenu, menuToggle, pickerToggle, parent,
2579
+ } = self;
2580
+ const isPicker = dropdown === colorPicker;
2581
+ const openDropdown = isPicker ? colorMenu : colorPicker;
2582
+ const activeBtn = isPicker ? menuToggle : pickerToggle;
2583
+ const nextBtn = !isPicker ? menuToggle : pickerToggle;
2584
+
2585
+ if (!hasClass(parent, 'open')) {
2586
+ addClass(parent, 'open');
2587
+ }
2588
+ if (openDropdown) {
2589
+ removeClass(openDropdown, 'show');
2590
+ removePosition(openDropdown);
2591
+ }
2592
+ addClass(dropdown, 'bottom');
2593
+ reflow(dropdown);
2594
+ addClass(dropdown, 'show');
2595
+
2596
+ if (isPicker) self.update();
2597
+
2598
+ if (!self.isOpen) {
2599
+ toggleEventsOnShown(self, true);
2600
+ self.updateDropdownPosition();
2601
+ self.isOpen = true;
2602
+ setAttribute(self.input, tabIndex, '0');
2603
+ if (menuToggle) {
2604
+ setAttribute(menuToggle, tabIndex, '0');
2605
+ }
2606
+ }
2607
+
2608
+ setAttribute(nextBtn, ariaExpanded, 'true');
2609
+ if (activeBtn) {
2610
+ setAttribute(activeBtn, ariaExpanded, 'false');
2611
+ }
2612
+ }
2613
+
2614
+ /**
2615
+ * Color Picker Web Component
2616
+ * @see http://thednp.github.io/color-picker
2617
+ */
2618
+ class ColorPicker {
2619
+ /**
2620
+ * Returns a new `ColorPicker` instance. The target of this constructor
2621
+ * must be an `HTMLInputElement`.
2622
+ *
2623
+ * @param {HTMLInputElement | string} target the target `<input>` element
2624
+ * @param {CP.ColorPickerOptions=} config instance options
2625
+ */
2626
+ constructor(target, config) {
2627
+ const self = this;
2628
+ /** @type {HTMLInputElement} */
2629
+ // @ts-ignore
2630
+ const input = querySelector(target);
2631
+
2632
+ // invalidate
2633
+ if (!input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
2634
+ self.input = input;
2635
+
2636
+ const parent = closest(input, colorPickerParentSelector);
2637
+ if (!parent) throw new TypeError('ColorPicker requires a specific markup to work.');
2638
+
2639
+ /** @type {HTMLElement} */
2640
+ // @ts-ignore
2641
+ self.parent = parent;
2642
+
2643
+ /** @type {number} */
2644
+ self.id = getUID(input, colorPickerString);
2645
+
2646
+ // set initial state
2647
+ /** @type {HTMLElement?} */
2648
+ self.dragElement = null;
2649
+ /** @type {boolean} */
2650
+ self.isOpen = false;
2651
+ /** @type {Record<string, number>} */
2652
+ self.controlPositions = {
2653
+ c1x: 0, c1y: 0, c2y: 0, c3y: 0,
2654
+ };
2655
+ /** @type {Record<string, string>} */
2656
+ self.colorLabels = {};
2657
+ /** @type {string[]=} */
2658
+ self.colorKeywords = undefined;
2659
+ /** @type {(ColorPalette | string[])=} */
2660
+ self.colorPresets = undefined;
2661
+
2662
+ // process options
2663
+ const {
2664
+ format, componentLabels, colorLabels, colorKeywords, colorPresets,
2665
+ } = normalizeOptions(this.isCE ? parent : input, colorPickerDefaults, config || {});
2666
+
2667
+ let translatedColorLabels = colorNames;
2668
+ if (colorLabels instanceof Array && colorLabels.length === 17) {
2669
+ translatedColorLabels = colorLabels;
2670
+ } else if (colorLabels && colorLabels.split(',').length === 17) {
2671
+ translatedColorLabels = colorLabels.split(',');
2672
+ }
2673
+
2674
+ // expose colour labels to all methods
2675
+ colorNames.forEach((c, i) => {
2676
+ self.colorLabels[c] = translatedColorLabels[i].trim();
2677
+ });
2678
+
2679
+ // update and expose component labels
2680
+ const tempLabels = ObjectAssign({}, colorPickerLabels);
2681
+ const jsonLabels = componentLabels && isValidJSON(componentLabels)
2682
+ ? JSON.parse(componentLabels) : componentLabels || {};
2683
+
2684
+ /** @type {Record<string, string>} */
2685
+ self.componentLabels = ObjectAssign(tempLabels, jsonLabels);
2686
+
2687
+ /** @type {Color} */
2688
+ self.color = new Color('white', format);
2689
+
2690
+ /** @type {CP.ColorFormats} */
2691
+ self.format = format;
2692
+
2693
+ // set colour defaults
2694
+ if (colorKeywords instanceof Array) {
2695
+ self.colorKeywords = colorKeywords;
2696
+ } else if (typeof colorKeywords === 'string' && colorKeywords.length) {
2697
+ self.colorKeywords = colorKeywords.split(',');
2698
+ }
2699
+
2700
+ // set colour presets
2701
+ if (colorPresets instanceof Array) {
2702
+ self.colorPresets = colorPresets;
2703
+ } else if (typeof colorPresets === 'string' && colorPresets.length) {
2704
+ if (isValidJSON(colorPresets)) {
2705
+ const { hue, hueSteps, lightSteps } = JSON.parse(colorPresets);
2706
+ self.colorPresets = new ColorPalette(hue, hueSteps, lightSteps);
2707
+ } else {
2708
+ self.colorPresets = colorPresets.split(',').map((x) => x.trim());
2709
+ }
2710
+ }
2711
+
2712
+ // bind events
2713
+ self.showPicker = self.showPicker.bind(self);
2714
+ self.togglePicker = self.togglePicker.bind(self);
2715
+ self.toggleMenu = self.toggleMenu.bind(self);
2716
+ self.menuClickHandler = self.menuClickHandler.bind(self);
2717
+ self.menuKeyHandler = self.menuKeyHandler.bind(self);
2718
+ self.pointerDown = self.pointerDown.bind(self);
2719
+ self.pointerMove = self.pointerMove.bind(self);
2720
+ self.pointerUp = self.pointerUp.bind(self);
2721
+ self.update = self.update.bind(self);
2722
+ self.handleScroll = self.handleScroll.bind(self);
2723
+ self.handleFocusOut = self.handleFocusOut.bind(self);
2724
+ self.changeHandler = self.changeHandler.bind(self);
2725
+ self.handleDismiss = self.handleDismiss.bind(self);
2726
+ self.keyToggle = self.keyToggle.bind(self);
2727
+ self.handleKnobs = self.handleKnobs.bind(self);
2728
+
2729
+ // generate markup
2730
+ initCallback(self);
2731
+
2732
+ const [colorPicker, colorMenu] = getElementsByClassName('color-dropdown', parent);
2733
+ // set main elements
2734
+ /** @type {HTMLElement} */
2735
+ // @ts-ignore
2736
+ self.pickerToggle = querySelector('.picker-toggle', parent);
2737
+ /** @type {HTMLElement} */
2738
+ // @ts-ignore
2739
+ self.menuToggle = querySelector('.menu-toggle', parent);
2740
+ /** @type {HTMLElement} */
2741
+ // @ts-ignore
2742
+ self.colorPicker = colorPicker;
2743
+ /** @type {HTMLElement} */
2744
+ // @ts-ignore
2745
+ self.colorMenu = colorMenu;
2746
+ /** @type {HTMLInputElement[]} */
2747
+ // @ts-ignore
2748
+ self.inputs = [...getElementsByClassName('color-input', parent)];
2749
+ const [controls] = getElementsByClassName('color-controls', parent);
2750
+ self.controls = controls;
2751
+ /** @type {(HTMLElement | Element)[]} */
2752
+ self.controlKnobs = [...getElementsByClassName('knob', controls)];
2753
+ /** @type {(HTMLElement)[]} */
2754
+ // @ts-ignore
2755
+ self.visuals = [...getElementsByClassName('visual-control', controls)];
2756
+
2757
+ // update colour picker controls, inputs and visuals
2758
+ self.update();
2759
+
2760
+ // add main events listeners
2761
+ toggleEvents(self, true);
2762
+
2763
+ // set component data
2764
+ Data.set(input, colorPickerString, self);
2765
+ }
2766
+
2767
+ /** Returns the current colour value */
2768
+ get value() { return this.input.value; }
2769
+
2770
+ /**
2771
+ * Sets a new colour value.
2772
+ * @param {string} v new colour value
2773
+ */
2774
+ set value(v) { this.input.value = v; }
2775
+
2776
+ /** Check if the colour presets include any non-colour. */
2777
+ get hasNonColor() {
2778
+ return this.colorKeywords instanceof Array
2779
+ && this.colorKeywords.some((x) => nonColors.includes(x));
2780
+ }
2781
+
2782
+ /** Check if the parent of the target is a `ColorPickerElement` instance. */
2783
+ get isCE() { return this.parent.localName === colorPickerString; }
2784
+
2785
+ /** Returns hexadecimal value of the current colour. */
2786
+ get hex() { return this.color.toHex(true); }
2787
+
2788
+ /** Returns the current colour value in {h,s,v,a} object format. */
2789
+ get hsv() { return this.color.toHsv(); }
2790
+
2791
+ /** Returns the current colour value in {h,s,l,a} object format. */
2792
+ get hsl() { return this.color.toHsl(); }
2793
+
2794
+ /** Returns the current colour value in {h,w,b,a} object format. */
2795
+ get hwb() { return this.color.toHwb(); }
2796
+
2797
+ /** Returns the current colour value in {r,g,b,a} object format. */
2798
+ get rgb() { return this.color.toRgb(); }
2799
+
2800
+ /** Returns the current colour brightness. */
2801
+ get brightness() { return this.color.brightness; }
2802
+
2803
+ /** Returns the current colour luminance. */
2804
+ get luminance() { return this.color.luminance; }
2805
+
2806
+ /** Checks if the current colour requires a light text colour. */
2807
+ get isDark() {
2808
+ const { color, brightness } = this;
2809
+ return brightness < 120 && color.a > 0.33;
2810
+ }
2811
+
2812
+ /** Checks if the current input value is a valid colour. */
2813
+ get isValid() {
2814
+ const inputValue = this.input.value;
2815
+ return inputValue !== '' && new Color(inputValue).isValid;
2816
+ }
2817
+
2818
+ /** Updates `ColorPicker` visuals. */
2819
+ updateVisuals() {
2820
+ const self = this;
2821
+ const {
2822
+ format, controlPositions, visuals,
2823
+ } = self;
2824
+ const [v1, v2, v3] = visuals;
2825
+ const { offsetWidth, offsetHeight } = v1;
2826
+ const hue = format === 'hsl'
2827
+ ? controlPositions.c1x / offsetWidth
2828
+ : controlPositions.c2y / offsetHeight;
2829
+ // @ts-ignore - `hslToRgb` is assigned to `Color` as static method
2830
+ const { r, g, b } = Color.hslToRgb(hue, 1, 0.5);
2831
+ const whiteGrad = 'linear-gradient(rgb(255,255,255) 0%, rgb(255,255,255) 100%)';
2832
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
2833
+ const roundA = roundPart((alpha * 100)) / 100;
2834
+
2835
+ if (format !== 'hsl') {
2836
+ const fill = new Color({
2837
+ h: hue, s: 1, l: 0.5, a: alpha,
2838
+ }).toRgbString();
2839
+ const hueGradient = `linear-gradient(
2840
+ rgb(255,0,0) 0%, rgb(255,255,0) 16.67%,
2841
+ rgb(0,255,0) 33.33%, rgb(0,255,255) 50%,
2842
+ rgb(0,0,255) 66.67%, rgb(255,0,255) 83.33%,
2843
+ rgb(255,0,0) 100%)`;
2844
+ setElementStyle(v1, {
2845
+ background: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,${roundA}) 100%),
2846
+ linear-gradient(to right, rgba(255,255,255,${roundA}) 0%, ${fill} 100%),
2847
+ ${whiteGrad}`,
2848
+ });
2849
+ setElementStyle(v2, { background: hueGradient });
2850
+ } else {
2851
+ const saturation = roundPart((controlPositions.c2y / offsetHeight) * 100);
2852
+ const fill0 = new Color({
2853
+ r: 255, g: 0, b: 0, a: alpha,
2854
+ }).saturate(-saturation).toRgbString();
2855
+ const fill1 = new Color({
2856
+ r: 255, g: 255, b: 0, a: alpha,
2857
+ }).saturate(-saturation).toRgbString();
2858
+ const fill2 = new Color({
2859
+ r: 0, g: 255, b: 0, a: alpha,
2860
+ }).saturate(-saturation).toRgbString();
2861
+ const fill3 = new Color({
2862
+ r: 0, g: 255, b: 255, a: alpha,
2863
+ }).saturate(-saturation).toRgbString();
2864
+ const fill4 = new Color({
2865
+ r: 0, g: 0, b: 255, a: alpha,
2866
+ }).saturate(-saturation).toRgbString();
2867
+ const fill5 = new Color({
2868
+ r: 255, g: 0, b: 255, a: alpha,
2869
+ }).saturate(-saturation).toRgbString();
2870
+ const fill6 = new Color({
2871
+ r: 255, g: 0, b: 0, a: alpha,
2872
+ }).saturate(-saturation).toRgbString();
2873
+ const fillGradient = `linear-gradient(to right,
2874
+ ${fill0} 0%, ${fill1} 16.67%, ${fill2} 33.33%, ${fill3} 50%,
2875
+ ${fill4} 66.67%, ${fill5} 83.33%, ${fill6} 100%)`;
2876
+ const lightGrad = `linear-gradient(rgba(255,255,255,${roundA}) 0%, rgba(255,255,255,0) 50%),
2877
+ linear-gradient(rgba(0,0,0,0) 50%, rgba(0,0,0,${roundA}) 100%)`;
2878
+
2879
+ setElementStyle(v1, { background: `${lightGrad},${fillGradient},${whiteGrad}` });
2880
+ const {
2881
+ r: gr, g: gg, b: gb,
2882
+ } = new Color({ r, g, b }).greyscale().toRgb();
2883
+
2884
+ setElementStyle(v2, {
2885
+ background: `linear-gradient(rgb(${r},${g},${b}) 0%, rgb(${gr},${gg},${gb}) 100%)`,
2886
+ });
2887
+ }
2888
+ setElementStyle(v3, {
2889
+ background: `linear-gradient(rgba(${r},${g},${b},1) 0%,rgba(${r},${g},${b},0) 100%)`,
2890
+ });
2891
+ }
2892
+
2893
+ /**
2894
+ * The `ColorPicker` *focusout* event listener when open.
2895
+ * @param {FocusEvent} e
2896
+ * @this {ColorPicker}
2897
+ */
2898
+ handleFocusOut({ relatedTarget }) {
2899
+ // @ts-ignore
2900
+ if (relatedTarget && !this.parent.contains(relatedTarget)) {
2901
+ this.hide(true);
2902
+ }
2903
+ }
2904
+
2905
+ /**
2906
+ * The `ColorPicker` *keyup* event listener when open.
2907
+ * @param {KeyboardEvent} e
2908
+ * @this {ColorPicker}
2909
+ */
2910
+ handleDismiss({ code }) {
2911
+ const self = this;
2912
+ if (self.isOpen && code === keyEscape) {
2913
+ self.hide();
2914
+ }
2915
+ }
2916
+
2917
+ /**
2918
+ * The `ColorPicker` *scroll* event listener when open.
2919
+ * @param {Event} e
2920
+ * @this {ColorPicker}
2921
+ */
2922
+ handleScroll(e) {
2923
+ const self = this;
2924
+ const { activeElement } = getDocument(self.input);
2925
+
2926
+ if ((isMobile && self.dragElement)
2927
+ || (activeElement && self.controlKnobs.includes(activeElement))) {
2928
+ e.stopPropagation();
2929
+ e.preventDefault();
2930
+ }
2931
+
2932
+ self.updateDropdownPosition();
2933
+ }
2934
+
2935
+ /**
2936
+ * The `ColorPicker` keyboard event listener for menu navigation.
2937
+ * @param {KeyboardEvent} e
2938
+ * @this {ColorPicker}
2939
+ */
2940
+ menuKeyHandler(e) {
2941
+ const { target, code } = e;
2942
+ // @ts-ignore
2943
+ const { previousElementSibling, nextElementSibling, parentElement } = target;
2944
+ const isColorOptionsMenu = parentElement && hasClass(parentElement, 'color-options');
2945
+ const allSiblings = [...parentElement.children];
2946
+ const columnsCount = isColorOptionsMenu
2947
+ && getElementStyle(parentElement, 'grid-template-columns').split(' ').length;
2948
+ const currentIndex = allSiblings.indexOf(target);
2949
+ const previousElement = currentIndex > -1
2950
+ && columnsCount && allSiblings[currentIndex - columnsCount];
2951
+ const nextElement = currentIndex > -1
2952
+ && columnsCount && allSiblings[currentIndex + columnsCount];
2953
+
2954
+ if ([keyArrowDown, keyArrowUp, keySpace].includes(code)) {
2955
+ // prevent scroll when navigating the menu via arrow keys / Space
2956
+ e.preventDefault();
2957
+ }
2958
+ if (isColorOptionsMenu) {
2959
+ if (previousElement && code === keyArrowUp) {
2960
+ focus(previousElement);
2961
+ } else if (nextElement && code === keyArrowDown) {
2962
+ focus(nextElement);
2963
+ } else if (previousElementSibling && code === keyArrowLeft) {
2964
+ focus(previousElementSibling);
2965
+ } else if (nextElementSibling && code === keyArrowRight) {
2966
+ focus(nextElementSibling);
2967
+ }
2968
+ } else if (previousElementSibling && [keyArrowLeft, keyArrowUp].includes(code)) {
2969
+ focus(previousElementSibling);
2970
+ } else if (nextElementSibling && [keyArrowRight, keyArrowDown].includes(code)) {
2971
+ focus(nextElementSibling);
2972
+ }
2973
+
2974
+ if ([keyEnter, keySpace].includes(code)) {
2975
+ this.menuClickHandler({ target });
2976
+ }
2977
+ }
2978
+
2979
+ /**
2980
+ * The `ColorPicker` click event listener for the colour menu presets / defaults.
2981
+ * @param {Partial<Event>} e
2982
+ * @this {ColorPicker}
2983
+ */
2984
+ menuClickHandler(e) {
2985
+ const self = this;
2986
+ /** @type {*} */
2987
+ const { target } = e;
2988
+ const { colorMenu } = self;
2989
+ const newOption = (getAttribute(target, 'data-value') || '').trim();
2990
+ // invalidate for targets other than color options
2991
+ if (!newOption.length) return;
2992
+ const currentActive = querySelector('li.active', colorMenu);
2993
+ let newColor = nonColors.includes(newOption) ? 'white' : newOption;
2994
+ newColor = newOption === 'transparent' ? 'rgba(0,0,0,0)' : newOption;
2995
+
2996
+ const {
2997
+ r, g, b, a,
2998
+ } = new Color(newColor);
2999
+
3000
+ ObjectAssign(self.color, {
3001
+ r, g, b, a,
3002
+ });
3003
+
3004
+ self.update();
3005
+
3006
+ if (currentActive !== target) {
3007
+ if (currentActive) {
3008
+ removeClass(currentActive, 'active');
3009
+ removeAttribute(currentActive, ariaSelected);
3010
+ }
3011
+
3012
+ addClass(target, 'active');
3013
+ setAttribute(target, ariaSelected, 'true');
3014
+
3015
+ if (nonColors.includes(newOption)) {
3016
+ self.value = newOption;
3017
+ }
3018
+ firePickerChange(self);
3019
+ }
3020
+ }
3021
+
3022
+ /**
3023
+ * The `ColorPicker` *touchstart* / *mousedown* events listener for control knobs.
3024
+ * @param {TouchEvent} e
3025
+ * @this {ColorPicker}
3026
+ */
3027
+ pointerDown(e) {
3028
+ const self = this;
3029
+ /** @type {*} */
3030
+ const {
3031
+ type, target, touches, pageX, pageY,
3032
+ } = e;
3033
+ const { colorMenu, visuals, controlKnobs } = self;
3034
+ const [v1, v2, v3] = visuals;
3035
+ const [c1, c2, c3] = controlKnobs;
3036
+ /** @type {HTMLElement} */
3037
+ const visual = hasClass(target, 'visual-control')
3038
+ ? target : querySelector('.visual-control', target.parentElement);
3039
+ const visualRect = getBoundingClientRect(visual);
3040
+ const X = type === 'touchstart' ? touches[0].pageX : pageX;
3041
+ const Y = type === 'touchstart' ? touches[0].pageY : pageY;
3042
+ const offsetX = X - window.pageXOffset - visualRect.left;
3043
+ const offsetY = Y - window.pageYOffset - visualRect.top;
3044
+
3045
+ if (target === v1 || target === c1) {
3046
+ self.dragElement = visual;
3047
+ self.changeControl1(offsetX, offsetY);
3048
+ } else if (target === v2 || target === c2) {
3049
+ self.dragElement = visual;
3050
+ self.changeControl2(offsetY);
3051
+ } else if (target === v3 || target === c3) {
3052
+ self.dragElement = visual;
3053
+ self.changeAlpha(offsetY);
3054
+ }
3055
+
3056
+ if (colorMenu) {
3057
+ const currentActive = querySelector('li.active', colorMenu);
3058
+ if (currentActive) {
3059
+ removeClass(currentActive, 'active');
3060
+ removeAttribute(currentActive, ariaSelected);
3061
+ }
3062
+ }
3063
+ e.preventDefault();
3064
+ }
3065
+
3066
+ /**
3067
+ * The `ColorPicker` *touchend* / *mouseup* events listener for control knobs.
3068
+ * @param {TouchEvent} e
3069
+ * @this {ColorPicker}
3070
+ */
3071
+ pointerUp({ target }) {
3072
+ const self = this;
3073
+ const { parent } = self;
3074
+ const doc = getDocument(parent);
3075
+ const currentOpen = querySelector(`${colorPickerParentSelector}.open`, doc) !== null;
3076
+ const selection = doc.getSelection();
3077
+ // @ts-ignore
3078
+ if (!self.dragElement && !selection.toString().length
3079
+ // @ts-ignore
3080
+ && !parent.contains(target)) {
3081
+ self.hide(currentOpen);
3082
+ }
3083
+
3084
+ self.dragElement = null;
3085
+ }
3086
+
3087
+ /**
3088
+ * The `ColorPicker` *touchmove* / *mousemove* events listener for control knobs.
3089
+ * @param {TouchEvent} e
3090
+ */
3091
+ pointerMove(e) {
3092
+ const self = this;
3093
+ const { dragElement, visuals } = self;
3094
+ const [v1, v2, v3] = visuals;
3095
+ const {
3096
+ // @ts-ignore
3097
+ type, touches, pageX, pageY,
3098
+ } = e;
3099
+
3100
+ if (!dragElement) return;
3101
+
3102
+ const controlRect = getBoundingClientRect(dragElement);
3103
+ const X = type === 'touchmove' ? touches[0].pageX : pageX;
3104
+ const Y = type === 'touchmove' ? touches[0].pageY : pageY;
3105
+ const offsetX = X - window.pageXOffset - controlRect.left;
3106
+ const offsetY = Y - window.pageYOffset - controlRect.top;
3107
+
3108
+ if (dragElement === v1) {
3109
+ self.changeControl1(offsetX, offsetY);
3110
+ }
3111
+
3112
+ if (dragElement === v2) {
3113
+ self.changeControl2(offsetY);
3114
+ }
3115
+
3116
+ if (dragElement === v3) {
3117
+ self.changeAlpha(offsetY);
3118
+ }
3119
+ }
3120
+
3121
+ /**
3122
+ * The `ColorPicker` *keydown* event listener for control knobs.
3123
+ * @param {KeyboardEvent} e
3124
+ */
3125
+ handleKnobs(e) {
3126
+ const { target, code } = e;
3127
+ const self = this;
3128
+
3129
+ // only react to arrow buttons
3130
+ if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
3131
+ e.preventDefault();
3132
+
3133
+ const { format, controlKnobs, visuals } = self;
3134
+ const { offsetWidth, offsetHeight } = visuals[0];
3135
+ const [c1, c2, c3] = controlKnobs;
3136
+ const { activeElement } = getDocument(c1);
3137
+ const currentKnob = controlKnobs.find((x) => x === activeElement);
3138
+ const yRatio = offsetHeight / (format === 'hsl' ? 100 : 360);
3139
+
3140
+ if (currentKnob) {
3141
+ let offsetX = 0;
3142
+ let offsetY = 0;
3143
+
3144
+ if (target === c1) {
3145
+ const xRatio = offsetWidth / (format === 'hsl' ? 360 : 100);
3146
+
3147
+ if ([keyArrowLeft, keyArrowRight].includes(code)) {
3148
+ self.controlPositions.c1x += code === keyArrowRight ? xRatio : -xRatio;
3149
+ } else if ([keyArrowUp, keyArrowDown].includes(code)) {
3150
+ self.controlPositions.c1y += code === keyArrowDown ? yRatio : -yRatio;
3151
+ }
3152
+
3153
+ offsetX = self.controlPositions.c1x;
3154
+ offsetY = self.controlPositions.c1y;
3155
+ self.changeControl1(offsetX, offsetY);
3156
+ } else if (target === c2) {
3157
+ self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code)
3158
+ ? yRatio
3159
+ : -yRatio;
3160
+
3161
+ offsetY = self.controlPositions.c2y;
3162
+ self.changeControl2(offsetY);
3163
+ } else if (target === c3) {
3164
+ self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code)
3165
+ ? yRatio
3166
+ : -yRatio;
3167
+
3168
+ offsetY = self.controlPositions.c3y;
3169
+ self.changeAlpha(offsetY);
3170
+ }
3171
+ self.handleScroll(e);
3172
+ }
3173
+ }
3174
+
3175
+ /** The event listener of the colour form inputs. */
3176
+ changeHandler() {
3177
+ const self = this;
3178
+ let colorSource;
3179
+ const {
3180
+ inputs, format, value: currentValue, input, controlPositions, visuals,
3181
+ } = self;
3182
+ /** @type {*} */
3183
+ const { activeElement } = getDocument(input);
3184
+ const { offsetHeight } = visuals[0];
3185
+ const [i1,,, i4] = inputs;
3186
+ const [v1, v2, v3, v4] = format === 'rgb'
3187
+ ? inputs.map((i) => parseFloat(i.value) / (i === i4 ? 100 : 1))
3188
+ : inputs.map((i) => parseFloat(i.value) / (i !== i1 ? 100 : 360));
3189
+ const isNonColorValue = self.hasNonColor && nonColors.includes(currentValue);
3190
+ const alpha = i4 ? v4 : (1 - controlPositions.c3y / offsetHeight);
3191
+
3192
+ if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
3193
+ if (activeElement === input) {
3194
+ if (isNonColorValue) {
3195
+ colorSource = 'white';
3196
+ } else {
3197
+ colorSource = currentValue;
3198
+ }
3199
+ } else if (format === 'hex') {
3200
+ colorSource = i1.value;
3201
+ } else if (format === 'hsl') {
3202
+ colorSource = {
3203
+ h: v1, s: v2, l: v3, a: alpha,
3204
+ };
3205
+ } else if (format === 'hwb') {
3206
+ colorSource = {
3207
+ h: v1, w: v2, b: v3, a: alpha,
3208
+ };
3209
+ } else {
3210
+ colorSource = {
3211
+ r: v1, g: v2, b: v3, a: alpha,
3212
+ };
3213
+ }
3214
+
3215
+ const {
3216
+ r, g, b, a,
3217
+ } = new Color(colorSource);
3218
+
3219
+ ObjectAssign(self.color, {
3220
+ r, g, b, a,
3221
+ });
3222
+ self.setControlPositions();
3223
+ self.updateAppearance();
3224
+ self.updateInputs();
3225
+ self.updateControls();
3226
+ self.updateVisuals();
3227
+
3228
+ // set non-color keyword
3229
+ if (activeElement === input && isNonColorValue) {
3230
+ self.value = currentValue;
3231
+ }
3232
+ }
3233
+ }
3234
+
3235
+ /**
3236
+ * Updates `ColorPicker` first control:
3237
+ * * `lightness` and `saturation` for HEX/RGB;
3238
+ * * `lightness` and `hue` for HSL.
3239
+ *
3240
+ * @param {number} X the X component of the offset
3241
+ * @param {number} Y the Y component of the offset
3242
+ */
3243
+ changeControl1(X, Y) {
3244
+ const self = this;
3245
+ let [offsetX, offsetY] = [0, 0];
3246
+ const {
3247
+ format, controlPositions, visuals,
3248
+ } = self;
3249
+ const { offsetHeight, offsetWidth } = visuals[0];
3250
+
3251
+ if (X > offsetWidth) offsetX = offsetWidth;
3252
+ else if (X >= 0) offsetX = X;
3253
+
3254
+ if (Y > offsetHeight) offsetY = offsetHeight;
3255
+ else if (Y >= 0) offsetY = Y;
3256
+
3257
+ const hue = format === 'hsl'
3258
+ ? offsetX / offsetWidth
3259
+ : controlPositions.c2y / offsetHeight;
3260
+
3261
+ const saturation = format === 'hsl'
3262
+ ? 1 - controlPositions.c2y / offsetHeight
3263
+ : offsetX / offsetWidth;
3264
+
3265
+ const lightness = 1 - offsetY / offsetHeight;
3266
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
3267
+
3268
+ const colorObject = format === 'hsl'
3269
+ ? {
3270
+ h: hue, s: saturation, l: lightness, a: alpha,
3271
+ }
3272
+ : {
3273
+ h: hue, s: saturation, v: lightness, a: alpha,
3274
+ };
3275
+
3276
+ // new color
3277
+ const {
3278
+ r, g, b, a,
3279
+ } = new Color(colorObject);
3280
+
3281
+ ObjectAssign(self.color, {
3282
+ r, g, b, a,
3283
+ });
3284
+
3285
+ // new positions
3286
+ self.controlPositions.c1x = offsetX;
3287
+ self.controlPositions.c1y = offsetY;
3288
+
3289
+ // update color picker
3290
+ self.updateAppearance();
3291
+ self.updateInputs();
3292
+ self.updateControls();
3293
+ self.updateVisuals();
3294
+ }
3295
+
3296
+ /**
3297
+ * Updates `ColorPicker` second control:
3298
+ * * `hue` for HEX/RGB/HWB;
3299
+ * * `saturation` for HSL.
3300
+ *
3301
+ * @param {number} Y the Y offset
3302
+ */
3303
+ changeControl2(Y) {
3304
+ const self = this;
3305
+ const {
3306
+ format, controlPositions, visuals,
3307
+ } = self;
3308
+ const { offsetHeight, offsetWidth } = visuals[0];
3309
+
3310
+ let offsetY = 0;
3311
+
3312
+ if (Y > offsetHeight) offsetY = offsetHeight;
3313
+ else if (Y >= 0) offsetY = Y;
3314
+
3315
+ const hue = format === 'hsl'
3316
+ ? controlPositions.c1x / offsetWidth
3317
+ : offsetY / offsetHeight;
3318
+ const saturation = format === 'hsl'
3319
+ ? 1 - offsetY / offsetHeight
3320
+ : controlPositions.c1x / offsetWidth;
3321
+ const lightness = 1 - controlPositions.c1y / offsetHeight;
3322
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
3323
+ const colorObject = format === 'hsl'
3324
+ ? {
3325
+ h: hue, s: saturation, l: lightness, a: alpha,
3326
+ }
3327
+ : {
3328
+ h: hue, s: saturation, v: lightness, a: alpha,
3329
+ };
3330
+
3331
+ // new color
3332
+ const {
3333
+ r, g, b, a,
3334
+ } = new Color(colorObject);
3335
+
3336
+ ObjectAssign(self.color, {
3337
+ r, g, b, a,
3338
+ });
3339
+
3340
+ // new position
3341
+ self.controlPositions.c2y = offsetY;
3342
+ // update color picker
3343
+ self.updateAppearance();
3344
+ self.updateInputs();
3345
+ self.updateControls();
3346
+ self.updateVisuals();
3347
+ }
3348
+
3349
+ /**
3350
+ * Updates `ColorPicker` last control,
3351
+ * the `alpha` channel.
3352
+ *
3353
+ * @param {number} Y
3354
+ */
3355
+ changeAlpha(Y) {
3356
+ const self = this;
3357
+ const { visuals } = self;
3358
+ const { offsetHeight } = visuals[0];
3359
+ let offsetY = 0;
3360
+
3361
+ if (Y > offsetHeight) offsetY = offsetHeight;
3362
+ else if (Y >= 0) offsetY = Y;
3363
+
3364
+ // update color alpha
3365
+ const alpha = 1 - offsetY / offsetHeight;
3366
+ self.color.setAlpha(alpha);
3367
+ // update position
3368
+ self.controlPositions.c3y = offsetY;
3369
+ // update color picker
3370
+ self.updateAppearance();
3371
+ self.updateInputs();
3372
+ self.updateControls();
3373
+ self.updateVisuals();
3374
+ }
3375
+
3376
+ /**
3377
+ * Updates `ColorPicker` control positions on:
3378
+ * * initialization
3379
+ * * window resize
3380
+ */
3381
+ update() {
3382
+ const self = this;
3383
+ self.updateDropdownPosition();
3384
+ self.updateAppearance();
3385
+ self.setControlPositions();
3386
+ self.updateInputs(true);
3387
+ self.updateControls();
3388
+ self.updateVisuals();
3389
+ }
3390
+
3391
+ /** Updates the open dropdown position on *scroll* event. */
3392
+ updateDropdownPosition() {
3393
+ const self = this;
3394
+ const { input, colorPicker, colorMenu } = self;
3395
+ const elRect = getBoundingClientRect(input);
3396
+ const { top, bottom } = elRect;
3397
+ const { offsetHeight: elHeight } = input;
3398
+ const windowHeight = getDocumentElement(input).clientHeight;
3399
+ const isPicker = hasClass(colorPicker, 'show');
3400
+ const dropdown = isPicker ? colorPicker : colorMenu;
3401
+ if (!dropdown) return;
3402
+ const { offsetHeight: dropHeight } = dropdown;
3403
+ const distanceBottom = windowHeight - bottom;
3404
+ const distanceTop = top;
3405
+ const bottomExceed = top + dropHeight + elHeight > windowHeight; // show
3406
+ const topExceed = top - dropHeight < 0; // show-top
3407
+
3408
+ if ((hasClass(dropdown, 'bottom') || !topExceed) && distanceBottom < distanceTop && bottomExceed) {
3409
+ removeClass(dropdown, 'bottom');
3410
+ addClass(dropdown, 'top');
3411
+ } else {
3412
+ removeClass(dropdown, 'top');
3413
+ addClass(dropdown, 'bottom');
3414
+ }
3415
+ }
3416
+
3417
+ /** Updates control knobs' positions. */
3418
+ setControlPositions() {
3419
+ const self = this;
3420
+ const {
3421
+ format, visuals, color, hsl, hsv,
3422
+ } = self;
3423
+ const { offsetHeight, offsetWidth } = visuals[0];
3424
+ const alpha = color.a;
3425
+ const hue = hsl.h;
3426
+
3427
+ const saturation = format !== 'hsl' ? hsv.s : hsl.s;
3428
+ const lightness = format !== 'hsl' ? hsv.v : hsl.l;
3429
+
3430
+ self.controlPositions.c1x = format !== 'hsl' ? saturation * offsetWidth : hue * offsetWidth;
3431
+ self.controlPositions.c1y = (1 - lightness) * offsetHeight;
3432
+ self.controlPositions.c2y = format !== 'hsl' ? hue * offsetHeight : (1 - saturation) * offsetHeight;
3433
+ self.controlPositions.c3y = (1 - alpha) * offsetHeight;
3434
+ }
3435
+
3436
+ /** Update the visual appearance label and control knob labels. */
3437
+ updateAppearance() {
3438
+ const self = this;
3439
+ const {
3440
+ componentLabels, colorLabels, color, parent,
3441
+ hsl, hsv, hex, format, controlKnobs,
3442
+ } = self;
3443
+ const {
3444
+ appearanceLabel, hexLabel, valueLabel,
3445
+ } = componentLabels;
3446
+ const { r, g, b } = color.toRgb();
3447
+ const [knob1, knob2, knob3] = controlKnobs;
3448
+ const hue = roundPart(hsl.h * 360);
3449
+ const alpha = color.a;
3450
+ const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
3451
+ const saturation = roundPart(saturationSource * 100);
3452
+ const lightness = roundPart(hsl.l * 100);
3453
+ const hsvl = hsv.v * 100;
3454
+ let colorName;
3455
+
3456
+ // determine color appearance
3457
+ if (lightness === 100 && saturation === 0) {
3458
+ colorName = colorLabels.white;
3459
+ } else if (lightness === 0) {
3460
+ colorName = colorLabels.black;
3461
+ } else if (saturation === 0) {
3462
+ colorName = colorLabels.grey;
3463
+ } else if (hue < 15 || hue >= 345) {
3464
+ colorName = colorLabels.red;
3465
+ } else if (hue >= 15 && hue < 45) {
3466
+ colorName = hsvl > 80 && saturation > 80 ? colorLabels.orange : colorLabels.brown;
3467
+ } else if (hue >= 45 && hue < 75) {
3468
+ const isGold = hue > 46 && hue < 54 && hsvl < 80 && saturation > 90;
3469
+ const isOlive = hue >= 54 && hue < 75 && hsvl < 80;
3470
+ colorName = isGold ? colorLabels.gold : colorLabels.yellow;
3471
+ colorName = isOlive ? colorLabels.olive : colorName;
3472
+ } else if (hue >= 75 && hue < 155) {
3473
+ colorName = hsvl < 68 ? colorLabels.green : colorLabels.lime;
3474
+ } else if (hue >= 155 && hue < 175) {
3475
+ colorName = colorLabels.teal;
3476
+ } else if (hue >= 175 && hue < 195) {
3477
+ colorName = colorLabels.cyan;
3478
+ } else if (hue >= 195 && hue < 255) {
3479
+ colorName = colorLabels.blue;
3480
+ } else if (hue >= 255 && hue < 270) {
3481
+ colorName = colorLabels.violet;
3482
+ } else if (hue >= 270 && hue < 295) {
3483
+ colorName = colorLabels.magenta;
3484
+ } else if (hue >= 295 && hue < 345) {
3485
+ colorName = colorLabels.pink;
3486
+ }
3487
+
3488
+ let colorLabel = `${hexLabel} ${hex.split('').join(' ')}`;
3489
+
3490
+ if (format === 'hsl') {
3491
+ colorLabel = `HSL: ${hue}°, ${saturation}%, ${lightness}%`;
3492
+ setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3493
+ setAttribute(knob1, ariaValueText, `${hue}° & ${lightness}%`);
3494
+ setAttribute(knob1, ariaValueNow, `${hue}`);
3495
+ setAttribute(knob2, ariaValueText, `${saturation}%`);
3496
+ setAttribute(knob2, ariaValueNow, `${saturation}`);
3497
+ } else if (format === 'hwb') {
3498
+ const { hwb } = self;
3499
+ const whiteness = roundPart(hwb.w * 100);
3500
+ const blackness = roundPart(hwb.b * 100);
3501
+ colorLabel = `HWB: ${hue}°, ${whiteness}%, ${blackness}%`;
3502
+ setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3503
+ setAttribute(knob1, ariaValueText, `${whiteness}% & ${blackness}%`);
3504
+ setAttribute(knob1, ariaValueNow, `${whiteness}`);
3505
+ setAttribute(knob2, ariaValueText, `${hue}%`);
3506
+ setAttribute(knob2, ariaValueNow, `${hue}`);
3507
+ } else {
3508
+ colorLabel = format === 'rgb' ? `RGB: ${r}, ${g}, ${b}` : colorLabel;
3509
+ setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3510
+ setAttribute(knob1, ariaValueText, `${lightness}% & ${saturation}%`);
3511
+ setAttribute(knob1, ariaValueNow, `${lightness}`);
3512
+ setAttribute(knob2, ariaValueText, `${hue}°`);
3513
+ setAttribute(knob2, ariaValueNow, `${hue}`);
3514
+ }
3515
+
3516
+ const alphaValue = roundPart(alpha * 100);
3517
+ setAttribute(knob3, ariaValueText, `${alphaValue}%`);
3518
+ setAttribute(knob3, ariaValueNow, `${alphaValue}`);
3519
+
3520
+ // update the input backgroundColor
3521
+ const newColor = color.toString();
3522
+ setElementStyle(self.input, { backgroundColor: newColor });
3523
+
3524
+ // toggle dark/light classes will also style the placeholder
3525
+ // dark sets color white, light sets color black
3526
+ // isDark ? '#000' : '#fff'
3527
+ if (!self.isDark) {
3528
+ if (hasClass(parent, 'txt-dark')) removeClass(parent, 'txt-dark');
3529
+ if (!hasClass(parent, 'txt-light')) addClass(parent, 'txt-light');
3530
+ } else {
3531
+ if (hasClass(parent, 'txt-light')) removeClass(parent, 'txt-light');
3532
+ if (!hasClass(parent, 'txt-dark')) addClass(parent, 'txt-dark');
3533
+ }
3534
+ }
3535
+
3536
+ /** Updates the control knobs actual positions. */
3537
+ updateControls() {
3538
+ const { controlKnobs, controlPositions } = this;
3539
+ let {
3540
+ c1x, c1y, c2y, c3y,
3541
+ } = controlPositions;
3542
+ const [control1, control2, control3] = controlKnobs;
3543
+ // round control positions
3544
+ [c1x, c1y, c2y, c3y] = [c1x, c1y, c2y, c3y].map(roundPart);
3545
+
3546
+ setElementStyle(control1, { transform: `translate3d(${c1x - 4}px,${c1y - 4}px,0)` });
3547
+ setElementStyle(control2, { transform: `translate3d(0,${c2y - 4}px,0)` });
3548
+ setElementStyle(control3, { transform: `translate3d(0,${c3y - 4}px,0)` });
3549
+ }
3550
+
3551
+ /**
3552
+ * Updates all color form inputs.
3553
+ * @param {boolean=} isPrevented when `true`, the component original event is prevented
3554
+ */
3555
+ updateInputs(isPrevented) {
3556
+ const self = this;
3557
+ const {
3558
+ value: oldColor, format, inputs, color, hsl,
3559
+ } = self;
3560
+ const [i1, i2, i3, i4] = inputs;
3561
+ const alpha = roundPart(color.a * 100);
3562
+ const hue = roundPart(hsl.h * 360);
3563
+ let newColor;
3564
+
3565
+ if (format === 'hex') {
3566
+ newColor = self.color.toHexString(true);
3567
+ i1.value = self.hex;
3568
+ } else if (format === 'hsl') {
3569
+ const lightness = roundPart(hsl.l * 100);
3570
+ const saturation = roundPart(hsl.s * 100);
3571
+ newColor = self.color.toHslString();
3572
+ i1.value = `${hue}`;
3573
+ i2.value = `${saturation}`;
3574
+ i3.value = `${lightness}`;
3575
+ i4.value = `${alpha}`;
3576
+ } else if (format === 'hwb') {
3577
+ const { w, b } = self.hwb;
3578
+ const whiteness = roundPart(w * 100);
3579
+ const blackness = roundPart(b * 100);
3580
+
3581
+ newColor = self.color.toHwbString();
3582
+ i1.value = `${hue}`;
3583
+ i2.value = `${whiteness}`;
3584
+ i3.value = `${blackness}`;
3585
+ i4.value = `${alpha}`;
3586
+ } else if (format === 'rgb') {
3587
+ let { r, g, b } = self.rgb;
3588
+ [r, g, b] = [r, g, b].map(roundPart);
3589
+
3590
+ newColor = self.color.toRgbString();
3591
+ i1.value = `${r}`;
3592
+ i2.value = `${g}`;
3593
+ i3.value = `${b}`;
3594
+ i4.value = `${alpha}`;
3595
+ }
3596
+
3597
+ // update the color value
3598
+ self.value = `${newColor}`;
3599
+
3600
+ // don't trigger the custom event unless it's really changed
3601
+ if (!isPrevented && newColor !== oldColor) {
3602
+ firePickerChange(self);
3603
+ }
3604
+ }
3605
+
3606
+ /**
3607
+ * The `Space` & `Enter` keys specific event listener.
3608
+ * Toggle visibility of the `ColorPicker` / the presets menu, showing one will hide the other.
3609
+ * @param {KeyboardEvent} e
3610
+ * @this {ColorPicker}
3611
+ */
3612
+ keyToggle(e) {
3613
+ const self = this;
3614
+ const { menuToggle } = self;
3615
+ const { activeElement } = getDocument(menuToggle);
3616
+ const { code } = e;
3617
+
3618
+ if ([keyEnter, keySpace].includes(code)) {
3619
+ if ((menuToggle && activeElement === menuToggle) || !activeElement) {
3620
+ e.preventDefault();
3621
+ if (!activeElement) {
3622
+ self.togglePicker(e);
3623
+ } else {
3624
+ self.toggleMenu();
3625
+ }
3626
+ }
3627
+ }
3628
+ }
3629
+
3630
+ /**
3631
+ * Toggle the `ColorPicker` dropdown visibility.
3632
+ * @param {Event} e
3633
+ * @this {ColorPicker}
3634
+ */
3635
+ togglePicker(e) {
3636
+ e.preventDefault();
3637
+ const self = this;
3638
+ const { colorPicker } = self;
3639
+
3640
+ if (self.isOpen && hasClass(colorPicker, 'show')) {
3641
+ self.hide(true);
3642
+ } else {
3643
+ showDropdown(self, colorPicker);
3644
+ }
3645
+ }
3646
+
3647
+ /** Shows the `ColorPicker` dropdown. */
3648
+ showPicker() {
3649
+ const self = this;
3650
+ const { colorPicker } = self;
3651
+
3652
+ if (!['top', 'bottom'].some((c) => hasClass(colorPicker, c))) {
3653
+ showDropdown(self, colorPicker);
3654
+ }
3655
+ }
3656
+
3657
+ /** Toggles the visibility of the `ColorPicker` presets menu. */
3658
+ toggleMenu() {
3659
+ const self = this;
3660
+ const { colorMenu } = self;
3661
+
3662
+ if (self.isOpen && hasClass(colorMenu, 'show')) {
3663
+ self.hide(true);
3664
+ } else {
3665
+ showDropdown(self, colorMenu);
3666
+ }
3667
+ }
3668
+
3669
+ /**
3670
+ * Hides the currently open `ColorPicker` dropdown.
3671
+ * @param {boolean=} focusPrevented
3672
+ */
3673
+ hide(focusPrevented) {
3674
+ const self = this;
3675
+ if (self.isOpen) {
3676
+ const {
3677
+ pickerToggle, menuToggle, colorPicker, colorMenu, parent, input,
3678
+ } = self;
3679
+ const openPicker = hasClass(colorPicker, 'show');
3680
+ const openDropdown = openPicker ? colorPicker : colorMenu;
3681
+ const relatedBtn = openPicker ? pickerToggle : menuToggle;
3682
+ const animationDuration = openDropdown && getElementTransitionDuration(openDropdown);
3683
+
3684
+ if (openDropdown) {
3685
+ removeClass(openDropdown, 'show');
3686
+ setAttribute(relatedBtn, ariaExpanded, 'false');
3687
+ setTimeout(() => {
3688
+ removePosition(openDropdown);
3689
+ if (!querySelector('.show', parent)) {
3690
+ removeClass(parent, 'open');
3691
+ toggleEventsOnShown(self);
3692
+ self.isOpen = false;
3693
+ }
3694
+ }, animationDuration);
3695
+ }
3696
+
3697
+ if (!self.isValid) {
3698
+ self.value = self.color.toString();
3699
+ }
3700
+ if (!focusPrevented) {
3701
+ focus(pickerToggle);
3702
+ }
3703
+ setAttribute(input, tabIndex, '-1');
3704
+ if (menuToggle) {
3705
+ setAttribute(menuToggle, tabIndex, '-1');
3706
+ }
3707
+ }
3708
+ }
3709
+
3710
+ /** Removes `ColorPicker` from target `<input>`. */
3711
+ dispose() {
3712
+ const self = this;
3713
+ const { input, parent } = self;
3714
+ self.hide(true);
3715
+ toggleEvents(self);
3716
+ [...parent.children].forEach((el) => {
3717
+ if (el !== input) el.remove();
3718
+ });
3719
+
3720
+ removeAttribute(input, tabIndex);
3721
+ setElementStyle(input, { backgroundColor: '' });
3722
+
3723
+ ['txt-light', 'txt-dark'].forEach((c) => removeClass(parent, c));
3724
+ Data.remove(input, colorPickerString);
3725
+ }
3726
+ }
3727
+
3728
+ ObjectAssign(ColorPicker, {
3729
+ Color,
3730
+ ColorPalette,
3731
+ Version,
3732
+ getInstance: getColorPickerInstance,
3733
+ init: initColorPicker,
3734
+ selector: colorPickerSelector,
3735
+ // utils important for render
3736
+ roundPart,
3737
+ setElementStyle,
3738
+ setAttribute,
3739
+ getBoundingClientRect,
3740
+ });
3741
+
3742
+ export default ColorPicker;