@thednp/color-picker 0.0.1-alpha1 → 0.0.1

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