@thednp/color-picker 0.0.1-alpha1 → 0.0.1-alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +40 -19
  3. package/dist/css/color-picker.css +481 -337
  4. package/dist/css/color-picker.min.css +2 -0
  5. package/dist/css/color-picker.rtl.css +506 -0
  6. package/dist/css/color-picker.rtl.min.css +2 -0
  7. package/dist/js/color-picker-element-esm.js +3810 -2
  8. package/dist/js/color-picker-element-esm.min.js +2 -0
  9. package/dist/js/color-picker-element.js +2009 -1242
  10. package/dist/js/color-picker-element.min.js +2 -2
  11. package/dist/js/color-picker-esm.js +3704 -0
  12. package/dist/js/color-picker-esm.min.js +2 -0
  13. package/dist/js/color-picker.js +1962 -1256
  14. package/dist/js/color-picker.min.js +2 -2
  15. package/package.json +18 -9
  16. package/src/js/color-palette.js +62 -0
  17. package/src/js/color-picker-element.js +55 -13
  18. package/src/js/color-picker.js +686 -595
  19. package/src/js/color.js +615 -349
  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 +27 -19
  25. package/src/js/util/getColorMenu.js +95 -0
  26. package/src/js/util/isValidJSON.js +13 -0
  27. package/src/js/util/nonColors.js +5 -0
  28. package/src/js/util/templates.js +1 -0
  29. package/src/scss/color-picker.rtl.scss +23 -0
  30. package/src/scss/color-picker.scss +430 -0
  31. package/types/cp.d.ts +263 -160
  32. package/types/index.d.ts +9 -2
  33. package/types/source/source.ts +2 -1
  34. package/types/source/types.d.ts +28 -5
  35. package/dist/js/color-picker.esm.js +0 -2998
  36. package/dist/js/color-picker.esm.min.js +0 -2
  37. package/src/js/util/getColorControl.js +0 -49
  38. package/src/js/util/init.js +0 -14
