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