@thednp/color-picker 0.0.1-alpha1 → 0.0.1

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