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

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