@thednp/color-picker 1.0.0 → 2.0.0-alpha1

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