@@ -0,0 +1,3704 @@
1
+ /*!
2
+ * ColorPicker v0.0.1alpha2 (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 multiple uses of `HTMLElement.style.propertyName` method.
666
+ * @param {HTMLElement | Element} element target element
667
+ * @param {Partial<CSSStyleDeclaration>} styles attribute value
668
+ */
669
+ // @ts-ignore
670
+ const setElementStyle = (element, styles) => { ObjectAssign(element.style, styles); };
671
+
672
+ /**
673
+ * Shortcut for `HTMLElement.getAttribute()` method.
674
+ * @param {HTMLElement | Element} element target element
675
+ * @param {string} attribute attribute name
676
+ * @returns {string?} attribute value
677
+ */
678
+ const getAttribute = (element, attribute) => element.getAttribute(attribute);
679
+
680
+ /**
681
+ * The raw value or a given component option.
682
+ *
683
+ * @typedef {string | HTMLElement | Function | number | boolean | null} niceValue
684
+ */
685
+
686
+ /**
687
+ * Utility to normalize component options
688
+ *
689
+ * @param {any} value the input value
690
+ * @return {niceValue} the normalized value
691
+ */
692
+ function normalizeValue(value) {
693
+ if (value === 'true') { // boolean
694
+ return true;
695
+ }
696
+
697
+ if (value === 'false') { // boolean
698
+ return false;
699
+ }
700
+
701
+ if (!Number.isNaN(+value)) { // number
702
+ return +value;
703
+ }
704
+
705
+ if (value === '' || value === 'null') { // null
706
+ return null;
707
+ }
708
+
709
+ // string / function / HTMLElement / object
710
+ return value;
711
+ }
712
+
713
+ /**
714
+ * Shortcut for `Object.keys()` static method.
715
+ * @param {Record<string, any>} obj a target object
716
+ * @returns {string[]}
717
+ */
718
+ const ObjectKeys = (obj) => Object.keys(obj);
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
+ // }
937
+ colorForm.append(cInputLabel, cInput);
938
+ });
939
+ return colorForm;
940
+ }
941
+
942
+ /**
943
+ * A global namespace for aria-label.
944
+ * @type {string}
945
+ */
946
+ const ariaLabel = 'aria-label';
947
+
948
+ /**
949
+ * A global namespace for aria-valuemin.
950
+ * @type {string}
951
+ */
952
+ const ariaValueMin = 'aria-valuemin';
953
+
954
+ /**
955
+ * A global namespace for aria-valuemax.
956
+ * @type {string}
957
+ */
958
+ const ariaValueMax = 'aria-valuemax';
959
+
960
+ /**
961
+ * Returns all color controls for `ColorPicker`.
962
+ *
963
+ * @param {CP.ColorPicker} self the `ColorPicker` instance
964
+ * @returns {HTMLElement | Element} color controls
965
+ */
966
+ function getColorControls(self) {
967
+ const { format, componentLabels } = self;
968
+ const {
969
+ hueLabel, alphaLabel, lightnessLabel, saturationLabel,
970
+ whitenessLabel, blacknessLabel,
971
+ } = componentLabels;
972
+
973
+ const max1 = format === 'hsl' ? 360 : 100;
974
+ const max2 = format === 'hsl' ? 100 : 360;
975
+ const max3 = 100;
976
+
977
+ let ctrl1Label = format === 'hsl'
978
+ ? `${hueLabel} & ${lightnessLabel}`
979
+ : `${lightnessLabel} & ${saturationLabel}`;
980
+
981
+ ctrl1Label = format === 'hwb'
982
+ ? `${whitenessLabel} & ${blacknessLabel}`
983
+ : ctrl1Label;
984
+
985
+ const ctrl2Label = format === 'hsl'
986
+ ? `${saturationLabel}`
987
+ : `${hueLabel}`;
988
+
989
+ const colorControls = createElement({
990
+ tagName: 'div',
991
+ className: `color-controls ${format}`,
992
+ });
993
+
994
+ const colorPointer = 'color-pointer';
995
+ const colorSlider = 'color-slider';
996
+
997
+ const controls = [
998
+ {
999
+ i: 1,
1000
+ c: colorPointer,
1001
+ l: ctrl1Label,
1002
+ min: 0,
1003
+ max: max1,
1004
+ },
1005
+ {
1006
+ i: 2,
1007
+ c: colorSlider,
1008
+ l: ctrl2Label,
1009
+ min: 0,
1010
+ max: max2,
1011
+ },
1012
+ {
1013
+ i: 3,
1014
+ c: colorSlider,
1015
+ l: alphaLabel,
1016
+ min: 0,
1017
+ max: max3,
1018
+ },
1019
+ ];
1020
+
1021
+ controls.forEach((template) => {
1022
+ const {
1023
+ i, c, l, min, max,
1024
+ } = template;
1025
+ // const hidden = i === 2 && format === 'hwb' ? ' v-hidden' : '';
1026
+ const control = createElement({
1027
+ tagName: 'div',
1028
+ // className: `color-control${hidden}`,
1029
+ className: 'color-control',
1030
+ });
1031
+ setAttribute(control, 'role', 'presentation');
1032
+
1033
+ control.append(
1034
+ createElement({
1035
+ tagName: 'div',
1036
+ className: `visual-control visual-control${i}`,
1037
+ }),
1038
+ );
1039
+
1040
+ const knob = createElement({
1041
+ tagName: 'div',
1042
+ className: `${c} knob`,
1043
+ ariaLive: 'polite',
1044
+ });
1045
+
1046
+ setAttribute(knob, ariaLabel, l);
1047
+ setAttribute(knob, 'role', 'slider');
1048
+ setAttribute(knob, 'tabindex', '0');
1049
+ setAttribute(knob, ariaValueMin, `${min}`);
1050
+ setAttribute(knob, ariaValueMax, `${max}`);
1051
+ control.append(knob);
1052
+ colorControls.append(control);
1053
+ });
1054
+
1055
+ return colorControls;
1056
+ }
1057
+
1058
+ /**
1059
+ * Returns the `document.head` or the `<head>` element.
1060
+ *
1061
+ * @param {(Node | HTMLElement | Element | globalThis)=} node
1062
+ * @returns {HTMLElement | HTMLHeadElement}
1063
+ */
1064
+ function getDocumentHead(node) {
1065
+ return getDocument(node).head;
1066
+ }
1067
+
1068
+ // Color supported formats
1069
+ const COLOR_FORMAT = ['rgb', 'hex', 'hsl', 'hsb', 'hwb'];
1070
+
1071
+ // Hue angles
1072
+ const ANGLES = 'deg|rad|grad|turn';
1073
+
1074
+ // <http://www.w3.org/TR/css3-values/#integers>
1075
+ const CSS_INTEGER = '[-\\+]?\\d+%?';
1076
+
1077
+ // Include CSS3 Module
1078
+ // <http://www.w3.org/TR/css3-values/#number-value>
1079
+ const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
1080
+
1081
+ // Include CSS4 Module Hue degrees unit
1082
+ // <https://www.w3.org/TR/css3-values/#angle-value>
1083
+ const CSS_ANGLE = `[-\\+]?\\d*\\.?\\d+(?:${ANGLES})?`;
1084
+
1085
+ // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
1086
+ const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
1087
+
1088
+ // Add angles to the mix
1089
+ const CSS_UNIT2 = `(?:${CSS_UNIT})|(?:${CSS_ANGLE})`;
1090
+
1091
+ // Actual matching.
1092
+ // Parentheses and commas are optional, but not required.
1093
+ // Whitespace can take the place of commas or opening paren
1094
+ const PERMISSIVE_MATCH = `[\\s|\\(]+(${CSS_UNIT2})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s|\\/\\s]*(${CSS_UNIT})?\\s*\\)?`;
1095
+
1096
+ const matchers = {
1097
+ CSS_UNIT: new RegExp(CSS_UNIT2),
1098
+ hwb: new RegExp(`hwb${PERMISSIVE_MATCH}`),
1099
+ rgb: new RegExp(`rgb(?:a)?${PERMISSIVE_MATCH}`),
1100
+ hsl: new RegExp(`hsl(?:a)?${PERMISSIVE_MATCH}`),
1101
+ hsv: new RegExp(`hsv(?:a)?${PERMISSIVE_MATCH}`),
1102
+ hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1103
+ hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
1104
+ hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1105
+ hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
1106
+ };
1107
+
1108
+ /**
1109
+ * Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
1110
+ * <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
1111
+ * @param {string} n testing number
1112
+ * @returns {boolean} the query result
1113
+ */
1114
+ function isOnePointZero(n) {
1115
+ return `${n}`.includes('.') && parseFloat(n) === 1;
1116
+ }
1117
+
1118
+ /**
1119
+ * Check to see if string passed in is a percentage
1120
+ * @param {string} n testing number
1121
+ * @returns {boolean} the query result
1122
+ */
1123
+ function isPercentage(n) {
1124
+ return `${n}`.includes('%');
1125
+ }
1126
+
1127
+ /**
1128
+ * Check to see if string passed in is an angle
1129
+ * @param {string} n testing string
1130
+ * @returns {boolean} the query result
1131
+ */
1132
+ function isAngle(n) {
1133
+ return ANGLES.split('|').some((a) => `${n}`.includes(a));
1134
+ }
1135
+
1136
+ /**
1137
+ * Check to see if string passed is a web safe colour.
1138
+ * @param {string} color a colour name, EG: *red*
1139
+ * @returns {boolean} the query result
1140
+ */
1141
+ function isColorName(color) {
1142
+ return !['#', ...COLOR_FORMAT].some((s) => color.includes(s))
1143
+ && !/[0-9]/.test(color);
1144
+ }
1145
+
1146
+ /**
1147
+ * Check to see if it looks like a CSS unit
1148
+ * (see `matchers` above for definition).
1149
+ * @param {string | number} color testing value
1150
+ * @returns {boolean} the query result
1151
+ */
1152
+ function isValidCSSUnit(color) {
1153
+ return Boolean(matchers.CSS_UNIT.exec(String(color)));
1154
+ }
1155
+
1156
+ /**
1157
+ * Take input from [0, n] and return it as [0, 1]
1158
+ * @param {*} N the input number
1159
+ * @param {number} max the number maximum value
1160
+ * @returns {number} the number in [0, 1] value range
1161
+ */
1162
+ function bound01(N, max) {
1163
+ let n = N;
1164
+ if (isOnePointZero(n)) n = '100%';
1165
+
1166
+ n = max === 360 ? n : Math.min(max, Math.max(0, parseFloat(n)));
1167
+
1168
+ // Handle hue angles
1169
+ if (isAngle(N)) n = N.replace(new RegExp(ANGLES), '');
1170
+
1171
+ // Automatically convert percentage into number
1172
+ if (isPercentage(n)) n = parseInt(String(n * max), 10) / 100;
1173
+
1174
+ // Handle floating point rounding errors
1175
+ if (Math.abs(n - max) < 0.000001) {
1176
+ return 1;
1177
+ }
1178
+ // Convert into [0, 1] range if it isn't already
1179
+ if (max === 360) {
1180
+ // If n is a hue given in degrees,
1181
+ // wrap around out-of-range values into [0, 360] range
1182
+ // then convert into [0, 1].
1183
+ n = (n < 0 ? (n % max) + max : n % max) / parseFloat(String(max));
1184
+ } else {
1185
+ // If n not a hue given in degrees
1186
+ // Convert into [0, 1] range if it isn't already.
1187
+ n = (n % max) / parseFloat(String(max));
1188
+ }
1189
+ return n;
1190
+ }
1191
+
1192
+ /**
1193
+ * Return a valid alpha value [0,1] with all invalid values being set to 1.
1194
+ * @param {string | number} a transparency value
1195
+ * @returns {number} a transparency value in the [0, 1] range
1196
+ */
1197
+ function boundAlpha(a) {
1198
+ let na = parseFloat(`${a}`);
1199
+
1200
+ if (Number.isNaN(na) || na < 0 || na > 1) {
1201
+ na = 1;
1202
+ }
1203
+
1204
+ return na;
1205
+ }
1206
+
1207
+ /**
1208
+ * Force a number between 0 and 1.
1209
+ * @param {number} v the float number
1210
+ * @returns {number} - the resulting number
1211
+ */
1212
+ function clamp01(v) {
1213
+ return Math.min(1, Math.max(0, v));
1214
+ }
1215
+
1216
+ /**
1217
+ * Returns the hexadecimal value of a web safe colour.
1218
+ * @param {string} name
1219
+ * @returns {string}
1220
+ */
1221
+ function getRGBFromName(name) {
1222
+ const documentHead = getDocumentHead();
1223
+ setElementStyle(documentHead, { color: name });
1224
+ const colorName = getElementStyle(documentHead, 'color');
1225
+ setElementStyle(documentHead, { color: '' });
1226
+ return colorName;
1227
+ }
1228
+
1229
+ /**
1230
+ * Converts a decimal value to hexadecimal.
1231
+ * @param {number} d the input number
1232
+ * @returns {string} - the hexadecimal value
1233
+ */
1234
+ function convertDecimalToHex(d) {
1235
+ return Math.round(d * 255).toString(16);
1236
+ }
1237
+
1238
+ /**
1239
+ * Converts a hexadecimal value to decimal.
1240
+ * @param {string} h hexadecimal value
1241
+ * @returns {number} number in decimal format
1242
+ */
1243
+ function convertHexToDecimal(h) {
1244
+ return parseIntFromHex(h) / 255;
1245
+ }
1246
+
1247
+ /**
1248
+ * Converts a base-16 hexadecimal value into a base-10 integer.
1249
+ * @param {string} val
1250
+ * @returns {number}
1251
+ */
1252
+ function parseIntFromHex(val) {
1253
+ return parseInt(val, 16);
1254
+ }
1255
+
1256
+ /**
1257
+ * Force a hexadecimal value to have 2 characters.
1258
+ * @param {string} c string with [0-9A-F] ranged values
1259
+ * @returns {string} 0 => 00, a => 0a
1260
+ */
1261
+ function pad2(c) {
1262
+ return c.length === 1 ? `0${c}` : String(c);
1263
+ }
1264
+
1265
+ /**
1266
+ * Converts an RGB colour value to HSL.
1267
+ *
1268
+ * @param {number} R Red component [0, 255]
1269
+ * @param {number} G Green component [0, 255]
1270
+ * @param {number} B Blue component [0, 255]
1271
+ * @returns {CP.HSL} {h,s,l} object with [0, 1] ranged values
1272
+ */
1273
+ function rgbToHsl(R, G, B) {
1274
+ const r = R / 255;
1275
+ const g = G / 255;
1276
+ const b = B / 255;
1277
+ const max = Math.max(r, g, b);
1278
+ const min = Math.min(r, g, b);
1279
+ let h = 0;
1280
+ let s = 0;
1281
+ const l = (max + min) / 2;
1282
+ if (max === min) {
1283
+ s = 0;
1284
+ h = 0; // achromatic
1285
+ } else {
1286
+ const d = max - min;
1287
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
1288
+ switch (max) {
1289
+ case r:
1290
+ h = (g - b) / d + (g < b ? 6 : 0);
1291
+ break;
1292
+ case g:
1293
+ h = (b - r) / d + 2;
1294
+ break;
1295
+ case b:
1296
+ h = (r - g) / d + 4;
1297
+ break;
1298
+ }
1299
+ h /= 6;
1300
+ }
1301
+ return { h, s, l };
1302
+ }
1303
+
1304
+ /**
1305
+ * Returns a normalized RGB component value.
1306
+ * @param {number} p
1307
+ * @param {number} q
1308
+ * @param {number} t
1309
+ * @returns {number}
1310
+ */
1311
+ function hueToRgb(p, q, t) {
1312
+ let T = t;
1313
+ if (T < 0) T += 1;
1314
+ if (T > 1) T -= 1;
1315
+ if (T < 1 / 6) return p + (q - p) * (6 * T);
1316
+ if (T < 1 / 2) return q;
1317
+ if (T < 2 / 3) return p + (q - p) * (2 / 3 - T) * 6;
1318
+ return p;
1319
+ }
1320
+
1321
+ /**
1322
+ * Returns an HWB colour object from an RGB colour object.
1323
+ * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
1324
+ * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
1325
+ *
1326
+ * @param {number} R Red component [0, 255]
1327
+ * @param {number} G Green [0, 255]
1328
+ * @param {number} B Blue [0, 255]
1329
+ * @return {CP.HWB} {h,w,b} object with [0, 1] ranged values
1330
+ */
1331
+ function rgbToHwb(R, G, B) {
1332
+ const r = R / 255;
1333
+ const g = G / 255;
1334
+ const b = B / 255;
1335
+
1336
+ let f = 0;
1337
+ let i = 0;
1338
+ const whiteness = Math.min(r, g, b);
1339
+ const max = Math.max(r, g, b);
1340
+ const black = 1 - max;
1341
+
1342
+ if (max === whiteness) return { h: 0, w: whiteness, b: black };
1343
+ if (r === whiteness) {
1344
+ f = g - b;
1345
+ i = 3;
1346
+ } else {
1347
+ f = g === whiteness ? b - r : r - g;
1348
+ i = g === whiteness ? 5 : 1;
1349
+ }
1350
+
1351
+ const h = (i - f / (max - whiteness)) / 6;
1352
+ return {
1353
+ h: h === 1 ? 0 : h,
1354
+ w: whiteness,
1355
+ b: black,
1356
+ };
1357
+ }
1358
+
1359
+ /**
1360
+ * Returns an RGB colour object from an HWB colour.
1361
+ *
1362
+ * @param {number} H Hue Angle [0, 1]
1363
+ * @param {number} W Whiteness [0, 1]
1364
+ * @param {number} B Blackness [0, 1]
1365
+ * @return {CP.RGB} {r,g,b} object with [0, 255] ranged values
1366
+ *
1367
+ * @link https://www.w3.org/TR/css-color-4/#hwb-to-rgb
1368
+ * @link http://alvyray.com/Papers/CG/hwb2rgb.htm
1369
+ */
1370
+ function hwbToRgb(H, W, B) {
1371
+ if (W + B >= 1) {
1372
+ const gray = (W / (W + B)) * 255;
1373
+ return { r: gray, g: gray, b: gray };
1374
+ }
1375
+ let { r, g, b } = hslToRgb(H, 1, 0.5);
1376
+ [r, g, b] = [r, g, b]
1377
+ .map((v) => (v / 255) * (1 - W - B) + W)
1378
+ .map((v) => v * 255);
1379
+
1380
+ return { r, g, b };
1381
+ }
1382
+
1383
+ /**
1384
+ * Converts an HSL colour value to RGB.
1385
+ *
1386
+ * @param {number} h Hue Angle [0, 1]
1387
+ * @param {number} s Saturation [0, 1]
1388
+ * @param {number} l Lightness Angle [0, 1]
1389
+ * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1390
+ */
1391
+ function hslToRgb(h, s, l) {
1392
+ let r = 0;
1393
+ let g = 0;
1394
+ let b = 0;
1395
+
1396
+ if (s === 0) {
1397
+ // achromatic
1398
+ g = l;
1399
+ b = l;
1400
+ r = l;
1401
+ } else {
1402
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1403
+ const p = 2 * l - q;
1404
+ r = hueToRgb(p, q, h + 1 / 3);
1405
+ g = hueToRgb(p, q, h);
1406
+ b = hueToRgb(p, q, h - 1 / 3);
1407
+ }
1408
+ [r, g, b] = [r, g, b].map((x) => x * 255);
1409
+
1410
+ return { r, g, b };
1411
+ }
1412
+
1413
+ /**
1414
+ * Converts an RGB colour value to HSV.
1415
+ *
1416
+ * @param {number} R Red component [0, 255]
1417
+ * @param {number} G Green [0, 255]
1418
+ * @param {number} B Blue [0, 255]
1419
+ * @returns {CP.HSV} {h,s,v} object with [0, 1] ranged values
1420
+ */
1421
+ function rgbToHsv(R, G, B) {
1422
+ const r = R / 255;
1423
+ const g = G / 255;
1424
+ const b = B / 255;
1425
+ const max = Math.max(r, g, b);
1426
+ const min = Math.min(r, g, b);
1427
+ let h = 0;
1428
+ const v = max;
1429
+ const d = max - min;
1430
+ const s = max === 0 ? 0 : d / max;
1431
+ if (max === min) {
1432
+ h = 0; // achromatic
1433
+ } else {
1434
+ switch (max) {
1435
+ case r:
1436
+ h = (g - b) / d + (g < b ? 6 : 0);
1437
+ break;
1438
+ case g:
1439
+ h = (b - r) / d + 2;
1440
+ break;
1441
+ case b:
1442
+ h = (r - g) / d + 4;
1443
+ break;
1444
+ }
1445
+ h /= 6;
1446
+ }
1447
+ return { h, s, v };
1448
+ }
1449
+
1450
+ /**
1451
+ * Converts an HSV colour value to RGB.
1452
+ *
1453
+ * @param {number} H Hue Angle [0, 1]
1454
+ * @param {number} S Saturation [0, 1]
1455
+ * @param {number} V Brightness Angle [0, 1]
1456
+ * @returns {CP.RGB} {r,g,b} object with [0, 1] ranged values
1457
+ */
1458
+ function hsvToRgb(H, S, V) {
1459
+ const h = H * 6;
1460
+ const s = S;
1461
+ const v = V;
1462
+ const i = Math.floor(h);
1463
+ const f = h - i;
1464
+ const p = v * (1 - s);
1465
+ const q = v * (1 - f * s);
1466
+ const t = v * (1 - (1 - f) * s);
1467
+ const mod = i % 6;
1468
+ const r = [v, q, p, p, t, v][mod];
1469
+ const g = [t, v, v, q, p, p][mod];
1470
+ const b = [p, p, t, v, v, q][mod];
1471
+ return { r: r * 255, g: g * 255, b: b * 255 };
1472
+ }
1473
+
1474
+ /**
1475
+ * Converts an RGB colour to hex
1476
+ *
1477
+ * Assumes r, g, and b are contained in the set [0, 255]
1478
+ * Returns a 3 or 6 character hex
1479
+ * @param {number} r Red component [0, 255]
1480
+ * @param {number} g Green [0, 255]
1481
+ * @param {number} b Blue [0, 255]
1482
+ * @param {boolean=} allow3Char
1483
+ * @returns {string}
1484
+ */
1485
+ function rgbToHex(r, g, b, allow3Char) {
1486
+ const hex = [
1487
+ pad2(Math.round(r).toString(16)),
1488
+ pad2(Math.round(g).toString(16)),
1489
+ pad2(Math.round(b).toString(16)),
1490
+ ];
1491
+
1492
+ // Return a 3 character hex if possible
1493
+ if (allow3Char && hex[0].charAt(0) === hex[0].charAt(1)
1494
+ && hex[1].charAt(0) === hex[1].charAt(1)
1495
+ && hex[2].charAt(0) === hex[2].charAt(1)) {
1496
+ return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
1497
+ }
1498
+
1499
+ return hex.join('');
1500
+ }
1501
+
1502
+ /**
1503
+ * Converts an RGBA color plus alpha transparency to hex8.
1504
+ *
1505
+ * @param {number} r Red component [0, 255]
1506
+ * @param {number} g Green [0, 255]
1507
+ * @param {number} b Blue [0, 255]
1508
+ * @param {number} a Alpha transparency [0, 1]
1509
+ * @param {boolean=} allow4Char when *true* it will also find hex shorthand
1510
+ * @returns {string} a hexadecimal value with alpha transparency
1511
+ */
1512
+ function rgbaToHex(r, g, b, a, allow4Char) {
1513
+ const hex = [
1514
+ pad2(Math.round(r).toString(16)),
1515
+ pad2(Math.round(g).toString(16)),
1516
+ pad2(Math.round(b).toString(16)),
1517
+ pad2(convertDecimalToHex(a)),
1518
+ ];
1519
+
1520
+ // Return a 4 character hex if possible
1521
+ if (allow4Char && hex[0].charAt(0) === hex[0].charAt(1)
1522
+ && hex[1].charAt(0) === hex[1].charAt(1)
1523
+ && hex[2].charAt(0) === hex[2].charAt(1)
1524
+ && hex[3].charAt(0) === hex[3].charAt(1)) {
1525
+ return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
1526
+ }
1527
+ return hex.join('');
1528
+ }
1529
+
1530
+ /**
1531
+ * Returns a colour object corresponding to a given number.
1532
+ * @param {number} color input number
1533
+ * @returns {CP.RGB} {r,g,b} object with [0, 255] ranged values
1534
+ */
1535
+ function numberInputToObject(color) {
1536
+ /* eslint-disable no-bitwise */
1537
+ return {
1538
+ r: color >> 16,
1539
+ g: (color & 0xff00) >> 8,
1540
+ b: color & 0xff,
1541
+ };
1542
+ /* eslint-enable no-bitwise */
1543
+ }
1544
+
1545
+ /**
1546
+ * Permissive string parsing. Take in a number of formats, and output an object
1547
+ * based on detected format. Returns {r,g,b} or {h,s,l} or {h,s,v}
1548
+ * @param {string} input colour value in any format
1549
+ * @returns {Record<string, (number | string)> | false} an object matching the RegExp
1550
+ */
1551
+ function stringInputToObject(input) {
1552
+ let color = input.trim().toLowerCase();
1553
+ if (color.length === 0) {
1554
+ return {
1555
+ r: 0, g: 0, b: 0, a: 0,
1556
+ };
1557
+ }
1558
+ let named = false;
1559
+ if (isColorName(color)) {
1560
+ color = getRGBFromName(color);
1561
+ named = true;
1562
+ } else if (nonColors.includes(color)) {
1563
+ const isTransparent = color === 'transparent';
1564
+ const rgb = isTransparent ? 0 : 255;
1565
+ const a = isTransparent ? 0 : 1;
1566
+ return {
1567
+ r: rgb, g: rgb, b: rgb, a, format: 'rgb',
1568
+ };
1569
+ }
1570
+
1571
+ // Try to match string input using regular expressions.
1572
+ // Keep most of the number bounding out of this function,
1573
+ // don't worry about [0,1] or [0,100] or [0,360]
1574
+ // Just return an object and let the conversion functions handle that.
1575
+ // This way the result will be the same whether Color is initialized with string or object.
1576
+ let [, m1, m2, m3, m4] = matchers.rgb.exec(color) || [];
1577
+ if (m1 && m2 && m3/* && m4 */) {
1578
+ return {
1579
+ r: m1, g: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'rgb',
1580
+ };
1581
+ }
1582
+ [, m1, m2, m3, m4] = matchers.hsl.exec(color) || [];
1583
+ if (m1 && m2 && m3/* && m4 */) {
1584
+ return {
1585
+ h: m1, s: m2, l: m3, a: m4 !== undefined ? m4 : 1, format: 'hsl',
1586
+ };
1587
+ }
1588
+ [, m1, m2, m3, m4] = matchers.hsv.exec(color) || [];
1589
+ if (m1 && m2 && m3/* && m4 */) {
1590
+ return {
1591
+ h: m1, s: m2, v: m3, a: m4 !== undefined ? m4 : 1, format: 'hsv',
1592
+ };
1593
+ }
1594
+ [, m1, m2, m3, m4] = matchers.hwb.exec(color) || [];
1595
+ if (m1 && m2 && m3) {
1596
+ return {
1597
+ h: m1, w: m2, b: m3, a: m4 !== undefined ? m4 : 1, format: 'hwb',
1598
+ };
1599
+ }
1600
+ [, m1, m2, m3, m4] = matchers.hex8.exec(color) || [];
1601
+ if (m1 && m2 && m3 && m4) {
1602
+ return {
1603
+ r: parseIntFromHex(m1),
1604
+ g: parseIntFromHex(m2),
1605
+ b: parseIntFromHex(m3),
1606
+ a: convertHexToDecimal(m4),
1607
+ // format: named ? 'rgb' : 'hex8',
1608
+ format: named ? 'rgb' : 'hex',
1609
+ };
1610
+ }
1611
+ [, m1, m2, m3] = matchers.hex6.exec(color) || [];
1612
+ if (m1 && m2 && m3) {
1613
+ return {
1614
+ r: parseIntFromHex(m1),
1615
+ g: parseIntFromHex(m2),
1616
+ b: parseIntFromHex(m3),
1617
+ format: named ? 'rgb' : 'hex',
1618
+ };
1619
+ }
1620
+ [, m1, m2, m3, m4] = matchers.hex4.exec(color) || [];
1621
+ if (m1 && m2 && m3 && m4) {
1622
+ return {
1623
+ r: parseIntFromHex(m1 + m1),
1624
+ g: parseIntFromHex(m2 + m2),
1625
+ b: parseIntFromHex(m3 + m3),
1626
+ a: convertHexToDecimal(m4 + m4),
1627
+ // format: named ? 'rgb' : 'hex8',
1628
+ format: named ? 'rgb' : 'hex',
1629
+ };
1630
+ }
1631
+ [, m1, m2, m3] = matchers.hex3.exec(color) || [];
1632
+ if (m1 && m2 && m3) {
1633
+ return {
1634
+ r: parseIntFromHex(m1 + m1),
1635
+ g: parseIntFromHex(m2 + m2),
1636
+ b: parseIntFromHex(m3 + m3),
1637
+ format: named ? 'rgb' : 'hex',
1638
+ };
1639
+ }
1640
+ return false;
1641
+ }
1642
+
1643
+ /**
1644
+ * Given a string or object, convert that input to RGB
1645
+ *
1646
+ * Possible string inputs:
1647
+ * ```
1648
+ * "red"
1649
+ * "#f00" or "f00"
1650
+ * "#ff0000" or "ff0000"
1651
+ * "#ff000000" or "ff000000" // CSS4 Module
1652
+ * "rgb 255 0 0" or "rgb (255, 0, 0)"
1653
+ * "rgb 1.0 0 0" or "rgb (1, 0, 0)"
1654
+ * "rgba(255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
1655
+ * "rgba(1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
1656
+ * "rgb(255 0 0 / 10%)" or "rgb 255 0 0 0.1" // CSS4 Module
1657
+ * "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
1658
+ * "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
1659
+ * "hsl(0deg 100% 50% / 50%)" or "hsl 0 100 50 50" // CSS4 Module
1660
+ * "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
1661
+ * "hsva(0, 100%, 100%, 0.1)" or "hsva 0 100% 100% 0.1"
1662
+ * "hsv(0deg 100% 100% / 10%)" or "hsv 0 100 100 0.1" // CSS4 Module
1663
+ * "hwb(0deg, 100%, 100%, 100%)" or "hwb 0 100% 100% 0.1" // CSS4 Module
1664
+ * ```
1665
+ * @param {string | Record<string, any>} input
1666
+ * @returns {CP.ColorObject}
1667
+ */
1668
+ function inputToRGB(input) {
1669
+ let rgb = { r: 0, g: 0, b: 0 };
1670
+ let color = input;
1671
+ let a = 1;
1672
+ let s = null;
1673
+ let v = null;
1674
+ let l = null;
1675
+ let w = null;
1676
+ let b = null;
1677
+ let h = null;
1678
+ let ok = false;
1679
+ let format = 'hex';
1680
+
1681
+ if (typeof input === 'string') {
1682
+ // @ts-ignore -- this now is converted to object
1683
+ color = stringInputToObject(input);
1684
+ if (color) ok = true;
1685
+ }
1686
+ if (typeof color === 'object') {
1687
+ if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
1688
+ rgb = { r: color.r, g: color.g, b: color.b }; // RGB values in [0, 255] range
1689
+ ok = true;
1690
+ format = 'rgb';
1691
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
1692
+ ({ h, s, v } = color);
1693
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1694
+ s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1695
+ v = typeof v === 'number' ? v : bound01(v, 100); // brightness can be `5%` or a [0, 1] value
1696
+ rgb = hsvToRgb(h, s, v);
1697
+ ok = true;
1698
+ format = 'hsv';
1699
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
1700
+ ({ h, s, l } = color);
1701
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1702
+ s = typeof s === 'number' ? s : bound01(s, 100); // saturation can be `5%` or a [0, 1] value
1703
+ l = typeof l === 'number' ? l : bound01(l, 100); // lightness can be `5%` or a [0, 1] value
1704
+ rgb = hslToRgb(h, s, l);
1705
+ ok = true;
1706
+ format = 'hsl';
1707
+ } else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.w) && isValidCSSUnit(color.b)) {
1708
+ ({ h, w, b } = color);
1709
+ h = typeof h === 'number' ? h : bound01(h, 360); // hue can be `5deg` or a [0, 1] value
1710
+ w = typeof w === 'number' ? w : bound01(w, 100); // whiteness can be `5%` or a [0, 1] value
1711
+ b = typeof b === 'number' ? b : bound01(b, 100); // blackness can be `5%` or a [0, 1] value
1712
+ rgb = hwbToRgb(h, w, b);
1713
+ ok = true;
1714
+ format = 'hwb';
1715
+ }
1716
+ if (isValidCSSUnit(color.a)) {
1717
+ a = color.a;
1718
+ a = isPercentage(`${a}`) ? bound01(a, 100) : a;
1719
+ }
1720
+ }
1721
+
1722
+ return {
1723
+ ok, // @ts-ignore
1724
+ format: color.format || format,
1725
+ r: Math.min(255, Math.max(rgb.r, 0)),
1726
+ g: Math.min(255, Math.max(rgb.g, 0)),
1727
+ b: Math.min(255, Math.max(rgb.b, 0)),
1728
+ a: boundAlpha(a),
1729
+ };
1730
+ }
1731
+
1732
+ /**
1733
+ * @class
1734
+ * Returns a new `Color` instance.
1735
+ * @see https://github.com/bgrins/TinyColor
1736
+ */
1737
+ class Color {
1738
+ /**
1739
+ * @constructor
1740
+ * @param {CP.ColorInput} input the given colour value
1741
+ * @param {CP.ColorFormats=} config the given format
1742
+ */
1743
+ constructor(input, config) {
1744
+ let color = input;
1745
+ const configFormat = config && COLOR_FORMAT.includes(config)
1746
+ ? config : 'rgb';
1747
+
1748
+ // If input is already a `Color`, return itself
1749
+ if (color instanceof Color) {
1750
+ color = inputToRGB(color);
1751
+ }
1752
+ if (typeof color === 'number') {
1753
+ color = numberInputToObject(color);
1754
+ }
1755
+ const {
1756
+ r, g, b, a, ok, format,
1757
+ } = inputToRGB(color);
1758
+
1759
+ // bind
1760
+ const self = this;
1761
+
1762
+ /** @type {CP.ColorInput} */
1763
+ self.originalInput = color;
1764
+ /** @type {number} */
1765
+ self.r = r;
1766
+ /** @type {number} */
1767
+ self.g = g;
1768
+ /** @type {number} */
1769
+ self.b = b;
1770
+ /** @type {number} */
1771
+ self.a = a;
1772
+ /** @type {boolean} */
1773
+ self.ok = ok;
1774
+ /** @type {CP.ColorFormats} */
1775
+ self.format = configFormat || format;
1776
+
1777
+ // Don't let the range of [0,255] come back in [0,1].
1778
+ // Potentially lose a little bit of precision here, but will fix issues where
1779
+ // .5 gets interpreted as half of the total, instead of half of 1
1780
+ // If it was supposed to be 128, this was already taken care of by `inputToRgb`
1781
+ if (r < 1) self.r = Math.round(r);
1782
+ if (g < 1) self.g = Math.round(g);
1783
+ if (b < 1) self.b = Math.round(b);
1784
+ }
1785
+
1786
+ /**
1787
+ * Checks if the current input value is a valid colour.
1788
+ * @returns {boolean} the query result
1789
+ */
1790
+ get isValid() {
1791
+ return this.ok;
1792
+ }
1793
+
1794
+ /**
1795
+ * Checks if the current colour requires a light text colour.
1796
+ * @returns {boolean} the query result
1797
+ */
1798
+ get isDark() {
1799
+ return this.brightness < 128;
1800
+ }
1801
+
1802
+ /**
1803
+ * Returns the perceived luminance of a colour.
1804
+ * @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
1805
+ * @returns {number} a number in the [0, 1] range
1806
+ */
1807
+ get luminance() {
1808
+ const { r, g, b } = this;
1809
+ let R = 0;
1810
+ let G = 0;
1811
+ let B = 0;
1812
+ const rp = r / 255;
1813
+ const rg = g / 255;
1814
+ const rb = b / 255;
1815
+
1816
+ if (rp <= 0.03928) {
1817
+ R = rp / 12.92;
1818
+ } else {
1819
+ R = ((rp + 0.055) / 1.055) ** 2.4;
1820
+ }
1821
+ if (rg <= 0.03928) {
1822
+ G = rg / 12.92;
1823
+ } else {
1824
+ G = ((rg + 0.055) / 1.055) ** 2.4;
1825
+ }
1826
+ if (rb <= 0.03928) {
1827
+ B = rb / 12.92;
1828
+ } else {
1829
+ B = ((rb + 0.055) / 1.055) ** 2.4;
1830
+ }
1831
+ return 0.2126 * R + 0.7152 * G + 0.0722 * B;
1832
+ }
1833
+
1834
+ /**
1835
+ * Returns the perceived brightness of the colour.
1836
+ * @returns {number} a number in the [0, 255] range
1837
+ */
1838
+ get brightness() {
1839
+ const { r, g, b } = this;
1840
+ return (r * 299 + g * 587 + b * 114) / 1000;
1841
+ }
1842
+
1843
+ /**
1844
+ * Returns the colour as an RGBA object.
1845
+ * @returns {CP.RGBA} an {r,g,b,a} object with [0, 255] ranged values
1846
+ */
1847
+ toRgb() {
1848
+ const {
1849
+ r, g, b, a,
1850
+ } = this;
1851
+ const [R, G, B] = [r, g, b].map((x) => Math.round(x));
1852
+
1853
+ return {
1854
+ r: R,
1855
+ g: G,
1856
+ b: B,
1857
+ a: Math.round(a * 100) / 100,
1858
+ };
1859
+ }
1860
+
1861
+ /**
1862
+ * Returns the RGBA values concatenated into a CSS3 Module string format.
1863
+ * * rgb(255,255,255)
1864
+ * * rgba(255,255,255,0.5)
1865
+ * @returns {string} the CSS valid colour in RGB/RGBA format
1866
+ */
1867
+ toRgbString() {
1868
+ const {
1869
+ r, g, b, a,
1870
+ } = this.toRgb();
1871
+
1872
+ return a === 1
1873
+ ? `rgb(${r}, ${g}, ${b})`
1874
+ : `rgba(${r}, ${g}, ${b}, ${a})`;
1875
+ }
1876
+
1877
+ /**
1878
+ * Returns the RGBA values concatenated into a CSS4 Module string format.
1879
+ * * rgb(255 255 255)
1880
+ * * rgb(255 255 255 / 50%)
1881
+ * @returns {string} the CSS valid colour in CSS4 RGB format
1882
+ */
1883
+ toRgbCSS4String() {
1884
+ const {
1885
+ r, g, b, a,
1886
+ } = this.toRgb();
1887
+ const A = a === 1 ? '' : ` / ${Math.round(a * 100)}%`;
1888
+
1889
+ return `rgb(${r} ${g} ${b}${A})`;
1890
+ }
1891
+
1892
+ /**
1893
+ * Returns the hexadecimal value of the colour. When the parameter is *true*
1894
+ * it will find a 3 characters shorthand of the decimal value.
1895
+ *
1896
+ * @param {boolean=} allow3Char when `true` returns shorthand HEX
1897
+ * @returns {string} the hexadecimal colour format
1898
+ */
1899
+ toHex(allow3Char) {
1900
+ const {
1901
+ r, g, b, a,
1902
+ } = this.toRgb();
1903
+
1904
+ return a === 1
1905
+ ? rgbToHex(r, g, b, allow3Char)
1906
+ : rgbaToHex(r, g, b, a, allow3Char);
1907
+ }
1908
+
1909
+ /**
1910
+ * Returns the CSS valid hexadecimal vaue of the colour. When the parameter is *true*
1911
+ * it will find a 3 characters shorthand of the value.
1912
+ *
1913
+ * @param {boolean=} allow3Char when `true` returns shorthand HEX
1914
+ * @returns {string} the CSS valid colour in hexadecimal format
1915
+ */
1916
+ toHexString(allow3Char) {
1917
+ return `#${this.toHex(allow3Char)}`;
1918
+ }
1919
+
1920
+ /**
1921
+ * Returns the HEX8 value of the colour.
1922
+ * @param {boolean=} allow4Char when `true` returns shorthand HEX
1923
+ * @returns {string} the CSS valid colour in hexadecimal format
1924
+ */
1925
+ toHex8(allow4Char) {
1926
+ const {
1927
+ r, g, b, a,
1928
+ } = this.toRgb();
1929
+
1930
+ return rgbaToHex(r, g, b, a, allow4Char);
1931
+ }
1932
+
1933
+ /**
1934
+ * Returns the HEX8 value of the colour.
1935
+ * @param {boolean=} allow4Char when `true` returns shorthand HEX
1936
+ * @returns {string} the CSS valid colour in hexadecimal format
1937
+ */
1938
+ toHex8String(allow4Char) {
1939
+ return `#${this.toHex8(allow4Char)}`;
1940
+ }
1941
+
1942
+ /**
1943
+ * Returns the colour as a HSVA object.
1944
+ * @returns {CP.HSVA} the `{h,s,v,a}` object with [0, 1] ranged values
1945
+ */
1946
+ toHsv() {
1947
+ const {
1948
+ r, g, b, a,
1949
+ } = this.toRgb();
1950
+ const { h, s, v } = rgbToHsv(r, g, b);
1951
+
1952
+ return {
1953
+ h, s, v, a,
1954
+ };
1955
+ }
1956
+
1957
+ /**
1958
+ * Returns the colour as an HSLA object.
1959
+ * @returns {CP.HSLA} the `{h,s,l,a}` object with [0, 1] ranged values
1960
+ */
1961
+ toHsl() {
1962
+ const {
1963
+ r, g, b, a,
1964
+ } = this.toRgb();
1965
+ const { h, s, l } = rgbToHsl(r, g, b);
1966
+
1967
+ return {
1968
+ h, s, l, a,
1969
+ };
1970
+ }
1971
+
1972
+ /**
1973
+ * Returns the HSLA values concatenated into a CSS3 Module format string.
1974
+ * * `hsl(150, 100%, 50%)`
1975
+ * * `hsla(150, 100%, 50%, 0.5)`
1976
+ * @returns {string} the CSS valid colour in HSL/HSLA format
1977
+ */
1978
+ toHslString() {
1979
+ let {
1980
+ h, s, l, a,
1981
+ } = this.toHsl();
1982
+ h = Math.round(h * 360);
1983
+ s = Math.round(s * 100);
1984
+ l = Math.round(l * 100);
1985
+ a = Math.round(a * 100) / 100;
1986
+
1987
+ return a === 1
1988
+ ? `hsl(${h}, ${s}%, ${l}%)`
1989
+ : `hsla(${h}, ${s}%, ${l}%, ${a})`;
1990
+ }
1991
+
1992
+ /**
1993
+ * Returns the HSLA values concatenated into a CSS4 Module format string.
1994
+ * * `hsl(150deg 100% 50%)`
1995
+ * * `hsl(150deg 100% 50% / 50%)`
1996
+ * @returns {string} the CSS valid colour in CSS4 HSL format
1997
+ */
1998
+ toHslCSS4String() {
1999
+ let {
2000
+ h, s, l, a,
2001
+ } = this.toHsl();
2002
+ h = Math.round(h * 360);
2003
+ s = Math.round(s * 100);
2004
+ l = Math.round(l * 100);
2005
+ a = Math.round(a * 100);
2006
+ const A = a < 100 ? ` / ${Math.round(a)}%` : '';
2007
+
2008
+ return `hsl(${h}deg ${s}% ${l}%${A})`;
2009
+ }
2010
+
2011
+ /**
2012
+ * Returns the colour as an HWBA object.
2013
+ * @returns {CP.HWBA} the `{h,w,b,a}` object with [0, 1] ranged values
2014
+ */
2015
+ toHwb() {
2016
+ const {
2017
+ r, g, b, a,
2018
+ } = this;
2019
+ const { h, w, b: bl } = rgbToHwb(r, g, b);
2020
+ return {
2021
+ h, w, b: bl, a,
2022
+ };
2023
+ }
2024
+
2025
+ /**
2026
+ * Returns the HWBA values concatenated into a string.
2027
+ * @returns {string} the CSS valid colour in HWB format
2028
+ */
2029
+ toHwbString() {
2030
+ let {
2031
+ h, w, b, a,
2032
+ } = this.toHwb();
2033
+ h = Math.round(h * 360);
2034
+ w = Math.round(w * 100);
2035
+ b = Math.round(b * 100);
2036
+ a = Math.round(a * 100);
2037
+ const A = a < 100 ? ` / ${Math.round(a)}%` : '';
2038
+
2039
+ return `hwb(${h}deg ${w}% ${b}%${A})`;
2040
+ }
2041
+
2042
+ /**
2043
+ * Sets the alpha value of the current colour.
2044
+ * @param {number} alpha a new alpha value in the [0, 1] range.
2045
+ * @returns {Color} the `Color` instance
2046
+ */
2047
+ setAlpha(alpha) {
2048
+ const self = this;
2049
+ self.a = boundAlpha(alpha);
2050
+ return self;
2051
+ }
2052
+
2053
+ /**
2054
+ * Saturate the colour with a given amount.
2055
+ * @param {number=} amount a value in the [0, 100] range
2056
+ * @returns {Color} the `Color` instance
2057
+ */
2058
+ saturate(amount) {
2059
+ const self = this;
2060
+ if (typeof amount !== 'number') return self;
2061
+ const { h, s, l } = self.toHsl();
2062
+ const { r, g, b } = hslToRgb(h, clamp01(s + amount / 100), l);
2063
+
2064
+ ObjectAssign(self, { r, g, b });
2065
+ return self;
2066
+ }
2067
+
2068
+ /**
2069
+ * Desaturate 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
+ desaturate(amount) {
2074
+ return typeof amount === 'number' ? this.saturate(-amount) : this;
2075
+ }
2076
+
2077
+ /**
2078
+ * Completely desaturates a colour into greyscale.
2079
+ * Same as calling `desaturate(100)`
2080
+ * @returns {Color} the `Color` instance
2081
+ */
2082
+ greyscale() {
2083
+ return this.saturate(-100);
2084
+ }
2085
+
2086
+ /**
2087
+ * Increase the colour lightness with a given amount.
2088
+ * @param {number=} amount a value in the [0, 100] range
2089
+ * @returns {Color} the `Color` instance
2090
+ */
2091
+ lighten(amount) {
2092
+ const self = this;
2093
+ if (typeof amount !== 'number') return self;
2094
+
2095
+ const { h, s, l } = self.toHsl();
2096
+ const { r, g, b } = hslToRgb(h, s, clamp01(l + amount / 100));
2097
+
2098
+ ObjectAssign(self, { r, g, b });
2099
+ return self;
2100
+ }
2101
+
2102
+ /**
2103
+ * Decrease the colour lightness with a given amount.
2104
+ * @param {number=} amount a value in the [0, 100] range
2105
+ * @returns {Color} the `Color` instance
2106
+ */
2107
+ darken(amount) {
2108
+ return typeof amount === 'number' ? this.lighten(-amount) : this;
2109
+ }
2110
+
2111
+ /**
2112
+ * Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
2113
+ * Values outside of this range will be wrapped into this range.
2114
+ *
2115
+ * @param {number=} amount a value in the [0, 100] range
2116
+ * @returns {Color} the `Color` instance
2117
+ */
2118
+ spin(amount) {
2119
+ const self = this;
2120
+ if (typeof amount !== 'number') return self;
2121
+
2122
+ const { h, s, l } = self.toHsl();
2123
+ const { r, g, b } = hslToRgb(clamp01(((h * 360 + amount) % 360) / 360), s, l);
2124
+
2125
+ ObjectAssign(self, { r, g, b });
2126
+ return self;
2127
+ }
2128
+
2129
+ /** Returns a clone of the current `Color` instance. */
2130
+ clone() {
2131
+ return new Color(this);
2132
+ }
2133
+
2134
+ /**
2135
+ * Returns the colour value in CSS valid string format.
2136
+ * @param {boolean=} allowShort when *true*, HEX values can be shorthand
2137
+ * @returns {string} the CSS valid colour in the configured format
2138
+ */
2139
+ toString(allowShort) {
2140
+ const self = this;
2141
+ const { format } = self;
2142
+
2143
+ if (format === 'hex') return self.toHexString(allowShort);
2144
+ if (format === 'hsl') return self.toHslString();
2145
+ if (format === 'hwb') return self.toHwbString();
2146
+
2147
+ return self.toRgbString();
2148
+ }
2149
+ }
2150
+
2151
+ ObjectAssign(Color, {
2152
+ ANGLES,
2153
+ CSS_ANGLE,
2154
+ CSS_INTEGER,
2155
+ CSS_NUMBER,
2156
+ CSS_UNIT,
2157
+ CSS_UNIT2,
2158
+ PERMISSIVE_MATCH,
2159
+ matchers,
2160
+ isOnePointZero,
2161
+ isPercentage,
2162
+ isValidCSSUnit,
2163
+ pad2,
2164
+ clamp01,
2165
+ bound01,
2166
+ boundAlpha,
2167
+ getRGBFromName,
2168
+ convertHexToDecimal,
2169
+ convertDecimalToHex,
2170
+ rgbToHsl,
2171
+ rgbToHex,
2172
+ rgbToHsv,
2173
+ rgbToHwb,
2174
+ rgbaToHex,
2175
+ hslToRgb,
2176
+ hsvToRgb,
2177
+ hueToRgb,
2178
+ hwbToRgb,
2179
+ parseIntFromHex,
2180
+ numberInputToObject,
2181
+ stringInputToObject,
2182
+ inputToRGB,
2183
+ ObjectAssign,
2184
+ });
2185
+
2186
+ /**
2187
+ * @class
2188
+ * Returns a color palette with a given set of parameters.
2189
+ * @example
2190
+ * new ColorPalette(0, 12, 10);
2191
+ * // => { hue: 0, hueSteps: 12, lightSteps: 10, colors: array }
2192
+ */
2193
+ class ColorPalette {
2194
+ /**
2195
+ * The `hue` parameter is optional, which would be set to 0.
2196
+ * @param {number[]} args represeinting hue, hueSteps, lightSteps
2197
+ * * `args.hue` the starting Hue [0, 360]
2198
+ * * `args.hueSteps` Hue Steps Count [5, 13]
2199
+ * * `args.lightSteps` Lightness Steps Count [8, 10]
2200
+ */
2201
+ constructor(...args) {
2202
+ let hue = 0;
2203
+ let hueSteps = 12;
2204
+ let lightSteps = 10;
2205
+ let lightnessArray = [0.5];
2206
+
2207
+ if (args.length === 3) {
2208
+ [hue, hueSteps, lightSteps] = args;
2209
+ } else if (args.length === 2) {
2210
+ [hueSteps, lightSteps] = args;
2211
+ } else {
2212
+ throw TypeError('The ColorPalette requires minimum 2 arguments');
2213
+ }
2214
+
2215
+ /** @type {string[]} */
2216
+ const colors = [];
2217
+
2218
+ const hueStep = 360 / hueSteps;
2219
+ const lightStep = 100 / (lightSteps + (lightSteps % 2 ? 0 : 1)) / 100;
2220
+ const half = Math.round((lightSteps - (lightSteps % 2 ? 1 : 0)) / 2);
2221
+
2222
+ // light tints
2223
+ for (let i = 0; i < half; i += 1) {
2224
+ lightnessArray = [...lightnessArray, (0.5 + lightStep * (i + 1))];
2225
+ }
2226
+
2227
+ // dark tints
2228
+ for (let i = 0; i < lightSteps - half - 1; i += 1) {
2229
+ lightnessArray = [(0.5 - lightStep * (i + 1)), ...lightnessArray];
2230
+ }
2231
+
2232
+ // feed `colors` Array
2233
+ for (let i = 0; i < hueSteps; i += 1) {
2234
+ const currentHue = ((hue + i * hueStep) % 360) / 360;
2235
+ lightnessArray.forEach((l) => {
2236
+ colors.push(new Color({ h: currentHue, s: 1, l }).toHexString());
2237
+ });
2238
+ }
2239
+
2240
+ this.hue = hue;
2241
+ this.hueSteps = hueSteps;
2242
+ this.lightSteps = lightSteps;
2243
+ this.colors = colors;
2244
+ }
2245
+ }
2246
+
2247
+ /**
2248
+ * Returns a color-defaults with given values and class.
2249
+ * @param {CP.ColorPicker} self
2250
+ * @param {CP.ColorPalette | string[]} colorsSource
2251
+ * @param {string} menuClass
2252
+ * @returns {HTMLElement | Element}
2253
+ */
2254
+ function getColorMenu(self, colorsSource, menuClass) {
2255
+ const { input, format, componentLabels } = self;
2256
+ const { defaultsLabel, presetsLabel } = componentLabels;
2257
+ const isOptionsMenu = menuClass === 'color-options';
2258
+ const isPalette = colorsSource instanceof ColorPalette;
2259
+ const menuLabel = isOptionsMenu ? presetsLabel : defaultsLabel;
2260
+ let colorsArray = isPalette ? colorsSource.colors : colorsSource;
2261
+ colorsArray = colorsArray instanceof Array ? colorsArray : [];
2262
+ const colorsCount = colorsArray.length;
2263
+ const { lightSteps } = isPalette ? colorsSource : { lightSteps: null };
2264
+ let fit = lightSteps
2265
+ || Math.max(...[5, 6, 7, 8, 9, 10].filter((x) => colorsCount > (x * 2) && !(colorsCount % x)));
2266
+ fit = Number.isFinite(fit) ? fit : 5;
2267
+ const isMultiLine = isOptionsMenu && colorsCount > fit;
2268
+ let rowCountHover = 1;
2269
+ rowCountHover = isMultiLine && colorsCount < 27 ? 2 : rowCountHover;
2270
+ rowCountHover = colorsCount >= 27 ? 3 : rowCountHover;
2271
+ rowCountHover = colorsCount >= 36 ? 4 : rowCountHover;
2272
+ rowCountHover = colorsCount >= 45 ? 5 : rowCountHover;
2273
+ const rowCount = rowCountHover - (colorsCount < 27 ? 1 : 2);
2274
+ const isScrollable = isMultiLine && colorsCount > rowCountHover * fit;
2275
+ let finalClass = menuClass;
2276
+ finalClass += isScrollable ? ' scrollable' : '';
2277
+ finalClass += isMultiLine ? ' multiline' : '';
2278
+ const gap = isMultiLine ? '1px' : '0.25rem';
2279
+ let optionSize = isMultiLine ? 1.75 : 2;
2280
+ optionSize = !(colorsCount % 10) && isMultiLine ? 1.5 : optionSize;
2281
+ const menuHeight = `${(rowCount || 1) * optionSize}rem`;
2282
+ const menuHeightHover = `calc(${rowCountHover} * ${optionSize}rem + ${rowCountHover - 1} * ${gap})`;
2283
+ const gridTemplateColumns = `repeat(${fit}, ${optionSize}rem)`;
2284
+ const gridTemplateRows = `repeat(auto-fill, ${optionSize}rem)`;
2285
+
2286
+ const menu = createElement({
2287
+ tagName: 'ul',
2288
+ className: finalClass,
2289
+ });
2290
+ setAttribute(menu, 'role', 'listbox');
2291
+ setAttribute(menu, ariaLabel, `${menuLabel}`);
2292
+
2293
+ if (isOptionsMenu) {
2294
+ if (isScrollable) {
2295
+ const styleText = 'this.style.height=';
2296
+ setAttribute(menu, 'onmouseout', `${styleText}'${menuHeight}'`);
2297
+ setAttribute(menu, 'onmouseover', `${styleText}'${menuHeightHover}'`);
2298
+ }
2299
+ const menuStyle = {
2300
+ height: isScrollable ? menuHeight : '', gridTemplateColumns, gridTemplateRows, gap,
2301
+ };
2302
+ setElementStyle(menu, menuStyle);
2303
+ }
2304
+
2305
+ colorsArray.forEach((x) => {
2306
+ const [value, label] = x.trim().split(':');
2307
+ const xRealColor = new Color(value, format).toString();
2308
+ const isActive = xRealColor === getAttribute(input, 'value');
2309
+ const active = isActive ? ' active' : '';
2310
+
2311
+ const option = createElement({
2312
+ tagName: 'li',
2313
+ className: `color-option${active}`,
2314
+ innerText: `${label || x}`,
2315
+ });
2316
+
2317
+ setAttribute(option, 'tabindex', '0');
2318
+ setAttribute(option, 'data-value', `${value}`);
2319
+ setAttribute(option, 'role', 'option');
2320
+ setAttribute(option, ariaSelected, isActive ? 'true' : 'false');
2321
+
2322
+ if (isOptionsMenu) {
2323
+ setElementStyle(option, {
2324
+ width: `${optionSize}rem`, height: `${optionSize}rem`, backgroundColor: x,
2325
+ });
2326
+ }
2327
+
2328
+ menu.append(option);
2329
+ });
2330
+ return menu;
2331
+ }
2332
+
2333
+ /**
2334
+ * Check if a string is valid JSON string.
2335
+ * @param {string} str the string input
2336
+ * @returns {boolean} the query result
2337
+ */
2338
+ function isValidJSON(str) {
2339
+ try {
2340
+ JSON.parse(str);
2341
+ } catch (e) {
2342
+ return false;
2343
+ }
2344
+ return true;
2345
+ }
2346
+
2347
+ var version = "0.0.1alpha2";
2348
+
2349
+ // @ts-ignore
2350
+
2351
+ const Version = version;
2352
+
2353
+ // ColorPicker GC
2354
+ // ==============
2355
+ const colorPickerString = 'color-picker';
2356
+ const colorPickerSelector = `[data-function="${colorPickerString}"]`;
2357
+ const colorPickerParentSelector = `.${colorPickerString},${colorPickerString}`;
2358
+ const colorPickerDefaults = {
2359
+ componentLabels: colorPickerLabels,
2360
+ colorLabels: colorNames,
2361
+ format: 'rgb',
2362
+ colorPresets: undefined,
2363
+ colorKeywords: nonColors,
2364
+ };
2365
+
2366
+ // ColorPicker Static Methods
2367
+ // ==========================
2368
+
2369
+ /** @type {CP.GetInstance<ColorPicker>} */
2370
+ const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
2371
+
2372
+ /** @type {CP.InitCallback<ColorPicker>} */
2373
+ const initColorPicker = (element) => new ColorPicker(element);
2374
+
2375
+ // ColorPicker Private Methods
2376
+ // ===========================
2377
+
2378
+ /**
2379
+ * Generate HTML markup and update instance properties.
2380
+ * @param {ColorPicker} self
2381
+ */
2382
+ function initCallback(self) {
2383
+ const {
2384
+ input, parent, format, id, componentLabels, colorKeywords, colorPresets,
2385
+ } = self;
2386
+ const colorValue = getAttribute(input, 'value') || '#fff';
2387
+
2388
+ const {
2389
+ toggleLabel, pickerLabel, formatLabel, hexLabel,
2390
+ } = componentLabels;
2391
+
2392
+ // update color
2393
+ const color = nonColors.includes(colorValue) ? '#fff' : colorValue;
2394
+ self.color = new Color(color, format);
2395
+
2396
+ // set initial controls dimensions
2397
+ // make the controls smaller on mobile
2398
+ const dropClass = isMobile ? ' mobile' : '';
2399
+ const formatString = format === 'hex' ? hexLabel : format.toUpperCase();
2400
+
2401
+ const pickerBtn = createElement({
2402
+ id: `picker-btn-${id}`,
2403
+ tagName: 'button',
2404
+ className: 'picker-toggle btn-appearance',
2405
+ });
2406
+ setAttribute(pickerBtn, ariaExpanded, 'false');
2407
+ setAttribute(pickerBtn, ariaHasPopup, 'true');
2408
+ pickerBtn.append(createElement({
2409
+ tagName: 'span',
2410
+ className: vHidden,
2411
+ innerText: `${pickerLabel}. ${formatLabel}: ${formatString}`,
2412
+ }));
2413
+
2414
+ const pickerDropdown = createElement({
2415
+ tagName: 'div',
2416
+ className: `color-dropdown picker${dropClass}`,
2417
+ });
2418
+ setAttribute(pickerDropdown, ariaLabelledBy, `picker-btn-${id}`);
2419
+ setAttribute(pickerDropdown, 'role', 'group');
2420
+
2421
+ const colorControls = getColorControls(self);
2422
+ const colorForm = getColorForm(self);
2423
+
2424
+ pickerDropdown.append(colorControls, colorForm);
2425
+ input.before(pickerBtn);
2426
+ parent.append(pickerDropdown);
2427
+
2428
+ // set colour key menu template
2429
+ if (colorKeywords || colorPresets) {
2430
+ const presetsDropdown = createElement({
2431
+ tagName: 'div',
2432
+ className: `color-dropdown scrollable menu${dropClass}`,
2433
+ });
2434
+
2435
+ // color presets
2436
+ if ((colorPresets instanceof Array && colorPresets.length)
2437
+ || (colorPresets instanceof ColorPalette && colorPresets.colors)) {
2438
+ const presetsMenu = getColorMenu(self, colorPresets, 'color-options');
2439
+ presetsDropdown.append(presetsMenu);
2440
+ }
2441
+
2442
+ // explicit defaults [reset, initial, inherit, transparent, currentColor]
2443
+ if (colorKeywords && colorKeywords.length) {
2444
+ const keywordsMenu = getColorMenu(self, colorKeywords, 'color-defaults');
2445
+ presetsDropdown.append(keywordsMenu);
2446
+ }
2447
+
2448
+ const presetsBtn = createElement({
2449
+ tagName: 'button',
2450
+ className: 'menu-toggle btn-appearance',
2451
+ });
2452
+ setAttribute(presetsBtn, 'tabindex', '-1');
2453
+ setAttribute(presetsBtn, ariaExpanded, 'false');
2454
+ setAttribute(presetsBtn, ariaHasPopup, 'true');
2455
+
2456
+ const xmlns = encodeURI('http://www.w3.org/2000/svg');
2457
+ const presetsIcon = createElementNS(xmlns, { tagName: 'svg' });
2458
+ setAttribute(presetsIcon, 'xmlns', xmlns);
2459
+ setAttribute(presetsIcon, 'viewBox', '0 0 512 512');
2460
+ setAttribute(presetsIcon, ariaHidden, 'true');
2461
+
2462
+ const path = createElementNS(xmlns, { tagName: 'path' });
2463
+ setAttribute(path, 'd', 'M98,158l157,156L411,158l27,27L255,368L71,185L98,158z');
2464
+ setAttribute(path, 'fill', '#fff');
2465
+ presetsIcon.append(path);
2466
+ presetsBtn.append(createElement({
2467
+ tagName: 'span',
2468
+ className: vHidden,
2469
+ innerText: `${toggleLabel}`,
2470
+ }), presetsIcon);
2471
+
2472
+ parent.append(presetsBtn, presetsDropdown);
2473
+ }
2474
+
2475
+ // solve non-colors after settings save
2476
+ if (colorKeywords && nonColors.includes(colorValue)) {
2477
+ self.value = colorValue;
2478
+ }
2479
+ setAttribute(input, 'tabindex', '-1');
2480
+ }
2481
+
2482
+ /**
2483
+ * Add / remove `ColorPicker` main event listeners.
2484
+ * @param {ColorPicker} self
2485
+ * @param {boolean=} action
2486
+ */
2487
+ function toggleEvents(self, action) {
2488
+ const fn = action ? addListener : removeListener;
2489
+ const { input, pickerToggle, menuToggle } = self;
2490
+
2491
+ fn(input, focusinEvent, self.showPicker);
2492
+ fn(pickerToggle, mouseclickEvent, self.togglePicker);
2493
+
2494
+ fn(input, keydownEvent, self.keyToggle);
2495
+
2496
+ if (menuToggle) {
2497
+ fn(menuToggle, mouseclickEvent, self.toggleMenu);
2498
+ }
2499
+ }
2500
+
2501
+ /**
2502
+ * Add / remove `ColorPicker` event listeners active only when open.
2503
+ * @param {ColorPicker} self
2504
+ * @param {boolean=} action
2505
+ */
2506
+ function toggleEventsOnShown(self, action) {
2507
+ const fn = action ? addListener : removeListener;
2508
+ const { input, colorMenu, parent } = self;
2509
+ const doc = getDocument(input);
2510
+ const win = getWindow(input);
2511
+ const pointerEvents = `on${touchstartEvent}` in doc
2512
+ ? { down: touchstartEvent, move: touchmoveEvent, up: touchendEvent }
2513
+ : { down: mousedownEvent, move: mousemoveEvent, up: mouseupEvent };
2514
+
2515
+ fn(self.controls, pointerEvents.down, self.pointerDown);
2516
+ self.controlKnobs.forEach((x) => fn(x, keydownEvent, self.handleKnobs));
2517
+
2518
+ // @ts-ignore -- this is `Window`
2519
+ fn(win, scrollEvent, self.handleScroll);
2520
+ // @ts-ignore -- this is `Window`
2521
+ fn(win, resizeEvent, self.update);
2522
+
2523
+ [input, ...self.inputs].forEach((x) => fn(x, changeEvent, self.changeHandler));
2524
+
2525
+ if (colorMenu) {
2526
+ fn(colorMenu, mouseclickEvent, self.menuClickHandler);
2527
+ fn(colorMenu, keydownEvent, self.menuKeyHandler);
2528
+ }
2529
+
2530
+ fn(doc, pointerEvents.move, self.pointerMove);
2531
+ fn(doc, pointerEvents.up, self.pointerUp);
2532
+ fn(parent, focusoutEvent, self.handleFocusOut);
2533
+ // @ts-ignore -- this is `Window`
2534
+ fn(win, keyupEvent, self.handleDismiss);
2535
+ }
2536
+
2537
+ /**
2538
+ * Triggers the `ColorPicker` original event.
2539
+ * @param {ColorPicker} self
2540
+ */
2541
+ function firePickerChange(self) {
2542
+ dispatchEvent(self.input, new CustomEvent('colorpicker.change'));
2543
+ }
2544
+
2545
+ /**
2546
+ * Hides a visible dropdown.
2547
+ * @param {HTMLElement} element
2548
+ * @returns {void}
2549
+ */
2550
+ function removePosition(element) {
2551
+ if (element) {
2552
+ ['bottom', 'top'].forEach((x) => removeClass(element, x));
2553
+ }
2554
+ }
2555
+
2556
+ /**
2557
+ * Shows a `ColorPicker` dropdown and close the curent open dropdown.
2558
+ * @param {ColorPicker} self
2559
+ * @param {HTMLElement | Element} dropdown
2560
+ */
2561
+ function showDropdown(self, dropdown) {
2562
+ const {
2563
+ colorPicker, colorMenu, menuToggle, pickerToggle, parent,
2564
+ } = self;
2565
+ const isPicker = dropdown === colorPicker;
2566
+ const openDropdown = isPicker ? colorMenu : colorPicker;
2567
+ const activeBtn = isPicker ? menuToggle : pickerToggle;
2568
+ const nextBtn = !isPicker ? menuToggle : pickerToggle;
2569
+
2570
+ if (!hasClass(parent, 'open')) {
2571
+ addClass(parent, 'open');
2572
+ }
2573
+ if (openDropdown) {
2574
+ removeClass(openDropdown, 'show');
2575
+ removePosition(openDropdown);
2576
+ }
2577
+ addClass(dropdown, 'bottom');
2578
+ reflow(dropdown);
2579
+ addClass(dropdown, 'show');
2580
+ if (isPicker) self.update();
2581
+ self.show();
2582
+ setAttribute(nextBtn, ariaExpanded, 'true');
2583
+ if (activeBtn) {
2584
+ setAttribute(activeBtn, ariaExpanded, 'false');
2585
+ }
2586
+ }
2587
+
2588
+ /**
2589
+ * Color Picker Web Component
2590
+ * @see http://thednp.github.io/color-picker
2591
+ */
2592
+ class ColorPicker {
2593
+ /**
2594
+ * Returns a new `ColorPicker` instance. The target of this constructor
2595
+ * must be an `HTMLInputElement`.
2596
+ *
2597
+ * @param {HTMLInputElement | string} target the target `<input>` element
2598
+ * @param {CP.ColorPickerOptions=} config instance options
2599
+ */
2600
+ constructor(target, config) {
2601
+ const self = this;
2602
+ /** @type {HTMLInputElement} */
2603
+ // @ts-ignore
2604
+ const input = querySelector(target);
2605
+
2606
+ // invalidate
2607
+ if (!input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
2608
+ self.input = input;
2609
+
2610
+ const parent = closest(input, colorPickerParentSelector);
2611
+ if (!parent) throw new TypeError('ColorPicker requires a specific markup to work.');
2612
+
2613
+ /** @type {HTMLElement} */
2614
+ // @ts-ignore
2615
+ self.parent = parent;
2616
+
2617
+ /** @type {number} */
2618
+ self.id = getUID(input, colorPickerString);
2619
+
2620
+ // set initial state
2621
+ /** @type {HTMLElement?} */
2622
+ self.dragElement = null;
2623
+ /** @type {boolean} */
2624
+ self.isOpen = false;
2625
+ /** @type {Record<string, number>} */
2626
+ self.controlPositions = {
2627
+ c1x: 0, c1y: 0, c2y: 0, c3y: 0,
2628
+ };
2629
+ /** @type {Record<string, string>} */
2630
+ self.colorLabels = {};
2631
+ /** @type {string[]=} */
2632
+ self.colorKeywords = undefined;
2633
+ /** @type {(ColorPalette | string[])=} */
2634
+ self.colorPresets = undefined;
2635
+
2636
+ // process options
2637
+ const {
2638
+ format, componentLabels, colorLabels, colorKeywords, colorPresets,
2639
+ } = normalizeOptions(this.isCE ? parent : input, colorPickerDefaults, config || {});
2640
+
2641
+ let translatedColorLabels = colorNames;
2642
+ if (colorLabels instanceof Array && colorLabels.length === 17) {
2643
+ translatedColorLabels = colorLabels;
2644
+ } else if (colorLabels && colorLabels.split(',').length === 17) {
2645
+ translatedColorLabels = colorLabels.split(',');
2646
+ }
2647
+
2648
+ // expose colour labels to all methods
2649
+ colorNames.forEach((c, i) => {
2650
+ self.colorLabels[c] = translatedColorLabels[i].trim();
2651
+ });
2652
+
2653
+ // update and expose component labels
2654
+ const tempLabels = ObjectAssign({}, colorPickerLabels);
2655
+ const jsonLabels = componentLabels && isValidJSON(componentLabels)
2656
+ ? JSON.parse(componentLabels) : componentLabels || {};
2657
+
2658
+ /** @type {Record<string, string>} */
2659
+ self.componentLabels = ObjectAssign(tempLabels, jsonLabels);
2660
+
2661
+ /** @type {Color} */
2662
+ self.color = new Color('white', format);
2663
+
2664
+ /** @type {CP.ColorFormats} */
2665
+ self.format = format;
2666
+
2667
+ // set colour defaults
2668
+ if (colorKeywords instanceof Array) {
2669
+ self.colorKeywords = colorKeywords;
2670
+ } else if (typeof colorKeywords === 'string' && colorKeywords.length) {
2671
+ self.colorKeywords = colorKeywords.split(',');
2672
+ }
2673
+
2674
+ // set colour presets
2675
+ if (colorPresets instanceof Array) {
2676
+ self.colorPresets = colorPresets;
2677
+ } else if (typeof colorPresets === 'string' && colorPresets.length) {
2678
+ if (isValidJSON(colorPresets)) {
2679
+ const { hue, hueSteps, lightSteps } = JSON.parse(colorPresets);
2680
+ self.colorPresets = new ColorPalette(hue, hueSteps, lightSteps);
2681
+ } else {
2682
+ self.colorPresets = colorPresets.split(',').map((x) => x.trim());
2683
+ }
2684
+ }
2685
+
2686
+ // bind events
2687
+ self.showPicker = self.showPicker.bind(self);
2688
+ self.togglePicker = self.togglePicker.bind(self);
2689
+ self.toggleMenu = self.toggleMenu.bind(self);
2690
+ self.menuClickHandler = self.menuClickHandler.bind(self);
2691
+ self.menuKeyHandler = self.menuKeyHandler.bind(self);
2692
+ self.pointerDown = self.pointerDown.bind(self);
2693
+ self.pointerMove = self.pointerMove.bind(self);
2694
+ self.pointerUp = self.pointerUp.bind(self);
2695
+ self.update = self.update.bind(self);
2696
+ self.handleScroll = self.handleScroll.bind(self);
2697
+ self.handleFocusOut = self.handleFocusOut.bind(self);
2698
+ self.changeHandler = self.changeHandler.bind(self);
2699
+ self.handleDismiss = self.handleDismiss.bind(self);
2700
+ self.keyToggle = self.keyToggle.bind(self);
2701
+ self.handleKnobs = self.handleKnobs.bind(self);
2702
+
2703
+ // generate markup
2704
+ initCallback(self);
2705
+
2706
+ const [colorPicker, colorMenu] = getElementsByClassName('color-dropdown', parent);
2707
+ // set main elements
2708
+ /** @type {HTMLElement} */
2709
+ // @ts-ignore
2710
+ self.pickerToggle = querySelector('.picker-toggle', parent);
2711
+ /** @type {HTMLElement} */
2712
+ // @ts-ignore
2713
+ self.menuToggle = querySelector('.menu-toggle', parent);
2714
+ /** @type {HTMLElement} */
2715
+ // @ts-ignore
2716
+ self.colorPicker = colorPicker;
2717
+ /** @type {HTMLElement} */
2718
+ // @ts-ignore
2719
+ self.colorMenu = colorMenu;
2720
+ /** @type {HTMLInputElement[]} */
2721
+ // @ts-ignore
2722
+ self.inputs = [...getElementsByClassName('color-input', parent)];
2723
+ const [controls] = getElementsByClassName('color-controls', parent);
2724
+ self.controls = controls;
2725
+ /** @type {(HTMLElement | Element)[]} */
2726
+ self.controlKnobs = [...getElementsByClassName('knob', controls)];
2727
+ /** @type {(HTMLElement)[]} */
2728
+ // @ts-ignore
2729
+ self.visuals = [...getElementsByClassName('visual-control', controls)];
2730
+
2731
+ // update colour picker controls, inputs and visuals
2732
+ self.update();
2733
+
2734
+ // add main events listeners
2735
+ toggleEvents(self, true);
2736
+
2737
+ // set component data
2738
+ Data.set(input, colorPickerString, self);
2739
+ }
2740
+
2741
+ /** Returns the current colour value */
2742
+ get value() { return this.input.value; }
2743
+
2744
+ /**
2745
+ * Sets a new colour value.
2746
+ * @param {string} v new colour value
2747
+ */
2748
+ set value(v) { this.input.value = v; }
2749
+
2750
+ /** Check if the colour presets include any non-colour. */
2751
+ get includeNonColor() {
2752
+ return this.colorKeywords instanceof Array
2753
+ && this.colorKeywords.some((x) => nonColors.includes(x));
2754
+ }
2755
+
2756
+ /** Check if the parent of the target is a `ColorPickerElement` instance. */
2757
+ get isCE() { return this.parent.localName === colorPickerString; }
2758
+
2759
+ /** Returns hexadecimal value of the current colour. */
2760
+ get hex() { return this.color.toHex(true); }
2761
+
2762
+ /** Returns the current colour value in {h,s,v,a} object format. */
2763
+ get hsv() { return this.color.toHsv(); }
2764
+
2765
+ /** Returns the current colour value in {h,s,l,a} object format. */
2766
+ get hsl() { return this.color.toHsl(); }
2767
+
2768
+ /** Returns the current colour value in {h,w,b,a} object format. */
2769
+ get hwb() { return this.color.toHwb(); }
2770
+
2771
+ /** Returns the current colour value in {r,g,b,a} object format. */
2772
+ get rgb() { return this.color.toRgb(); }
2773
+
2774
+ /** Returns the current colour brightness. */
2775
+ get brightness() { return this.color.brightness; }
2776
+
2777
+ /** Returns the current colour luminance. */
2778
+ get luminance() { return this.color.luminance; }
2779
+
2780
+ /** Checks if the current colour requires a light text colour. */
2781
+ get isDark() {
2782
+ const { color, brightness } = this;
2783
+ return brightness < 120 && color.a > 0.33;
2784
+ }
2785
+
2786
+ /** Checks if the current input value is a valid colour. */
2787
+ get isValid() {
2788
+ const inputValue = this.input.value;
2789
+ return inputValue !== '' && new Color(inputValue).isValid;
2790
+ }
2791
+
2792
+ /** Updates `ColorPicker` visuals. */
2793
+ updateVisuals() {
2794
+ const self = this;
2795
+ const {
2796
+ format, controlPositions, visuals,
2797
+ } = self;
2798
+ const [v1, v2, v3] = visuals;
2799
+ const { offsetWidth, offsetHeight } = v1;
2800
+ const hue = format === 'hsl'
2801
+ ? controlPositions.c1x / offsetWidth
2802
+ : controlPositions.c2y / offsetHeight;
2803
+ // @ts-ignore - `hslToRgb` is assigned to `Color` as static method
2804
+ const { r, g, b } = Color.hslToRgb(hue, 1, 0.5);
2805
+ const whiteGrad = 'linear-gradient(rgb(255,255,255) 0%, rgb(255,255,255) 100%)';
2806
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
2807
+ const roundA = Math.round((alpha * 100)) / 100;
2808
+
2809
+ if (format !== 'hsl') {
2810
+ const fill = new Color({
2811
+ h: hue, s: 1, l: 0.5, a: alpha,
2812
+ }).toRgbString();
2813
+ const hueGradient = `linear-gradient(
2814
+ rgb(255,0,0) 0%, rgb(255,255,0) 16.67%,
2815
+ rgb(0,255,0) 33.33%, rgb(0,255,255) 50%,
2816
+ rgb(0,0,255) 66.67%, rgb(255,0,255) 83.33%,
2817
+ rgb(255,0,0) 100%)`;
2818
+ setElementStyle(v1, {
2819
+ background: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,${roundA}) 100%),
2820
+ linear-gradient(to right, rgba(255,255,255,${roundA}) 0%, ${fill} 100%),
2821
+ ${whiteGrad}`,
2822
+ });
2823
+ setElementStyle(v2, { background: hueGradient });
2824
+ } else {
2825
+ const saturation = Math.round((controlPositions.c2y / offsetHeight) * 100);
2826
+ const fill0 = new Color({
2827
+ r: 255, g: 0, b: 0, a: alpha,
2828
+ }).saturate(-saturation).toRgbString();
2829
+ const fill1 = new Color({
2830
+ r: 255, g: 255, b: 0, a: alpha,
2831
+ }).saturate(-saturation).toRgbString();
2832
+ const fill2 = new Color({
2833
+ r: 0, g: 255, b: 0, a: alpha,
2834
+ }).saturate(-saturation).toRgbString();
2835
+ const fill3 = new Color({
2836
+ r: 0, g: 255, b: 255, a: alpha,
2837
+ }).saturate(-saturation).toRgbString();
2838
+ const fill4 = new Color({
2839
+ r: 0, g: 0, b: 255, a: alpha,
2840
+ }).saturate(-saturation).toRgbString();
2841
+ const fill5 = new Color({
2842
+ r: 255, g: 0, b: 255, a: alpha,
2843
+ }).saturate(-saturation).toRgbString();
2844
+ const fill6 = new Color({
2845
+ r: 255, g: 0, b: 0, a: alpha,
2846
+ }).saturate(-saturation).toRgbString();
2847
+ const fillGradient = `linear-gradient(to right,
2848
+ ${fill0} 0%, ${fill1} 16.67%, ${fill2} 33.33%, ${fill3} 50%,
2849
+ ${fill4} 66.67%, ${fill5} 83.33%, ${fill6} 100%)`;
2850
+ const lightGrad = `linear-gradient(rgba(255,255,255,${roundA}) 0%, rgba(255,255,255,0) 50%),
2851
+ linear-gradient(rgba(0,0,0,0) 50%, rgba(0,0,0,${roundA}) 100%)`;
2852
+
2853
+ setElementStyle(v1, { background: `${lightGrad},${fillGradient},${whiteGrad}` });
2854
+ const {
2855
+ r: gr, g: gg, b: gb,
2856
+ } = new Color({ r, g, b }).greyscale().toRgb();
2857
+
2858
+ setElementStyle(v2, {
2859
+ background: `linear-gradient(rgb(${r},${g},${b}) 0%, rgb(${gr},${gg},${gb}) 100%)`,
2860
+ });
2861
+ }
2862
+ setElementStyle(v3, {
2863
+ background: `linear-gradient(rgba(${r},${g},${b},1) 0%,rgba(${r},${g},${b},0) 100%)`,
2864
+ });
2865
+ }
2866
+
2867
+ /**
2868
+ * The `ColorPicker` *focusout* event listener when open.
2869
+ * @param {FocusEvent} e
2870
+ * @this {ColorPicker}
2871
+ */
2872
+ handleFocusOut({ relatedTarget }) {
2873
+ // @ts-ignore
2874
+ if (relatedTarget && !this.parent.contains(relatedTarget)) {
2875
+ this.hide(true);
2876
+ }
2877
+ }
2878
+
2879
+ /**
2880
+ * The `ColorPicker` *keyup* event listener when open.
2881
+ * @param {KeyboardEvent} e
2882
+ * @this {ColorPicker}
2883
+ */
2884
+ handleDismiss({ code }) {
2885
+ const self = this;
2886
+ if (self.isOpen && code === keyEscape) {
2887
+ self.hide();
2888
+ }
2889
+ }
2890
+
2891
+ /**
2892
+ * The `ColorPicker` *scroll* event listener when open.
2893
+ * @param {Event} e
2894
+ * @this {ColorPicker}
2895
+ */
2896
+ handleScroll(e) {
2897
+ const self = this;
2898
+ const { activeElement } = getDocument(self.input);
2899
+
2900
+ if ((isMobile && self.dragElement)
2901
+ || (activeElement && self.controlKnobs.includes(activeElement))) {
2902
+ e.stopPropagation();
2903
+ e.preventDefault();
2904
+ }
2905
+
2906
+ self.updateDropdownPosition();
2907
+ }
2908
+
2909
+ /**
2910
+ * The `ColorPicker` keyboard event listener for menu navigation.
2911
+ * @param {KeyboardEvent} e
2912
+ * @this {ColorPicker}
2913
+ */
2914
+ menuKeyHandler(e) {
2915
+ const { target, code } = e;
2916
+ // @ts-ignore
2917
+ const { previousElementSibling, nextElementSibling, parentElement } = target;
2918
+ const isColorOptionsMenu = parentElement && hasClass(parentElement, 'color-options');
2919
+ const allSiblings = [...parentElement.children];
2920
+ const columnsCount = isColorOptionsMenu
2921
+ && getElementStyle(parentElement, 'grid-template-columns').split(' ').length;
2922
+ const currentIndex = allSiblings.indexOf(target);
2923
+ const previousElement = currentIndex > -1
2924
+ && columnsCount && allSiblings[currentIndex - columnsCount];
2925
+ const nextElement = currentIndex > -1
2926
+ && columnsCount && allSiblings[currentIndex + columnsCount];
2927
+
2928
+ if ([keyArrowDown, keyArrowUp, keySpace].includes(code)) {
2929
+ // prevent scroll when navigating the menu via arrow keys / Space
2930
+ e.preventDefault();
2931
+ }
2932
+ if (isColorOptionsMenu) {
2933
+ if (previousElement && code === keyArrowUp) {
2934
+ focus(previousElement);
2935
+ } else if (nextElement && code === keyArrowDown) {
2936
+ focus(nextElement);
2937
+ } else if (previousElementSibling && code === keyArrowLeft) {
2938
+ focus(previousElementSibling);
2939
+ } else if (nextElementSibling && code === keyArrowRight) {
2940
+ focus(nextElementSibling);
2941
+ }
2942
+ } else if (previousElementSibling && [keyArrowLeft, keyArrowUp].includes(code)) {
2943
+ focus(previousElementSibling);
2944
+ } else if (nextElementSibling && [keyArrowRight, keyArrowDown].includes(code)) {
2945
+ focus(nextElementSibling);
2946
+ }
2947
+
2948
+ if ([keyEnter, keySpace].includes(code)) {
2949
+ this.menuClickHandler({ target });
2950
+ }
2951
+ }
2952
+
2953
+ /**
2954
+ * The `ColorPicker` click event listener for the colour menu presets / defaults.
2955
+ * @param {Partial<Event>} e
2956
+ * @this {ColorPicker}
2957
+ */
2958
+ menuClickHandler(e) {
2959
+ const self = this;
2960
+ /** @type {*} */
2961
+ const { target } = e;
2962
+ const { colorMenu } = self;
2963
+ const newOption = (getAttribute(target, 'data-value') || '').trim();
2964
+ // invalidate for targets other than color options
2965
+ if (!newOption.length) return;
2966
+ const currentActive = querySelector('li.active', colorMenu);
2967
+ let newColor = nonColors.includes(newOption) ? 'white' : newOption;
2968
+ newColor = newOption === 'transparent' ? 'rgba(0,0,0,0)' : newOption;
2969
+
2970
+ const {
2971
+ r, g, b, a,
2972
+ } = new Color(newColor);
2973
+
2974
+ ObjectAssign(self.color, {
2975
+ r, g, b, a,
2976
+ });
2977
+
2978
+ self.update();
2979
+
2980
+ if (currentActive) {
2981
+ removeClass(currentActive, 'active');
2982
+ removeAttribute(currentActive, ariaSelected);
2983
+ }
2984
+
2985
+ if (currentActive !== target) {
2986
+ addClass(target, 'active');
2987
+ setAttribute(target, ariaSelected, 'true');
2988
+
2989
+ if (nonColors.includes(newOption)) {
2990
+ self.value = newOption;
2991
+ }
2992
+ firePickerChange(self);
2993
+ }
2994
+ }
2995
+
2996
+ /**
2997
+ * The `ColorPicker` *touchstart* / *mousedown* events listener for control knobs.
2998
+ * @param {TouchEvent} e
2999
+ * @this {ColorPicker}
3000
+ */
3001
+ pointerDown(e) {
3002
+ const self = this;
3003
+ /** @type {*} */
3004
+ const {
3005
+ type, target, touches, pageX, pageY,
3006
+ } = e;
3007
+ const { colorMenu, visuals, controlKnobs } = self;
3008
+ const [v1, v2, v3] = visuals;
3009
+ const [c1, c2, c3] = controlKnobs;
3010
+ /** @type {HTMLElement} */
3011
+ const visual = hasClass(target, 'visual-control')
3012
+ ? target : querySelector('.visual-control', target.parentElement);
3013
+ const visualRect = getBoundingClientRect(visual);
3014
+ const X = type === 'touchstart' ? touches[0].pageX : pageX;
3015
+ const Y = type === 'touchstart' ? touches[0].pageY : pageY;
3016
+ const offsetX = X - window.pageXOffset - visualRect.left;
3017
+ const offsetY = Y - window.pageYOffset - visualRect.top;
3018
+
3019
+ if (target === v1 || target === c1) {
3020
+ self.dragElement = visual;
3021
+ self.changeControl1(offsetX, offsetY);
3022
+ } else if (target === v2 || target === c2) {
3023
+ self.dragElement = visual;
3024
+ self.changeControl2(offsetY);
3025
+ } else if (target === v3 || target === c3) {
3026
+ self.dragElement = visual;
3027
+ self.changeAlpha(offsetY);
3028
+ }
3029
+
3030
+ if (colorMenu) {
3031
+ const currentActive = querySelector('li.active', colorMenu);
3032
+ if (currentActive) {
3033
+ removeClass(currentActive, 'active');
3034
+ removeAttribute(currentActive, ariaSelected);
3035
+ }
3036
+ }
3037
+ e.preventDefault();
3038
+ }
3039
+
3040
+ /**
3041
+ * The `ColorPicker` *touchend* / *mouseup* events listener for control knobs.
3042
+ * @param {TouchEvent} e
3043
+ * @this {ColorPicker}
3044
+ */
3045
+ pointerUp({ target }) {
3046
+ const self = this;
3047
+ const { parent } = self;
3048
+ const doc = getDocument(parent);
3049
+ const currentOpen = querySelector(`${colorPickerParentSelector}.open`, doc) !== null;
3050
+ const selection = doc.getSelection();
3051
+ // @ts-ignore
3052
+ if (!self.dragElement && !selection.toString().length
3053
+ // @ts-ignore
3054
+ && !parent.contains(target)) {
3055
+ self.hide(currentOpen);
3056
+ }
3057
+
3058
+ self.dragElement = null;
3059
+ }
3060
+
3061
+ /**
3062
+ * The `ColorPicker` *touchmove* / *mousemove* events listener for control knobs.
3063
+ * @param {TouchEvent} e
3064
+ */
3065
+ pointerMove(e) {
3066
+ const self = this;
3067
+ const { dragElement, visuals } = self;
3068
+ const [v1, v2, v3] = visuals;
3069
+ const {
3070
+ // @ts-ignore
3071
+ type, touches, pageX, pageY,
3072
+ } = e;
3073
+
3074
+ if (!dragElement) return;
3075
+
3076
+ const controlRect = getBoundingClientRect(dragElement);
3077
+ const X = type === 'touchmove' ? touches[0].pageX : pageX;
3078
+ const Y = type === 'touchmove' ? touches[0].pageY : pageY;
3079
+ const offsetX = X - window.pageXOffset - controlRect.left;
3080
+ const offsetY = Y - window.pageYOffset - controlRect.top;
3081
+
3082
+ if (dragElement === v1) {
3083
+ self.changeControl1(offsetX, offsetY);
3084
+ }
3085
+
3086
+ if (dragElement === v2) {
3087
+ self.changeControl2(offsetY);
3088
+ }
3089
+
3090
+ if (dragElement === v3) {
3091
+ self.changeAlpha(offsetY);
3092
+ }
3093
+ }
3094
+
3095
+ /**
3096
+ * The `ColorPicker` *keydown* event listener for control knobs.
3097
+ * @param {KeyboardEvent} e
3098
+ */
3099
+ handleKnobs(e) {
3100
+ const { target, code } = e;
3101
+ const self = this;
3102
+
3103
+ // only react to arrow buttons
3104
+ if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
3105
+ e.preventDefault();
3106
+
3107
+ const { controlKnobs } = self;
3108
+ const [c1, c2, c3] = controlKnobs;
3109
+ const { activeElement } = getDocument(c1);
3110
+ const currentKnob = controlKnobs.find((x) => x === activeElement);
3111
+
3112
+ if (currentKnob) {
3113
+ let offsetX = 0;
3114
+ let offsetY = 0;
3115
+ if (target === c1) {
3116
+ if ([keyArrowLeft, keyArrowRight].includes(code)) {
3117
+ self.controlPositions.c1x += code === keyArrowRight ? +1 : -1;
3118
+ } else if ([keyArrowUp, keyArrowDown].includes(code)) {
3119
+ self.controlPositions.c1y += code === keyArrowDown ? +1 : -1;
3120
+ }
3121
+
3122
+ offsetX = self.controlPositions.c1x;
3123
+ offsetY = self.controlPositions.c1y;
3124
+ self.changeControl1(offsetX, offsetY);
3125
+ } else if (target === c2) {
3126
+ self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3127
+ offsetY = self.controlPositions.c2y;
3128
+ self.changeControl2(offsetY);
3129
+ } else if (target === c3) {
3130
+ self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
3131
+ offsetY = self.controlPositions.c3y;
3132
+ self.changeAlpha(offsetY);
3133
+ }
3134
+ self.handleScroll(e);
3135
+ }
3136
+ }
3137
+
3138
+ /** The event listener of the colour form inputs. */
3139
+ changeHandler() {
3140
+ const self = this;
3141
+ let colorSource;
3142
+ const {
3143
+ inputs, format, value: currentValue, input, controlPositions, visuals,
3144
+ } = self;
3145
+ /** @type {*} */
3146
+ const { activeElement } = getDocument(input);
3147
+ const { offsetHeight } = visuals[0];
3148
+ const [i1,,, i4] = inputs;
3149
+ const [v1, v2, v3, v4] = format === 'rgb'
3150
+ ? inputs.map((i) => parseFloat(i.value) / (i === i4 ? 100 : 1))
3151
+ : inputs.map((i) => parseFloat(i.value) / (i !== i1 ? 100 : 360));
3152
+ const isNonColorValue = self.includeNonColor && nonColors.includes(currentValue);
3153
+ const alpha = i4 ? v4 : (1 - controlPositions.c3y / offsetHeight);
3154
+
3155
+ if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
3156
+ if (activeElement === input) {
3157
+ if (isNonColorValue) {
3158
+ colorSource = 'white';
3159
+ } else {
3160
+ colorSource = currentValue;
3161
+ }
3162
+ } else if (format === 'hex') {
3163
+ colorSource = i1.value;
3164
+ } else if (format === 'hsl') {
3165
+ colorSource = {
3166
+ h: v1, s: v2, l: v3, a: alpha,
3167
+ };
3168
+ } else if (format === 'hwb') {
3169
+ colorSource = {
3170
+ h: v1, w: v2, b: v3, a: alpha,
3171
+ };
3172
+ } else {
3173
+ colorSource = {
3174
+ r: v1, g: v2, b: v3, a: alpha,
3175
+ };
3176
+ }
3177
+
3178
+ const {
3179
+ r, g, b, a,
3180
+ } = new Color(colorSource);
3181
+
3182
+ ObjectAssign(self.color, {
3183
+ r, g, b, a,
3184
+ });
3185
+ self.setControlPositions();
3186
+ self.updateAppearance();
3187
+ self.updateInputs();
3188
+ self.updateControls();
3189
+ self.updateVisuals();
3190
+
3191
+ // set non-color keyword
3192
+ if (activeElement === input && isNonColorValue) {
3193
+ self.value = currentValue;
3194
+ }
3195
+ }
3196
+ }
3197
+
3198
+ /**
3199
+ * Updates `ColorPicker` first control:
3200
+ * * `lightness` and `saturation` for HEX/RGB;
3201
+ * * `lightness` and `hue` for HSL.
3202
+ *
3203
+ * @param {number} X the X component of the offset
3204
+ * @param {number} Y the Y component of the offset
3205
+ */
3206
+ changeControl1(X, Y) {
3207
+ const self = this;
3208
+ let [offsetX, offsetY] = [0, 0];
3209
+ const {
3210
+ format, controlPositions, visuals,
3211
+ } = self;
3212
+ const { offsetHeight, offsetWidth } = visuals[0];
3213
+
3214
+ if (X > offsetWidth) offsetX = offsetWidth;
3215
+ else if (X >= 0) offsetX = X;
3216
+
3217
+ if (Y > offsetHeight) offsetY = offsetHeight;
3218
+ else if (Y >= 0) offsetY = Y;
3219
+
3220
+ const hue = format === 'hsl'
3221
+ ? offsetX / offsetWidth
3222
+ : controlPositions.c2y / offsetHeight;
3223
+
3224
+ const saturation = format === 'hsl'
3225
+ ? 1 - controlPositions.c2y / offsetHeight
3226
+ : offsetX / offsetWidth;
3227
+
3228
+ const lightness = 1 - offsetY / offsetHeight;
3229
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
3230
+
3231
+ const colorObject = format === 'hsl'
3232
+ ? {
3233
+ h: hue, s: saturation, l: lightness, a: alpha,
3234
+ }
3235
+ : {
3236
+ h: hue, s: saturation, v: lightness, a: alpha,
3237
+ };
3238
+
3239
+ // new color
3240
+ const {
3241
+ r, g, b, a,
3242
+ } = new Color(colorObject);
3243
+
3244
+ ObjectAssign(self.color, {
3245
+ r, g, b, a,
3246
+ });
3247
+
3248
+ // new positions
3249
+ self.controlPositions.c1x = offsetX;
3250
+ self.controlPositions.c1y = offsetY;
3251
+
3252
+ // update color picker
3253
+ self.updateAppearance();
3254
+ self.updateInputs();
3255
+ self.updateControls();
3256
+ self.updateVisuals();
3257
+ }
3258
+
3259
+ /**
3260
+ * Updates `ColorPicker` second control:
3261
+ * * `hue` for HEX/RGB/HWB;
3262
+ * * `saturation` for HSL.
3263
+ *
3264
+ * @param {number} Y the Y offset
3265
+ */
3266
+ changeControl2(Y) {
3267
+ const self = this;
3268
+ const {
3269
+ format, controlPositions, visuals,
3270
+ } = self;
3271
+ const { offsetHeight, offsetWidth } = visuals[0];
3272
+
3273
+ let offsetY = 0;
3274
+
3275
+ if (Y > offsetHeight) offsetY = offsetHeight;
3276
+ else if (Y >= 0) offsetY = Y;
3277
+
3278
+ const hue = format === 'hsl'
3279
+ ? controlPositions.c1x / offsetWidth
3280
+ : offsetY / offsetHeight;
3281
+ const saturation = format === 'hsl'
3282
+ ? 1 - offsetY / offsetHeight
3283
+ : controlPositions.c1x / offsetWidth;
3284
+ const lightness = 1 - controlPositions.c1y / offsetHeight;
3285
+ const alpha = 1 - controlPositions.c3y / offsetHeight;
3286
+ const colorObject = format === 'hsl'
3287
+ ? {
3288
+ h: hue, s: saturation, l: lightness, a: alpha,
3289
+ }
3290
+ : {
3291
+ h: hue, s: saturation, v: lightness, a: alpha,
3292
+ };
3293
+
3294
+ // new color
3295
+ const {
3296
+ r, g, b, a,
3297
+ } = new Color(colorObject);
3298
+
3299
+ ObjectAssign(self.color, {
3300
+ r, g, b, a,
3301
+ });
3302
+
3303
+ // new position
3304
+ self.controlPositions.c2y = offsetY;
3305
+ // update color picker
3306
+ self.updateAppearance();
3307
+ self.updateInputs();
3308
+ self.updateControls();
3309
+ self.updateVisuals();
3310
+ }
3311
+
3312
+ /**
3313
+ * Updates `ColorPicker` last control,
3314
+ * the `alpha` channel.
3315
+ *
3316
+ * @param {number} Y
3317
+ */
3318
+ changeAlpha(Y) {
3319
+ const self = this;
3320
+ const { visuals } = self;
3321
+ const { offsetHeight } = visuals[0];
3322
+ let offsetY = 0;
3323
+
3324
+ if (Y > offsetHeight) offsetY = offsetHeight;
3325
+ else if (Y >= 0) offsetY = Y;
3326
+
3327
+ // update color alpha
3328
+ const alpha = 1 - offsetY / offsetHeight;
3329
+ self.color.setAlpha(alpha);
3330
+ // update position
3331
+ self.controlPositions.c3y = offsetY;
3332
+ // update color picker
3333
+ self.updateAppearance();
3334
+ self.updateInputs();
3335
+ self.updateControls();
3336
+ self.updateVisuals();
3337
+ }
3338
+
3339
+ /**
3340
+ * Updates `ColorPicker` control positions on:
3341
+ * * initialization
3342
+ * * window resize
3343
+ */
3344
+ update() {
3345
+ const self = this;
3346
+ self.updateDropdownPosition();
3347
+ self.updateAppearance();
3348
+ self.setControlPositions();
3349
+ self.updateInputs(true);
3350
+ self.updateControls();
3351
+ self.updateVisuals();
3352
+ }
3353
+
3354
+ /** Updates the open dropdown position on *scroll* event. */
3355
+ updateDropdownPosition() {
3356
+ const self = this;
3357
+ const { input, colorPicker, colorMenu } = self;
3358
+ const elRect = getBoundingClientRect(input);
3359
+ const { top, bottom } = elRect;
3360
+ const { offsetHeight: elHeight } = input;
3361
+ const windowHeight = getDocumentElement(input).clientHeight;
3362
+ const isPicker = hasClass(colorPicker, 'show');
3363
+ const dropdown = isPicker ? colorPicker : colorMenu;
3364
+ if (!dropdown) return;
3365
+ const { offsetHeight: dropHeight } = dropdown;
3366
+ const distanceBottom = windowHeight - bottom;
3367
+ const distanceTop = top;
3368
+ const bottomExceed = top + dropHeight + elHeight > windowHeight; // show
3369
+ const topExceed = top - dropHeight < 0; // show-top
3370
+
3371
+ if ((hasClass(dropdown, 'bottom') || !topExceed) && distanceBottom < distanceTop && bottomExceed) {
3372
+ removeClass(dropdown, 'bottom');
3373
+ addClass(dropdown, 'top');
3374
+ } else {
3375
+ removeClass(dropdown, 'top');
3376
+ addClass(dropdown, 'bottom');
3377
+ }
3378
+ }
3379
+
3380
+ /** Updates control knobs' positions. */
3381
+ setControlPositions() {
3382
+ const self = this;
3383
+ const {
3384
+ format, visuals, color, hsl, hsv,
3385
+ } = self;
3386
+ const { offsetHeight, offsetWidth } = visuals[0];
3387
+ const alpha = color.a;
3388
+ const hue = hsl.h;
3389
+
3390
+ const saturation = format !== 'hsl' ? hsv.s : hsl.s;
3391
+ const lightness = format !== 'hsl' ? hsv.v : hsl.l;
3392
+
3393
+ self.controlPositions.c1x = format !== 'hsl' ? saturation * offsetWidth : hue * offsetWidth;
3394
+ self.controlPositions.c1y = (1 - lightness) * offsetHeight;
3395
+ self.controlPositions.c2y = format !== 'hsl' ? hue * offsetHeight : (1 - saturation) * offsetHeight;
3396
+ self.controlPositions.c3y = (1 - alpha) * offsetHeight;
3397
+ }
3398
+
3399
+ /** Update the visual appearance label and control knob labels. */
3400
+ updateAppearance() {
3401
+ const self = this;
3402
+ const {
3403
+ componentLabels, colorLabels, color, parent,
3404
+ hsl, hsv, hex, format, controlKnobs,
3405
+ } = self;
3406
+ const {
3407
+ appearanceLabel, hexLabel, valueLabel,
3408
+ } = componentLabels;
3409
+ const { r, g, b } = color.toRgb();
3410
+ const [knob1, knob2, knob3] = controlKnobs;
3411
+ const hue = Math.round(hsl.h * 360);
3412
+ const alpha = color.a;
3413
+ const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
3414
+ const saturation = Math.round(saturationSource * 100);
3415
+ const lightness = Math.round(hsl.l * 100);
3416
+ const hsvl = hsv.v * 100;
3417
+ let colorName;
3418
+
3419
+ // determine color appearance
3420
+ if (lightness === 100 && saturation === 0) {
3421
+ colorName = colorLabels.white;
3422
+ } else if (lightness === 0) {
3423
+ colorName = colorLabels.black;
3424
+ } else if (saturation === 0) {
3425
+ colorName = colorLabels.grey;
3426
+ } else if (hue < 15 || hue >= 345) {
3427
+ colorName = colorLabels.red;
3428
+ } else if (hue >= 15 && hue < 45) {
3429
+ colorName = hsvl > 80 && saturation > 80 ? colorLabels.orange : colorLabels.brown;
3430
+ } else if (hue >= 45 && hue < 75) {
3431
+ const isGold = hue > 46 && hue < 54 && hsvl < 80 && saturation > 90;
3432
+ const isOlive = hue >= 54 && hue < 75 && hsvl < 80;
3433
+ colorName = isGold ? colorLabels.gold : colorLabels.yellow;
3434
+ colorName = isOlive ? colorLabels.olive : colorName;
3435
+ } else if (hue >= 75 && hue < 155) {
3436
+ colorName = hsvl < 68 ? colorLabels.green : colorLabels.lime;
3437
+ } else if (hue >= 155 && hue < 175) {
3438
+ colorName = colorLabels.teal;
3439
+ } else if (hue >= 175 && hue < 195) {
3440
+ colorName = colorLabels.cyan;
3441
+ } else if (hue >= 195 && hue < 255) {
3442
+ colorName = colorLabels.blue;
3443
+ } else if (hue >= 255 && hue < 270) {
3444
+ colorName = colorLabels.violet;
3445
+ } else if (hue >= 270 && hue < 295) {
3446
+ colorName = colorLabels.magenta;
3447
+ } else if (hue >= 295 && hue < 345) {
3448
+ colorName = colorLabels.pink;
3449
+ }
3450
+
3451
+ let colorLabel = `${hexLabel} ${hex.split('').join(' ')}`;
3452
+
3453
+ if (format === 'hsl') {
3454
+ colorLabel = `HSL: ${hue}°, ${saturation}%, ${lightness}%`;
3455
+ setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3456
+ setAttribute(knob1, ariaValueText, `${hue}° & ${lightness}%`);
3457
+ setAttribute(knob1, ariaValueNow, `${hue}`);
3458
+ setAttribute(knob2, ariaValueText, `${saturation}%`);
3459
+ setAttribute(knob2, ariaValueNow, `${saturation}`);
3460
+ } else if (format === 'hwb') {
3461
+ const { hwb } = self;
3462
+ const whiteness = Math.round(hwb.w * 100);
3463
+ const blackness = Math.round(hwb.b * 100);
3464
+ colorLabel = `HWB: ${hue}°, ${whiteness}%, ${blackness}%`;
3465
+ setAttribute(knob1, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3466
+ setAttribute(knob1, ariaValueText, `${whiteness}% & ${blackness}%`);
3467
+ setAttribute(knob1, ariaValueNow, `${whiteness}`);
3468
+ setAttribute(knob2, ariaValueText, `${hue}%`);
3469
+ setAttribute(knob2, ariaValueNow, `${hue}`);
3470
+ } else {
3471
+ colorLabel = format === 'rgb' ? `RGB: ${r}, ${g}, ${b}` : colorLabel;
3472
+ setAttribute(knob2, ariaDescription, `${valueLabel}: ${colorLabel}. ${appearanceLabel}: ${colorName}.`);
3473
+ setAttribute(knob1, ariaValueText, `${lightness}% & ${saturation}%`);
3474
+ setAttribute(knob1, ariaValueNow, `${lightness}`);
3475
+ setAttribute(knob2, ariaValueText, `${hue}°`);
3476
+ setAttribute(knob2, ariaValueNow, `${hue}`);
3477
+ }
3478
+
3479
+ const alphaValue = Math.round(alpha * 100);
3480
+ setAttribute(knob3, ariaValueText, `${alphaValue}%`);
3481
+ setAttribute(knob3, ariaValueNow, `${alphaValue}`);
3482
+
3483
+ // update the input backgroundColor
3484
+ const newColor = color.toString();
3485
+ setElementStyle(self.input, { backgroundColor: newColor });
3486
+
3487
+ // toggle dark/light classes will also style the placeholder
3488
+ // dark sets color white, light sets color black
3489
+ // isDark ? '#000' : '#fff'
3490
+ if (!self.isDark) {
3491
+ if (hasClass(parent, 'txt-dark')) removeClass(parent, 'txt-dark');
3492
+ if (!hasClass(parent, 'txt-light')) addClass(parent, 'txt-light');
3493
+ } else {
3494
+ if (hasClass(parent, 'txt-light')) removeClass(parent, 'txt-light');
3495
+ if (!hasClass(parent, 'txt-dark')) addClass(parent, 'txt-dark');
3496
+ }
3497
+ }
3498
+
3499
+ /** Updates the control knobs actual positions. */
3500
+ updateControls() {
3501
+ const { controlKnobs, controlPositions } = this;
3502
+ const [control1, control2, control3] = controlKnobs;
3503
+ setElementStyle(control1, { transform: `translate3d(${controlPositions.c1x - 4}px,${controlPositions.c1y - 4}px,0)` });
3504
+ setElementStyle(control2, { transform: `translate3d(0,${controlPositions.c2y - 4}px,0)` });
3505
+ setElementStyle(control3, { transform: `translate3d(0,${controlPositions.c3y - 4}px,0)` });
3506
+ }
3507
+
3508
+ /**
3509
+ * Updates all color form inputs.
3510
+ * @param {boolean=} isPrevented when `true`, the component original event is prevented
3511
+ */
3512
+ updateInputs(isPrevented) {
3513
+ const self = this;
3514
+ const {
3515
+ value: oldColor, format, inputs, color, hsl,
3516
+ } = self;
3517
+ const [i1, i2, i3, i4] = inputs;
3518
+ const alpha = Math.round(color.a * 100);
3519
+ const hue = Math.round(hsl.h * 360);
3520
+ let newColor;
3521
+
3522
+ if (format === 'hex') {
3523
+ newColor = self.color.toHexString(true);
3524
+ i1.value = self.hex;
3525
+ } else if (format === 'hsl') {
3526
+ const lightness = Math.round(hsl.l * 100);
3527
+ const saturation = Math.round(hsl.s * 100);
3528
+ newColor = self.color.toHslString();
3529
+ i1.value = `${hue}`;
3530
+ i2.value = `${saturation}`;
3531
+ i3.value = `${lightness}`;
3532
+ i4.value = `${alpha}`;
3533
+ } else if (format === 'hwb') {
3534
+ const { w, b } = self.hwb;
3535
+ const whiteness = Math.round(w * 100);
3536
+ const blackness = Math.round(b * 100);
3537
+
3538
+ newColor = self.color.toHwbString();
3539
+ i1.value = `${hue}`;
3540
+ i2.value = `${whiteness}`;
3541
+ i3.value = `${blackness}`;
3542
+ i4.value = `${alpha}`;
3543
+ } else if (format === 'rgb') {
3544
+ const { r, g, b } = self.rgb;
3545
+
3546
+ newColor = self.color.toRgbString();
3547
+ i1.value = `${r}`;
3548
+ i2.value = `${g}`;
3549
+ i3.value = `${b}`;
3550
+ i4.value = `${alpha}`;
3551
+ }
3552
+
3553
+ // update the color value
3554
+ self.value = `${newColor}`;
3555
+
3556
+ // don't trigger the custom event unless it's really changed
3557
+ if (!isPrevented && newColor !== oldColor) {
3558
+ firePickerChange(self);
3559
+ }
3560
+ }
3561
+
3562
+ /**
3563
+ * The `Space` & `Enter` keys specific event listener.
3564
+ * Toggle visibility of the `ColorPicker` / the presets menu, showing one will hide the other.
3565
+ * @param {KeyboardEvent} e
3566
+ * @this {ColorPicker}
3567
+ */
3568
+ keyToggle(e) {
3569
+ const self = this;
3570
+ const { menuToggle } = self;
3571
+ const { activeElement } = getDocument(menuToggle);
3572
+ const { code } = e;
3573
+
3574
+ if ([keyEnter, keySpace].includes(code)) {
3575
+ if ((menuToggle && activeElement === menuToggle) || !activeElement) {
3576
+ e.preventDefault();
3577
+ if (!activeElement) {
3578
+ self.togglePicker(e);
3579
+ } else {
3580
+ self.toggleMenu();
3581
+ }
3582
+ }
3583
+ }
3584
+ }
3585
+
3586
+ /**
3587
+ * Toggle the `ColorPicker` dropdown visibility.
3588
+ * @param {Event} e
3589
+ * @this {ColorPicker}
3590
+ */
3591
+ togglePicker(e) {
3592
+ e.preventDefault();
3593
+ const self = this;
3594
+ const { colorPicker } = self;
3595
+
3596
+ if (self.isOpen && hasClass(colorPicker, 'show')) {
3597
+ self.hide(true);
3598
+ } else {
3599
+ showDropdown(self, colorPicker);
3600
+ }
3601
+ }
3602
+
3603
+ /** Shows the `ColorPicker` dropdown. */
3604
+ showPicker() {
3605
+ const self = this;
3606
+ const { colorPicker } = self;
3607
+
3608
+ if (!hasClass(colorPicker, 'show')) {
3609
+ showDropdown(self, colorPicker);
3610
+ }
3611
+ }
3612
+
3613
+ /** Toggles the visibility of the `ColorPicker` presets menu. */
3614
+ toggleMenu() {
3615
+ const self = this;
3616
+ const { colorMenu } = self;
3617
+
3618
+ if (self.isOpen && hasClass(colorMenu, 'show')) {
3619
+ self.hide(true);
3620
+ } else {
3621
+ showDropdown(self, colorMenu);
3622
+ }
3623
+ }
3624
+
3625
+ /** Shows the `ColorPicker` dropdown or the presets menu. */
3626
+ show() {
3627
+ const self = this;
3628
+ const { menuToggle } = self;
3629
+ if (!self.isOpen) {
3630
+ toggleEventsOnShown(self, true);
3631
+ self.updateDropdownPosition();
3632
+ self.isOpen = true;
3633
+ setAttribute(self.input, 'tabindex', '0');
3634
+ if (menuToggle) {
3635
+ setAttribute(menuToggle, 'tabindex', '0');
3636
+ }
3637
+ }
3638
+ }
3639
+
3640
+ /**
3641
+ * Hides the currently open `ColorPicker` dropdown.
3642
+ * @param {boolean=} focusPrevented
3643
+ */
3644
+ hide(focusPrevented) {
3645
+ const self = this;
3646
+ if (self.isOpen) {
3647
+ const {
3648
+ pickerToggle, menuToggle, colorPicker, colorMenu, parent, input,
3649
+ } = self;
3650
+ const openPicker = hasClass(colorPicker, 'show');
3651
+ const openDropdown = openPicker ? colorPicker : colorMenu;
3652
+ const relatedBtn = openPicker ? pickerToggle : menuToggle;
3653
+ const animationDuration = openDropdown && getElementTransitionDuration(openDropdown);
3654
+
3655
+ if (openDropdown) {
3656
+ removeClass(openDropdown, 'show');
3657
+ setAttribute(relatedBtn, ariaExpanded, 'false');
3658
+ setTimeout(() => {
3659
+ removePosition(openDropdown);
3660
+ if (!querySelector('.show', parent)) {
3661
+ removeClass(parent, 'open');
3662
+ toggleEventsOnShown(self);
3663
+ self.isOpen = false;
3664
+ }
3665
+ }, animationDuration);
3666
+ }
3667
+
3668
+ if (!self.isValid) {
3669
+ self.value = self.color.toString();
3670
+ }
3671
+ if (!focusPrevented) {
3672
+ focus(pickerToggle);
3673
+ }
3674
+ setAttribute(input, 'tabindex', '-1');
3675
+ if (menuToggle) {
3676
+ setAttribute(menuToggle, 'tabindex', '-1');
3677
+ }
3678
+ }
3679
+ }
3680
+
3681
+ /** Removes `ColorPicker` from target `<input>`. */
3682
+ dispose() {
3683
+ const self = this;
3684
+ const { input, parent } = self;
3685
+ self.hide(true);
3686
+ toggleEvents(self);
3687
+ [...parent.children].forEach((el) => {
3688
+ if (el !== input) el.remove();
3689
+ });
3690
+ setElementStyle(input, { backgroundColor: '' });
3691
+ ['txt-light', 'txt-dark'].forEach((c) => removeClass(parent, c));
3692
+ Data.remove(input, colorPickerString);
3693
+ }
3694
+ }
3695
+
3696
+ ObjectAssign(ColorPicker, {
3697
+ Color,
3698
+ Version,
3699
+ getInstance: getColorPickerInstance,
3700
+ init: initColorPicker,
3701
+ selector: colorPickerSelector,
3702
+ });
3703
+
3704
+ export default ColorPicker;