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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +40 -19
  3. package/dist/css/color-picker.css +481 -337
  4. package/dist/css/color-picker.min.css +2 -0
  5. package/dist/css/color-picker.rtl.css +506 -0
  6. package/dist/css/color-picker.rtl.min.css +2 -0
  7. package/dist/js/color-picker-element-esm.js +3810 -2
  8. package/dist/js/color-picker-element-esm.min.js +2 -0
  9. package/dist/js/color-picker-element.js +2009 -1242
  10. package/dist/js/color-picker-element.min.js +2 -2
  11. package/dist/js/color-picker-esm.js +3704 -0
  12. package/dist/js/color-picker-esm.min.js +2 -0
  13. package/dist/js/color-picker.js +1962 -1256
  14. package/dist/js/color-picker.min.js +2 -2
  15. package/package.json +18 -9
  16. package/src/js/color-palette.js +62 -0
  17. package/src/js/color-picker-element.js +55 -13
  18. package/src/js/color-picker.js +686 -595
  19. package/src/js/color.js +615 -349
  20. package/src/js/index.js +0 -9
  21. package/src/js/util/colorNames.js +2 -152
  22. package/src/js/util/colorPickerLabels.js +22 -0
  23. package/src/js/util/getColorControls.js +103 -0
  24. package/src/js/util/getColorForm.js +27 -19
  25. package/src/js/util/getColorMenu.js +95 -0
  26. package/src/js/util/isValidJSON.js +13 -0
  27. package/src/js/util/nonColors.js +5 -0
  28. package/src/js/util/templates.js +1 -0
  29. package/src/scss/color-picker.rtl.scss +23 -0
  30. package/src/scss/color-picker.scss +430 -0
  31. package/types/cp.d.ts +263 -160
  32. package/types/index.d.ts +9 -2
  33. package/types/source/source.ts +2 -1
  34. package/types/source/types.d.ts +28 -5
  35. package/dist/js/color-picker.esm.js +0 -2998
  36. package/dist/js/color-picker.esm.min.js +0 -2
  37. package/src/js/util/getColorControl.js +0 -49
  38. package/src/js/util/init.js +0 -14
@@ -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;