@thednp/color-picker 0.0.1-alpha1
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.
- package/LICENSE +21 -0
- package/README.md +89 -0
- package/dist/css/color-picker.css +338 -0
- package/dist/js/color-picker-element-esm.js +2 -0
- package/dist/js/color-picker-element.js +3051 -0
- package/dist/js/color-picker-element.min.js +2 -0
- package/dist/js/color-picker.esm.js +2998 -0
- package/dist/js/color-picker.esm.min.js +2 -0
- package/dist/js/color-picker.js +3006 -0
- package/dist/js/color-picker.min.js +2 -0
- package/package.json +79 -0
- package/src/js/color-picker-element.js +61 -0
- package/src/js/color-picker.js +1333 -0
- package/src/js/color.js +860 -0
- package/src/js/index.js +12 -0
- package/src/js/util/colorNames.js +156 -0
- package/src/js/util/getColorControl.js +49 -0
- package/src/js/util/getColorForm.js +58 -0
- package/src/js/util/init.js +14 -0
- package/src/js/util/templates.js +9 -0
- package/src/js/util/vHidden.js +2 -0
- package/src/js/version.js +6 -0
- package/types/cp.d.ts +411 -0
- package/types/index.d.ts +41 -0
- package/types/source/source.ts +4 -0
- package/types/source/types.d.ts +67 -0
@@ -0,0 +1,3051 @@
|
|
1
|
+
/*!
|
2
|
+
* ColorPickerElement v0.0.1alpha1 (http://thednp.github.io/color-picker)
|
3
|
+
* Copyright 2022 © thednp
|
4
|
+
* Licensed under MIT (https://github.com/thednp/color-picker/blob/master/LICENSE)
|
5
|
+
*/
|
6
|
+
(function (global, factory) {
|
7
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
8
|
+
typeof define === 'function' && define.amd ? define(factory) :
|
9
|
+
(global = global || self, global.ColorPickerElement = factory());
|
10
|
+
}(this, (function () { 'use strict';
|
11
|
+
|
12
|
+
/**
|
13
|
+
* Returns the `document` or the `#document` element.
|
14
|
+
* @see https://github.com/floating-ui/floating-ui
|
15
|
+
* @param {(Node | HTMLElement | Element | globalThis)=} node
|
16
|
+
* @returns {Document}
|
17
|
+
*/
|
18
|
+
function getDocument(node) {
|
19
|
+
if (node instanceof HTMLElement) return node.ownerDocument;
|
20
|
+
if (node instanceof Window) return node.document;
|
21
|
+
return window.document;
|
22
|
+
}
|
23
|
+
|
24
|
+
/**
|
25
|
+
* A global array of possible `ParentNode`.
|
26
|
+
*/
|
27
|
+
const parentNodes = [Document, Element, HTMLElement];
|
28
|
+
|
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.
|
37
|
+
*
|
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
|
41
|
+
*/
|
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);
|
50
|
+
}
|
51
|
+
|
52
|
+
/**
|
53
|
+
* Shortcut for `Object.assign()` static method.
|
54
|
+
* @param {Record<string, any>} obj a target object
|
55
|
+
* @param {Record<string, any>} source a source object
|
56
|
+
*/
|
57
|
+
const ObjectAssign = (obj, source) => Object.assign(obj, source);
|
58
|
+
|
59
|
+
/**
|
60
|
+
* This is a shortie for `document.createElement` method
|
61
|
+
* which allows you to create a new `HTMLElement` for a given `tagName`
|
62
|
+
* or based on an object with specific non-readonly attributes:
|
63
|
+
* `id`, `className`, `textContent`, `style`, etc.
|
64
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement
|
65
|
+
*
|
66
|
+
* @param {Record<string, string> | string} param `tagName` or object
|
67
|
+
* @return {HTMLElement | Element} a new `HTMLElement` or `Element`
|
68
|
+
*/
|
69
|
+
function createElement(param) {
|
70
|
+
if (typeof param === 'string') {
|
71
|
+
return getDocument().createElement(param);
|
72
|
+
}
|
73
|
+
|
74
|
+
const { tagName } = param;
|
75
|
+
const attr = { ...param };
|
76
|
+
const newElement = createElement(tagName);
|
77
|
+
delete attr.tagName;
|
78
|
+
ObjectAssign(newElement, attr);
|
79
|
+
return newElement;
|
80
|
+
}
|
81
|
+
|
82
|
+
/**
|
83
|
+
* Returns the `document.head` or the `<head>` element.
|
84
|
+
*
|
85
|
+
* @param {(Node | HTMLElement | Element | globalThis)=} node
|
86
|
+
* @returns {HTMLElement | HTMLHeadElement}
|
87
|
+
*/
|
88
|
+
function getDocumentHead(node) {
|
89
|
+
return getDocument(node).head;
|
90
|
+
}
|
91
|
+
|
92
|
+
/**
|
93
|
+
* Shortcut for `window.getComputedStyle(element).propertyName`
|
94
|
+
* static method.
|
95
|
+
*
|
96
|
+
* * If `element` parameter is not an `HTMLElement`, `getComputedStyle`
|
97
|
+
* throws a `ReferenceError`.
|
98
|
+
*
|
99
|
+
* @param {HTMLElement | Element} element target
|
100
|
+
* @param {string} property the css property
|
101
|
+
* @return {string} the css property value
|
102
|
+
*/
|
103
|
+
function getElementStyle(element, property) {
|
104
|
+
const computedStyle = getComputedStyle(element);
|
105
|
+
|
106
|
+
// @ts-ignore -- must use camelcase strings,
|
107
|
+
// or non-camelcase strings with `getPropertyValue`
|
108
|
+
return property in computedStyle ? computedStyle[property] : '';
|
109
|
+
}
|
110
|
+
|
111
|
+
/**
|
112
|
+
* Shortcut for multiple uses of `HTMLElement.style.propertyName` method.
|
113
|
+
* @param {HTMLElement | Element} element target element
|
114
|
+
* @param {Partial<CSSStyleDeclaration>} styles attribute value
|
115
|
+
*/
|
116
|
+
// @ts-ignore
|
117
|
+
const setElementStyle = (element, styles) => { ObjectAssign(element.style, styles); };
|
118
|
+
|
119
|
+
/**
|
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[]}
|
123
|
+
*/
|
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
|
+
];
|
274
|
+
|
275
|
+
// <http://www.w3.org/TR/css3-values/#integers>
|
276
|
+
const CSS_INTEGER = '[-\\+]?\\d+%?';
|
277
|
+
|
278
|
+
// <http://www.w3.org/TR/css3-values/#number-value>
|
279
|
+
const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
|
280
|
+
|
281
|
+
// Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
|
282
|
+
const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
|
283
|
+
|
284
|
+
// Actual matching.
|
285
|
+
// Parentheses and commas are optional, but not required.
|
286
|
+
// 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*\\)?`;
|
289
|
+
|
290
|
+
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}`),
|
298
|
+
hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
299
|
+
hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
|
300
|
+
hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
301
|
+
hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
|
302
|
+
};
|
303
|
+
|
304
|
+
/**
|
305
|
+
* Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
|
306
|
+
* <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
|
307
|
+
* @param {string} n
|
308
|
+
* @returns {boolean}
|
309
|
+
*/
|
310
|
+
function isOnePointZero(n) {
|
311
|
+
return typeof n === 'string' && n.includes('.') && parseFloat(n) === 1;
|
312
|
+
}
|
313
|
+
|
314
|
+
/**
|
315
|
+
* Check to see if string passed in is a percentage
|
316
|
+
* @param {string} n
|
317
|
+
* @returns {boolean}
|
318
|
+
*/
|
319
|
+
function isPercentage(n) {
|
320
|
+
return typeof n === 'string' && n.includes('%');
|
321
|
+
}
|
322
|
+
|
323
|
+
/**
|
324
|
+
* Check to see if it looks like a CSS unit
|
325
|
+
* (see `matchers` above for definition).
|
326
|
+
* @param {string | number} color
|
327
|
+
* @returns {boolean}
|
328
|
+
*/
|
329
|
+
function isValidCSSUnit(color) {
|
330
|
+
return Boolean(matchers.CSS_UNIT.exec(String(color)));
|
331
|
+
}
|
332
|
+
|
333
|
+
/**
|
334
|
+
* Take input from [0, n] and return it as [0, 1]
|
335
|
+
* @param {*} n
|
336
|
+
* @param {number} max
|
337
|
+
* @returns {number}
|
338
|
+
*/
|
339
|
+
function bound01(n, max) {
|
340
|
+
let N = n;
|
341
|
+
if (isOnePointZero(n)) N = '100%';
|
342
|
+
|
343
|
+
N = max === 360 ? N : Math.min(max, Math.max(0, parseFloat(N)));
|
344
|
+
|
345
|
+
// Automatically convert percentage into number
|
346
|
+
if (isPercentage(N)) {
|
347
|
+
N = parseInt(String(N * max), 10) / 100;
|
348
|
+
}
|
349
|
+
// Handle floating point rounding errors
|
350
|
+
if (Math.abs(N - max) < 0.000001) {
|
351
|
+
return 1;
|
352
|
+
}
|
353
|
+
// Convert into [0, 1] range if it isn't already
|
354
|
+
if (max === 360) {
|
355
|
+
// If n is a hue given in degrees,
|
356
|
+
// wrap around out-of-range values into [0, 360] range
|
357
|
+
// then convert into [0, 1].
|
358
|
+
N = (N < 0 ? (N % max) + max : N % max) / parseFloat(String(max));
|
359
|
+
} else {
|
360
|
+
// If n not a hue given in degrees
|
361
|
+
// Convert into [0, 1] range if it isn't already.
|
362
|
+
N = (N % max) / parseFloat(String(max));
|
363
|
+
}
|
364
|
+
return N;
|
365
|
+
}
|
366
|
+
|
367
|
+
/**
|
368
|
+
* Return a valid alpha value [0,1] with all invalid values being set to 1.
|
369
|
+
* @param {string | number} a
|
370
|
+
* @returns {number}
|
371
|
+
*/
|
372
|
+
function boundAlpha(a) {
|
373
|
+
// @ts-ignore
|
374
|
+
let na = parseFloat(a);
|
375
|
+
|
376
|
+
if (Number.isNaN(na) || na < 0 || na > 1) {
|
377
|
+
na = 1;
|
378
|
+
}
|
379
|
+
|
380
|
+
return na;
|
381
|
+
}
|
382
|
+
|
383
|
+
/**
|
384
|
+
* Force a number between 0 and 1
|
385
|
+
* @param {number} val
|
386
|
+
* @returns {number}
|
387
|
+
*/
|
388
|
+
function clamp01(val) {
|
389
|
+
return Math.min(1, Math.max(0, val));
|
390
|
+
}
|
391
|
+
|
392
|
+
/**
|
393
|
+
* Returns the hexadecimal value of a web safe colour.
|
394
|
+
* @param {string} name
|
395
|
+
* @returns {string}
|
396
|
+
*/
|
397
|
+
function getHexFromColorName(name) {
|
398
|
+
const documentHead = getDocumentHead();
|
399
|
+
setElementStyle(documentHead, { color: name });
|
400
|
+
const colorName = getElementStyle(documentHead, 'color');
|
401
|
+
setElementStyle(documentHead, { color: '' });
|
402
|
+
return colorName;
|
403
|
+
}
|
404
|
+
|
405
|
+
/**
|
406
|
+
* Replace a decimal with it's percentage value
|
407
|
+
* @param {number | string} n
|
408
|
+
* @return {string | number}
|
409
|
+
*/
|
410
|
+
function convertToPercentage(n) {
|
411
|
+
if (n <= 1) {
|
412
|
+
return `${Number(n) * 100}%`;
|
413
|
+
}
|
414
|
+
return n;
|
415
|
+
}
|
416
|
+
|
417
|
+
/**
|
418
|
+
* Force a hex value to have 2 characters
|
419
|
+
* @param {string} c
|
420
|
+
* @returns {string}
|
421
|
+
*/
|
422
|
+
function pad2(c) {
|
423
|
+
return c.length === 1 ? `0${c}` : String(c);
|
424
|
+
}
|
425
|
+
|
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
|
+
/**
|
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}
|
437
|
+
*/
|
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
|
+
};
|
444
|
+
}
|
445
|
+
|
446
|
+
/**
|
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}
|
454
|
+
*/
|
455
|
+
function rgbToHsl(R, G, B) {
|
456
|
+
const r = bound01(R, 255);
|
457
|
+
const g = bound01(G, 255);
|
458
|
+
const b = bound01(B, 255);
|
459
|
+
const max = Math.max(r, g, b);
|
460
|
+
const min = Math.min(r, g, b);
|
461
|
+
let h = 0;
|
462
|
+
let s = 0;
|
463
|
+
const l = (max + min) / 2;
|
464
|
+
if (max === min) {
|
465
|
+
s = 0;
|
466
|
+
h = 0; // achromatic
|
467
|
+
} else {
|
468
|
+
const d = max - min;
|
469
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
470
|
+
switch (max) {
|
471
|
+
case r:
|
472
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
473
|
+
break;
|
474
|
+
case g:
|
475
|
+
h = (b - r) / d + 2;
|
476
|
+
break;
|
477
|
+
case b:
|
478
|
+
h = (r - g) / d + 4;
|
479
|
+
break;
|
480
|
+
}
|
481
|
+
h /= 6;
|
482
|
+
}
|
483
|
+
return { h, s, l };
|
484
|
+
}
|
485
|
+
|
486
|
+
/**
|
487
|
+
* Returns a normalized RGB component value.
|
488
|
+
* @param {number} P
|
489
|
+
* @param {number} Q
|
490
|
+
* @param {number} T
|
491
|
+
* @returns {number}
|
492
|
+
*/
|
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
|
+
}
|
512
|
+
return p;
|
513
|
+
}
|
514
|
+
|
515
|
+
/**
|
516
|
+
* Converts an HSL colour value to RGB.
|
517
|
+
*
|
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) {
|
526
|
+
let r = 0;
|
527
|
+
let g = 0;
|
528
|
+
let b = 0;
|
529
|
+
const h = bound01(H, 360);
|
530
|
+
const s = bound01(S, 100);
|
531
|
+
const l = bound01(L, 100);
|
532
|
+
|
533
|
+
if (s === 0) {
|
534
|
+
// achromatic
|
535
|
+
g = l;
|
536
|
+
b = l;
|
537
|
+
r = l;
|
538
|
+
} else {
|
539
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
540
|
+
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);
|
544
|
+
}
|
545
|
+
return { r: r * 255, g: g * 255, b: b * 255 };
|
546
|
+
}
|
547
|
+
|
548
|
+
/**
|
549
|
+
* Converts an RGB colour value to HSV.
|
550
|
+
*
|
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}
|
557
|
+
*/
|
558
|
+
function rgbToHsv(R, G, B) {
|
559
|
+
const r = bound01(R, 255);
|
560
|
+
const g = bound01(G, 255);
|
561
|
+
const b = bound01(B, 255);
|
562
|
+
const max = Math.max(r, g, b);
|
563
|
+
const min = Math.min(r, g, b);
|
564
|
+
let h = 0;
|
565
|
+
const v = max;
|
566
|
+
const d = max - min;
|
567
|
+
const s = max === 0 ? 0 : d / max;
|
568
|
+
if (max === min) {
|
569
|
+
h = 0; // achromatic
|
570
|
+
} else {
|
571
|
+
switch (max) {
|
572
|
+
case r:
|
573
|
+
h = (g - b) / d + (g < b ? 6 : 0);
|
574
|
+
break;
|
575
|
+
case g:
|
576
|
+
h = (b - r) / d + 2;
|
577
|
+
break;
|
578
|
+
case b:
|
579
|
+
h = (r - g) / d + 4;
|
580
|
+
break;
|
581
|
+
}
|
582
|
+
h /= 6;
|
583
|
+
}
|
584
|
+
return { h, s, v };
|
585
|
+
}
|
586
|
+
|
587
|
+
/**
|
588
|
+
* Converts an HSV color value to RGB.
|
589
|
+
*
|
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}
|
596
|
+
*/
|
597
|
+
function hsvToRgb(H, S, V) {
|
598
|
+
const h = bound01(H, 360) * 6;
|
599
|
+
const s = bound01(S, 100);
|
600
|
+
const v = bound01(V, 100);
|
601
|
+
const i = Math.floor(h);
|
602
|
+
const f = h - i;
|
603
|
+
const p = v * (1 - s);
|
604
|
+
const q = v * (1 - f * s);
|
605
|
+
const t = v * (1 - (1 - f) * s);
|
606
|
+
const mod = i % 6;
|
607
|
+
const r = [v, q, p, p, t, v][mod];
|
608
|
+
const g = [t, v, v, q, p, p][mod];
|
609
|
+
const b = [p, p, t, v, v, q][mod];
|
610
|
+
return { r: r * 255, g: g * 255, b: b * 255 };
|
611
|
+
}
|
612
|
+
|
613
|
+
/**
|
614
|
+
* Converts an RGB color to hex
|
615
|
+
*
|
616
|
+
* Assumes r, g, and b are contained in the set [0, 255]
|
617
|
+
* Returns a 3 or 6 character hex
|
618
|
+
* @param {number} r
|
619
|
+
* @param {number} g
|
620
|
+
* @param {number} b
|
621
|
+
* @returns {string}
|
622
|
+
*/
|
623
|
+
function rgbToHex(r, g, b) {
|
624
|
+
const hex = [
|
625
|
+
pad2(Math.round(r).toString(16)),
|
626
|
+
pad2(Math.round(g).toString(16)),
|
627
|
+
pad2(Math.round(b).toString(16)),
|
628
|
+
];
|
629
|
+
|
630
|
+
return hex.join('');
|
631
|
+
}
|
632
|
+
|
633
|
+
/**
|
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
|
+
}
|
641
|
+
|
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);
|
649
|
+
}
|
650
|
+
|
651
|
+
/**
|
652
|
+
* Returns an `{r,g,b}` color object corresponding to a given number.
|
653
|
+
* @param {number} color
|
654
|
+
* @returns {CP.RGB}
|
655
|
+
*/
|
656
|
+
function numberInputToObject(color) {
|
657
|
+
/* eslint-disable no-bitwise */
|
658
|
+
return {
|
659
|
+
r: color >> 16,
|
660
|
+
g: (color & 0xff00) >> 8,
|
661
|
+
b: color & 0xff,
|
662
|
+
};
|
663
|
+
/* eslint-enable no-bitwise */
|
664
|
+
}
|
665
|
+
|
666
|
+
/**
|
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}
|
671
|
+
*/
|
672
|
+
function stringInputToObject(input) {
|
673
|
+
let color = input.trim().toLowerCase();
|
674
|
+
if (color.length === 0) {
|
675
|
+
return {
|
676
|
+
r: 0, g: 0, b: 0, a: 0,
|
677
|
+
};
|
678
|
+
}
|
679
|
+
let named = false;
|
680
|
+
if (colorNames.includes(color)) {
|
681
|
+
color = getHexFromColorName(color);
|
682
|
+
named = true;
|
683
|
+
} else if (color === 'transparent') {
|
684
|
+
return {
|
685
|
+
r: 0, g: 0, b: 0, a: 0, format: 'name',
|
686
|
+
};
|
687
|
+
}
|
688
|
+
|
689
|
+
// Try to match string input using regular expressions.
|
690
|
+
// Keep most of the number bounding out of this function,
|
691
|
+
// don't worry about [0,1] or [0,100] or [0,360]
|
692
|
+
// Just return an object and let the conversion functions handle that.
|
693
|
+
// 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) {
|
700
|
+
return {
|
701
|
+
r: match[1], g: match[2], b: match[3], a: match[4],
|
702
|
+
};
|
703
|
+
}
|
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) {
|
710
|
+
return {
|
711
|
+
h: match[1], s: match[2], l: match[3], a: match[4],
|
712
|
+
};
|
713
|
+
}
|
714
|
+
match = matchers.hsv.exec(color);
|
715
|
+
if (match) {
|
716
|
+
return { h: match[1], s: match[2], v: match[3] };
|
717
|
+
}
|
718
|
+
match = matchers.hsva.exec(color);
|
719
|
+
if (match) {
|
720
|
+
return {
|
721
|
+
h: match[1], s: match[2], v: match[3], a: match[4],
|
722
|
+
};
|
723
|
+
}
|
724
|
+
match = matchers.hex8.exec(color);
|
725
|
+
if (match) {
|
726
|
+
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',
|
732
|
+
};
|
733
|
+
}
|
734
|
+
match = matchers.hex6.exec(color);
|
735
|
+
if (match) {
|
736
|
+
return {
|
737
|
+
r: parseIntFromHex(match[1]),
|
738
|
+
g: parseIntFromHex(match[2]),
|
739
|
+
b: parseIntFromHex(match[3]),
|
740
|
+
format: named ? 'name' : 'hex',
|
741
|
+
};
|
742
|
+
}
|
743
|
+
match = matchers.hex4.exec(color);
|
744
|
+
if (match) {
|
745
|
+
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',
|
751
|
+
};
|
752
|
+
}
|
753
|
+
match = matchers.hex3.exec(color);
|
754
|
+
if (match) {
|
755
|
+
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',
|
760
|
+
};
|
761
|
+
}
|
762
|
+
return false;
|
763
|
+
}
|
764
|
+
|
765
|
+
/**
|
766
|
+
* Given a string or object, convert that input to RGB
|
767
|
+
*
|
768
|
+
* Possible string inputs:
|
769
|
+
* ```
|
770
|
+
* "red"
|
771
|
+
* "#f00" or "f00"
|
772
|
+
* "#ff0000" or "ff0000"
|
773
|
+
* "#ff000000" or "ff000000"
|
774
|
+
* "rgb 255 0 0" or "rgb (255, 0, 0)"
|
775
|
+
* "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"
|
778
|
+
* "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
|
779
|
+
* "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
|
780
|
+
* "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
|
781
|
+
* ```
|
782
|
+
* @param {string | Record<string, any>} input
|
783
|
+
* @returns {CP.ColorObject}
|
784
|
+
*/
|
785
|
+
function inputToRGB(input) {
|
786
|
+
/** @type {CP.RGB} */
|
787
|
+
let rgb = { r: 0, g: 0, b: 0 };
|
788
|
+
let color = input;
|
789
|
+
let a;
|
790
|
+
let s = null;
|
791
|
+
let v = null;
|
792
|
+
let l = null;
|
793
|
+
let ok = false;
|
794
|
+
let format = 'hex';
|
795
|
+
|
796
|
+
if (typeof input === 'string') {
|
797
|
+
// @ts-ignore -- this now is converted to object
|
798
|
+
color = stringInputToObject(input);
|
799
|
+
if (color) ok = true;
|
800
|
+
}
|
801
|
+
if (typeof color === 'object') {
|
802
|
+
if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
|
803
|
+
rgb = rgbToRgb(color.r, color.g, color.b);
|
804
|
+
ok = true;
|
805
|
+
format = `${color.r}`.slice(-1) === '%' ? 'prgb' : 'rgb';
|
806
|
+
} 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);
|
810
|
+
ok = true;
|
811
|
+
format = 'hsv';
|
812
|
+
} 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);
|
816
|
+
ok = true;
|
817
|
+
format = 'hsl';
|
818
|
+
}
|
819
|
+
if ('a' in color) a = color.a;
|
820
|
+
}
|
821
|
+
|
822
|
+
return {
|
823
|
+
ok, // @ts-ignore
|
824
|
+
format: color.format || format,
|
825
|
+
r: Math.min(255, Math.max(rgb.r, 0)),
|
826
|
+
g: Math.min(255, Math.max(rgb.g, 0)),
|
827
|
+
b: Math.min(255, Math.max(rgb.b, 0)),
|
828
|
+
a: boundAlpha(a),
|
829
|
+
};
|
830
|
+
}
|
831
|
+
|
832
|
+
/** @type {CP.ColorOptions} */
|
833
|
+
const colorPickerDefaults = {
|
834
|
+
format: 'hex',
|
835
|
+
};
|
836
|
+
|
837
|
+
/**
|
838
|
+
* Returns a new `Color` instance.
|
839
|
+
* @see https://github.com/bgrins/TinyColor
|
840
|
+
* @class
|
841
|
+
*/
|
842
|
+
class Color {
|
843
|
+
/**
|
844
|
+
* @constructor
|
845
|
+
* @param {CP.ColorInput} input
|
846
|
+
* @param {CP.ColorOptions=} config
|
847
|
+
*/
|
848
|
+
constructor(input, config) {
|
849
|
+
let color = input;
|
850
|
+
const opts = typeof config === 'object'
|
851
|
+
? ObjectAssign(colorPickerDefaults, config)
|
852
|
+
: ObjectAssign({}, colorPickerDefaults);
|
853
|
+
|
854
|
+
// If input is already a `Color`, return itself
|
855
|
+
if (color instanceof Color) {
|
856
|
+
color = inputToRGB(color);
|
857
|
+
}
|
858
|
+
if (typeof color === 'number') {
|
859
|
+
color = numberInputToObject(color);
|
860
|
+
}
|
861
|
+
const {
|
862
|
+
r, g, b, a, ok, format,
|
863
|
+
} = inputToRGB(color);
|
864
|
+
|
865
|
+
/** @type {CP.ColorInput} */
|
866
|
+
this.originalInput = color;
|
867
|
+
/** @type {number} */
|
868
|
+
this.r = r;
|
869
|
+
/** @type {number} */
|
870
|
+
this.g = g;
|
871
|
+
/** @type {number} */
|
872
|
+
this.b = b;
|
873
|
+
/** @type {number} */
|
874
|
+
this.a = a;
|
875
|
+
/** @type {boolean} */
|
876
|
+
this.ok = ok;
|
877
|
+
/** @type {number} */
|
878
|
+
this.roundA = Math.round(100 * this.a) / 100;
|
879
|
+
/** @type {CP.ColorFormats} */
|
880
|
+
this.format = opts.format || format;
|
881
|
+
|
882
|
+
// Don't let the range of [0,255] come back in [0,1].
|
883
|
+
// Potentially lose a little bit of precision here, but will fix issues where
|
884
|
+
// .5 gets interpreted as half of the total, instead of half of 1
|
885
|
+
// If it was supposed to be 128, this was already taken care of by `inputToRgb`
|
886
|
+
if (this.r < 1) {
|
887
|
+
this.r = Math.round(this.r);
|
888
|
+
}
|
889
|
+
if (this.g < 1) {
|
890
|
+
this.g = Math.round(this.g);
|
891
|
+
}
|
892
|
+
if (this.b < 1) {
|
893
|
+
this.b = Math.round(this.b);
|
894
|
+
}
|
895
|
+
}
|
896
|
+
|
897
|
+
/**
|
898
|
+
* Checks if the current input value is a valid colour.
|
899
|
+
* @returns {boolean} the query result
|
900
|
+
*/
|
901
|
+
get isValid() {
|
902
|
+
return this.ok;
|
903
|
+
}
|
904
|
+
|
905
|
+
/**
|
906
|
+
* Checks if the current colour requires a light text colour.
|
907
|
+
* @returns {boolean} the query result
|
908
|
+
*/
|
909
|
+
get isDark() {
|
910
|
+
return this.brightness < 128;
|
911
|
+
}
|
912
|
+
|
913
|
+
/**
|
914
|
+
* Returns the perceived luminance of a color.
|
915
|
+
* @see http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
916
|
+
* @returns {number} a number in [0-1] range
|
917
|
+
*/
|
918
|
+
get luminance() {
|
919
|
+
const { r, g, b } = this;
|
920
|
+
let R = 0;
|
921
|
+
let G = 0;
|
922
|
+
let B = 0;
|
923
|
+
const RsRGB = r / 255;
|
924
|
+
const GsRGB = g / 255;
|
925
|
+
const BsRGB = b / 255;
|
926
|
+
|
927
|
+
if (RsRGB <= 0.03928) {
|
928
|
+
R = RsRGB / 12.92;
|
929
|
+
} else {
|
930
|
+
R = ((RsRGB + 0.055) / 1.055) ** 2.4;
|
931
|
+
}
|
932
|
+
if (GsRGB <= 0.03928) {
|
933
|
+
G = GsRGB / 12.92;
|
934
|
+
} else {
|
935
|
+
G = ((GsRGB + 0.055) / 1.055) ** 2.4;
|
936
|
+
}
|
937
|
+
if (BsRGB <= 0.03928) {
|
938
|
+
B = BsRGB / 12.92;
|
939
|
+
} else {
|
940
|
+
B = ((BsRGB + 0.055) / 1.055) ** 2.4;
|
941
|
+
}
|
942
|
+
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
943
|
+
}
|
944
|
+
|
945
|
+
/**
|
946
|
+
* Returns the perceived brightness of the color.
|
947
|
+
* @returns {number} a number in [0-255] range
|
948
|
+
*/
|
949
|
+
get brightness() {
|
950
|
+
const { r, g, b } = this;
|
951
|
+
return (r * 299 + g * 587 + b * 114) / 1000;
|
952
|
+
}
|
953
|
+
|
954
|
+
/**
|
955
|
+
* Returns the color as a RGBA object.
|
956
|
+
* @returns {CP.RGBA}
|
957
|
+
*/
|
958
|
+
toRgb() {
|
959
|
+
return {
|
960
|
+
r: Math.round(this.r),
|
961
|
+
g: Math.round(this.g),
|
962
|
+
b: Math.round(this.b),
|
963
|
+
a: this.a,
|
964
|
+
};
|
965
|
+
}
|
966
|
+
|
967
|
+
/**
|
968
|
+
* Returns the RGBA values concatenated into a string.
|
969
|
+
* @returns {string} the CSS valid color in RGB/RGBA format
|
970
|
+
*/
|
971
|
+
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})`;
|
978
|
+
}
|
979
|
+
|
980
|
+
/**
|
981
|
+
* Returns the HEX value of the color.
|
982
|
+
* @returns {string} the hexadecimal color format
|
983
|
+
*/
|
984
|
+
toHex() {
|
985
|
+
return rgbToHex(this.r, this.g, this.b);
|
986
|
+
}
|
987
|
+
|
988
|
+
/**
|
989
|
+
* Returns the HEX value of the color.
|
990
|
+
* @returns {string} the CSS valid color in hexadecimal format
|
991
|
+
*/
|
992
|
+
toHexString() {
|
993
|
+
return `#${this.toHex()}`;
|
994
|
+
}
|
995
|
+
|
996
|
+
/**
|
997
|
+
* Returns the color as a HSVA object.
|
998
|
+
* @returns {CP.HSVA} the `{h,s,v,a}` object
|
999
|
+
*/
|
1000
|
+
toHsv() {
|
1001
|
+
const { h, s, v } = rgbToHsv(this.r, this.g, this.b);
|
1002
|
+
return {
|
1003
|
+
h: h * 360, s, v, a: this.a,
|
1004
|
+
};
|
1005
|
+
}
|
1006
|
+
|
1007
|
+
/**
|
1008
|
+
* Returns the color as a HSLA object.
|
1009
|
+
* @returns {CP.HSLA}
|
1010
|
+
*/
|
1011
|
+
toHsl() {
|
1012
|
+
const { h, s, l } = rgbToHsl(this.r, this.g, this.b);
|
1013
|
+
return {
|
1014
|
+
h: h * 360, s, l, a: this.a,
|
1015
|
+
};
|
1016
|
+
}
|
1017
|
+
|
1018
|
+
/**
|
1019
|
+
* Returns the HSLA values concatenated into a string.
|
1020
|
+
* @returns {string} the CSS valid color in HSL/HSLA format
|
1021
|
+
*/
|
1022
|
+
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})`;
|
1030
|
+
}
|
1031
|
+
|
1032
|
+
/**
|
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
|
1036
|
+
*/
|
1037
|
+
setAlpha(alpha) {
|
1038
|
+
this.a = boundAlpha(alpha);
|
1039
|
+
this.roundA = Math.round(100 * this.a) / 100;
|
1040
|
+
return this;
|
1041
|
+
}
|
1042
|
+
|
1043
|
+
/**
|
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
|
1047
|
+
*/
|
1048
|
+
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);
|
1054
|
+
}
|
1055
|
+
|
1056
|
+
/**
|
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
|
1060
|
+
*/
|
1061
|
+
desaturate(amount) {
|
1062
|
+
return amount ? this.saturate(-amount) : this;
|
1063
|
+
}
|
1064
|
+
|
1065
|
+
/**
|
1066
|
+
* Completely desaturates a color into greyscale.
|
1067
|
+
* Same as calling `desaturate(100)`
|
1068
|
+
* @returns {Color} a new `Color` instance
|
1069
|
+
*/
|
1070
|
+
greyscale() {
|
1071
|
+
return this.desaturate(100);
|
1072
|
+
}
|
1073
|
+
|
1074
|
+
/** Returns a clone of the current `Color` instance. */
|
1075
|
+
clone() {
|
1076
|
+
return new Color(this);
|
1077
|
+
}
|
1078
|
+
|
1079
|
+
/**
|
1080
|
+
* Returns the color value in CSS valid string format.
|
1081
|
+
* @returns {string}
|
1082
|
+
*/
|
1083
|
+
toString() {
|
1084
|
+
const { format } = this;
|
1085
|
+
|
1086
|
+
if (format === 'rgb') {
|
1087
|
+
return this.toRgbString();
|
1088
|
+
}
|
1089
|
+
if (format === 'hsl') {
|
1090
|
+
return this.toHslString();
|
1091
|
+
}
|
1092
|
+
return this.toHexString();
|
1093
|
+
}
|
1094
|
+
}
|
1095
|
+
|
1096
|
+
ObjectAssign(Color, {
|
1097
|
+
colorNames,
|
1098
|
+
CSS_INTEGER,
|
1099
|
+
CSS_NUMBER,
|
1100
|
+
CSS_UNIT,
|
1101
|
+
PERMISSIVE_MATCH3,
|
1102
|
+
PERMISSIVE_MATCH4,
|
1103
|
+
matchers,
|
1104
|
+
isOnePointZero,
|
1105
|
+
isPercentage,
|
1106
|
+
isValidCSSUnit,
|
1107
|
+
bound01,
|
1108
|
+
boundAlpha,
|
1109
|
+
clamp01,
|
1110
|
+
getHexFromColorName,
|
1111
|
+
convertToPercentage,
|
1112
|
+
convertHexToDecimal,
|
1113
|
+
pad2,
|
1114
|
+
rgbToRgb,
|
1115
|
+
rgbToHsl,
|
1116
|
+
rgbToHex,
|
1117
|
+
rgbToHsv,
|
1118
|
+
hslToRgb,
|
1119
|
+
hsvToRgb,
|
1120
|
+
hue2rgb,
|
1121
|
+
parseIntFromHex,
|
1122
|
+
numberInputToObject,
|
1123
|
+
stringInputToObject,
|
1124
|
+
inputToRGB,
|
1125
|
+
});
|
1126
|
+
|
1127
|
+
/** @type {Record<string, any>} */
|
1128
|
+
const EventRegistry = {};
|
1129
|
+
|
1130
|
+
/**
|
1131
|
+
* The global event listener.
|
1132
|
+
*
|
1133
|
+
* @this {Element | HTMLElement | Window | Document}
|
1134
|
+
* @param {Event} e
|
1135
|
+
* @returns {void}
|
1136
|
+
*/
|
1137
|
+
function globalListener(e) {
|
1138
|
+
const that = this;
|
1139
|
+
const { type } = e;
|
1140
|
+
const oneEvMap = EventRegistry[type] ? [...EventRegistry[type]] : [];
|
1141
|
+
|
1142
|
+
oneEvMap.forEach((elementsMap) => {
|
1143
|
+
const [element, listenersMap] = elementsMap;
|
1144
|
+
[...listenersMap].forEach((listenerMap) => {
|
1145
|
+
if (element === that) {
|
1146
|
+
const [listener, options] = listenerMap;
|
1147
|
+
listener.apply(element, [e]);
|
1148
|
+
|
1149
|
+
if (options && options.once) {
|
1150
|
+
removeListener(element, type, listener, options);
|
1151
|
+
}
|
1152
|
+
}
|
1153
|
+
});
|
1154
|
+
});
|
1155
|
+
}
|
1156
|
+
|
1157
|
+
/**
|
1158
|
+
* Register a new listener with its options and attach the `globalListener`
|
1159
|
+
* to the target if this is the first listener.
|
1160
|
+
*
|
1161
|
+
* @param {Element | HTMLElement | Window | Document} element
|
1162
|
+
* @param {string} eventType
|
1163
|
+
* @param {EventListenerObject['handleEvent']} listener
|
1164
|
+
* @param {AddEventListenerOptions=} options
|
1165
|
+
*/
|
1166
|
+
const addListener = (element, eventType, listener, options) => {
|
1167
|
+
// get element listeners first
|
1168
|
+
if (!EventRegistry[eventType]) {
|
1169
|
+
EventRegistry[eventType] = new Map();
|
1170
|
+
}
|
1171
|
+
const oneEventMap = EventRegistry[eventType];
|
1172
|
+
|
1173
|
+
if (!oneEventMap.has(element)) {
|
1174
|
+
oneEventMap.set(element, new Map());
|
1175
|
+
}
|
1176
|
+
const oneElementMap = oneEventMap.get(element);
|
1177
|
+
|
1178
|
+
// get listeners size
|
1179
|
+
const { size } = oneElementMap;
|
1180
|
+
|
1181
|
+
// register listener with its options
|
1182
|
+
if (oneElementMap) {
|
1183
|
+
oneElementMap.set(listener, options);
|
1184
|
+
}
|
1185
|
+
|
1186
|
+
// add listener last
|
1187
|
+
if (!size) {
|
1188
|
+
element.addEventListener(eventType, globalListener, options);
|
1189
|
+
}
|
1190
|
+
};
|
1191
|
+
|
1192
|
+
/**
|
1193
|
+
* Remove a listener from registry and detach the `globalListener`
|
1194
|
+
* if no listeners are found in the registry.
|
1195
|
+
*
|
1196
|
+
* @param {Element | HTMLElement | Window | Document} element
|
1197
|
+
* @param {string} eventType
|
1198
|
+
* @param {EventListenerObject['handleEvent']} listener
|
1199
|
+
* @param {AddEventListenerOptions=} options
|
1200
|
+
*/
|
1201
|
+
const removeListener = (element, eventType, listener, options) => {
|
1202
|
+
// get listener first
|
1203
|
+
const oneEventMap = EventRegistry[eventType];
|
1204
|
+
const oneElementMap = oneEventMap && oneEventMap.get(element);
|
1205
|
+
const savedOptions = oneElementMap && oneElementMap.get(listener);
|
1206
|
+
|
1207
|
+
// also recover initial options
|
1208
|
+
const { options: eventOptions } = savedOptions !== undefined
|
1209
|
+
? savedOptions
|
1210
|
+
: { options };
|
1211
|
+
|
1212
|
+
// unsubscribe second, remove from registry
|
1213
|
+
if (oneElementMap && oneElementMap.has(listener)) oneElementMap.delete(listener);
|
1214
|
+
if (oneEventMap && (!oneElementMap || !oneElementMap.size)) oneEventMap.delete(element);
|
1215
|
+
if (!oneEventMap || !oneEventMap.size) delete EventRegistry[eventType];
|
1216
|
+
|
1217
|
+
// remove listener last
|
1218
|
+
if (!oneElementMap || !oneElementMap.size) {
|
1219
|
+
element.removeEventListener(eventType, globalListener, eventOptions);
|
1220
|
+
}
|
1221
|
+
};
|
1222
|
+
|
1223
|
+
/**
|
1224
|
+
* A global namespace for aria-selected.
|
1225
|
+
* @type {string}
|
1226
|
+
*/
|
1227
|
+
const ariaSelected = 'aria-selected';
|
1228
|
+
|
1229
|
+
/**
|
1230
|
+
* A global namespace for aria-expanded.
|
1231
|
+
* @type {string}
|
1232
|
+
*/
|
1233
|
+
const ariaExpanded = 'aria-expanded';
|
1234
|
+
|
1235
|
+
/**
|
1236
|
+
* A global namespace for aria-hidden.
|
1237
|
+
* @type {string}
|
1238
|
+
*/
|
1239
|
+
const ariaHidden = 'aria-hidden';
|
1240
|
+
|
1241
|
+
/**
|
1242
|
+
* A global namespace for aria-labelledby.
|
1243
|
+
* @type {string}
|
1244
|
+
*/
|
1245
|
+
const ariaLabelledBy = 'aria-labelledby';
|
1246
|
+
|
1247
|
+
/**
|
1248
|
+
* A global namespace for `ArrowDown` key.
|
1249
|
+
* @type {string} e.which = 40 equivalent
|
1250
|
+
*/
|
1251
|
+
const keyArrowDown = 'ArrowDown';
|
1252
|
+
|
1253
|
+
/**
|
1254
|
+
* A global namespace for `ArrowUp` key.
|
1255
|
+
* @type {string} e.which = 38 equivalent
|
1256
|
+
*/
|
1257
|
+
const keyArrowUp = 'ArrowUp';
|
1258
|
+
|
1259
|
+
/**
|
1260
|
+
* A global namespace for `ArrowLeft` key.
|
1261
|
+
* @type {string} e.which = 37 equivalent
|
1262
|
+
*/
|
1263
|
+
const keyArrowLeft = 'ArrowLeft';
|
1264
|
+
|
1265
|
+
/**
|
1266
|
+
* A global namespace for `ArrowRight` key.
|
1267
|
+
* @type {string} e.which = 39 equivalent
|
1268
|
+
*/
|
1269
|
+
const keyArrowRight = 'ArrowRight';
|
1270
|
+
|
1271
|
+
/**
|
1272
|
+
* A global namespace for `Enter` key.
|
1273
|
+
* @type {string} e.which = 13 equivalent
|
1274
|
+
*/
|
1275
|
+
const keyEnter = 'Enter';
|
1276
|
+
|
1277
|
+
/**
|
1278
|
+
* A global namespace for `Space` key.
|
1279
|
+
* @type {string} e.which = 32 equivalent
|
1280
|
+
*/
|
1281
|
+
const keySpace = 'Space';
|
1282
|
+
|
1283
|
+
/**
|
1284
|
+
* A global namespace for `Escape` key.
|
1285
|
+
* @type {string} e.which = 27 equivalent
|
1286
|
+
*/
|
1287
|
+
const keyEscape = 'Escape';
|
1288
|
+
|
1289
|
+
// @ts-ignore
|
1290
|
+
const { userAgentData: uaDATA } = navigator;
|
1291
|
+
|
1292
|
+
/**
|
1293
|
+
* A global namespace for `userAgentData` object.
|
1294
|
+
*/
|
1295
|
+
const userAgentData = uaDATA;
|
1296
|
+
|
1297
|
+
const { userAgent: userAgentString } = navigator;
|
1298
|
+
|
1299
|
+
/**
|
1300
|
+
* A global namespace for `navigator.userAgent` string.
|
1301
|
+
*/
|
1302
|
+
const userAgent = userAgentString;
|
1303
|
+
|
1304
|
+
const mobileBrands = /iPhone|iPad|iPod|Android/i;
|
1305
|
+
let isMobileCheck = false;
|
1306
|
+
|
1307
|
+
if (userAgentData) {
|
1308
|
+
isMobileCheck = userAgentData.brands
|
1309
|
+
.some((/** @type {Record<String, any>} */x) => mobileBrands.test(x.brand));
|
1310
|
+
} else {
|
1311
|
+
isMobileCheck = mobileBrands.test(userAgent);
|
1312
|
+
}
|
1313
|
+
|
1314
|
+
/**
|
1315
|
+
* A global `boolean` for mobile detection.
|
1316
|
+
* @type {boolean}
|
1317
|
+
*/
|
1318
|
+
const isMobile = isMobileCheck;
|
1319
|
+
|
1320
|
+
let elementUID = 1;
|
1321
|
+
const elementIDMap = new Map();
|
1322
|
+
|
1323
|
+
/**
|
1324
|
+
* Returns a unique identifier for popover, tooltip, scrollspy.
|
1325
|
+
*
|
1326
|
+
* @param {HTMLElement | Element} element target element
|
1327
|
+
* @param {string=} key predefined key
|
1328
|
+
* @returns {number} an existing or new unique ID
|
1329
|
+
*/
|
1330
|
+
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);
|
1347
|
+
}
|
1348
|
+
} else if (!Number.isNaN(elMap)) {
|
1349
|
+
result = elMap;
|
1350
|
+
} else {
|
1351
|
+
elementIDMap.set(element, result);
|
1352
|
+
}
|
1353
|
+
return result;
|
1354
|
+
}
|
1355
|
+
|
1356
|
+
/**
|
1357
|
+
* Returns the bounding client rect of a target `HTMLElement`.
|
1358
|
+
*
|
1359
|
+
* @see https://github.com/floating-ui/floating-ui
|
1360
|
+
*
|
1361
|
+
* @param {HTMLElement | Element} element event.target
|
1362
|
+
* @param {boolean=} includeScale when *true*, the target scale is also computed
|
1363
|
+
* @returns {SHORTER.BoundingClientRect} the bounding client rect object
|
1364
|
+
*/
|
1365
|
+
function getBoundingClientRect(element, includeScale) {
|
1366
|
+
const {
|
1367
|
+
width, height, top, right, bottom, left,
|
1368
|
+
} = element.getBoundingClientRect();
|
1369
|
+
let scaleX = 1;
|
1370
|
+
let scaleY = 1;
|
1371
|
+
|
1372
|
+
if (includeScale && element instanceof HTMLElement) {
|
1373
|
+
const { offsetWidth, offsetHeight } = element;
|
1374
|
+
scaleX = offsetWidth > 0 ? Math.round(width) / offsetWidth || 1 : 1;
|
1375
|
+
scaleY = offsetHeight > 0 ? Math.round(height) / offsetHeight || 1 : 1;
|
1376
|
+
}
|
1377
|
+
|
1378
|
+
return {
|
1379
|
+
width: width / scaleX,
|
1380
|
+
height: height / scaleY,
|
1381
|
+
top: top / scaleY,
|
1382
|
+
right: right / scaleX,
|
1383
|
+
bottom: bottom / scaleY,
|
1384
|
+
left: left / scaleX,
|
1385
|
+
x: left / scaleX,
|
1386
|
+
y: top / scaleY,
|
1387
|
+
};
|
1388
|
+
}
|
1389
|
+
|
1390
|
+
/**
|
1391
|
+
* A shortcut for `(document|Element).querySelectorAll`.
|
1392
|
+
*
|
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
|
1396
|
+
*/
|
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);
|
1402
|
+
}
|
1403
|
+
|
1404
|
+
/**
|
1405
|
+
* Shortcut for `HTMLElement.closest` method which also works
|
1406
|
+
* with children of `ShadowRoot`. The order of the parameters
|
1407
|
+
* is intentional since they're both required.
|
1408
|
+
*
|
1409
|
+
* @see https://stackoverflow.com/q/54520554/803358
|
1410
|
+
*
|
1411
|
+
* @param {HTMLElement | Element} element Element to look into
|
1412
|
+
* @param {string} selector the selector name
|
1413
|
+
* @return {(HTMLElement | Element)?} the query result
|
1414
|
+
*/
|
1415
|
+
function closest(element, selector) {
|
1416
|
+
return element ? (element.closest(selector)
|
1417
|
+
// @ts-ignore -- break out of `ShadowRoot`
|
1418
|
+
|| closest(element.getRootNode().host, selector)) : null;
|
1419
|
+
}
|
1420
|
+
|
1421
|
+
/**
|
1422
|
+
* This is a shortie for `document.createElementNS` method
|
1423
|
+
* which allows you to create a new `HTMLElement` for a given `tagName`
|
1424
|
+
* or based on an object with specific non-readonly attributes:
|
1425
|
+
* `id`, `className`, `textContent`, `style`, etc.
|
1426
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS
|
1427
|
+
*
|
1428
|
+
* @param {string} namespace `namespaceURI` to associate with the new `HTMLElement`
|
1429
|
+
* @param {Record<string, string> | string} param `tagName` or object
|
1430
|
+
* @return {HTMLElement | Element} a new `HTMLElement` or `Element`
|
1431
|
+
*/
|
1432
|
+
function createElementNS(namespace, param) {
|
1433
|
+
if (typeof param === 'string') {
|
1434
|
+
return getDocument().createElementNS(namespace, param);
|
1435
|
+
}
|
1436
|
+
|
1437
|
+
const { tagName } = param;
|
1438
|
+
const attr = { ...param };
|
1439
|
+
const newElement = createElementNS(namespace, tagName);
|
1440
|
+
delete attr.tagName;
|
1441
|
+
ObjectAssign(newElement, attr);
|
1442
|
+
return newElement;
|
1443
|
+
}
|
1444
|
+
|
1445
|
+
/**
|
1446
|
+
* Shortcut for the `Element.dispatchEvent(Event)` method.
|
1447
|
+
*
|
1448
|
+
* @param {HTMLElement | Element} element is the target
|
1449
|
+
* @param {Event} event is the `Event` object
|
1450
|
+
*/
|
1451
|
+
const dispatchEvent = (element, event) => element.dispatchEvent(event);
|
1452
|
+
|
1453
|
+
/** @type {Map<string, Map<HTMLElement | Element, Record<string, any>>>} */
|
1454
|
+
const componentData = new Map();
|
1455
|
+
/**
|
1456
|
+
* An interface for web components background data.
|
1457
|
+
* @see https://github.com/thednp/bootstrap.native/blob/master/src/components/base-component.js
|
1458
|
+
*/
|
1459
|
+
const Data = {
|
1460
|
+
/**
|
1461
|
+
* Sets web components data.
|
1462
|
+
* @param {HTMLElement | Element | string} target target element
|
1463
|
+
* @param {string} component the component's name or a unique key
|
1464
|
+
* @param {Record<string, any>} instance the component instance
|
1465
|
+
*/
|
1466
|
+
set: (target, component, instance) => {
|
1467
|
+
const element = querySelector(target);
|
1468
|
+
if (!element) return;
|
1469
|
+
|
1470
|
+
if (!componentData.has(component)) {
|
1471
|
+
componentData.set(component, new Map());
|
1472
|
+
}
|
1473
|
+
|
1474
|
+
const instanceMap = componentData.get(component);
|
1475
|
+
// @ts-ignore - not undefined, but defined right above
|
1476
|
+
instanceMap.set(element, instance);
|
1477
|
+
},
|
1478
|
+
|
1479
|
+
/**
|
1480
|
+
* Returns all instances for specified component.
|
1481
|
+
* @param {string} component the component's name or a unique key
|
1482
|
+
* @returns {Map<HTMLElement | Element, Record<string, any>>?} all the component instances
|
1483
|
+
*/
|
1484
|
+
getAllFor: (component) => {
|
1485
|
+
const instanceMap = componentData.get(component);
|
1486
|
+
|
1487
|
+
return instanceMap || null;
|
1488
|
+
},
|
1489
|
+
|
1490
|
+
/**
|
1491
|
+
* Returns the instance associated with the target.
|
1492
|
+
* @param {HTMLElement | Element | string} target target element
|
1493
|
+
* @param {string} component the component's name or a unique key
|
1494
|
+
* @returns {Record<string, any>?} the instance
|
1495
|
+
*/
|
1496
|
+
get: (target, component) => {
|
1497
|
+
const element = querySelector(target);
|
1498
|
+
const allForC = Data.getAllFor(component);
|
1499
|
+
const instance = element && allForC && allForC.get(element);
|
1500
|
+
|
1501
|
+
return instance || null;
|
1502
|
+
},
|
1503
|
+
|
1504
|
+
/**
|
1505
|
+
* Removes web components data.
|
1506
|
+
* @param {HTMLElement | Element | string} target target element
|
1507
|
+
* @param {string} component the component's name or a unique key
|
1508
|
+
*/
|
1509
|
+
remove: (target, component) => {
|
1510
|
+
const element = querySelector(target);
|
1511
|
+
const instanceMap = componentData.get(component);
|
1512
|
+
if (!instanceMap || !element) return;
|
1513
|
+
|
1514
|
+
instanceMap.delete(element);
|
1515
|
+
|
1516
|
+
if (instanceMap.size === 0) {
|
1517
|
+
componentData.delete(component);
|
1518
|
+
}
|
1519
|
+
},
|
1520
|
+
};
|
1521
|
+
|
1522
|
+
/**
|
1523
|
+
* An alias for `Data.get()`.
|
1524
|
+
* @type {SHORTER.getInstance<any>}
|
1525
|
+
*/
|
1526
|
+
const getInstance = (target, component) => Data.get(target, component);
|
1527
|
+
|
1528
|
+
/**
|
1529
|
+
* Check class in `HTMLElement.classList`.
|
1530
|
+
*
|
1531
|
+
* @param {HTMLElement | Element} element target
|
1532
|
+
* @param {string} classNAME to check
|
1533
|
+
* @returns {boolean}
|
1534
|
+
*/
|
1535
|
+
function hasClass(element, classNAME) {
|
1536
|
+
return element.classList.contains(classNAME);
|
1537
|
+
}
|
1538
|
+
|
1539
|
+
/**
|
1540
|
+
* Add class to `HTMLElement.classList`.
|
1541
|
+
*
|
1542
|
+
* @param {HTMLElement | Element} element target
|
1543
|
+
* @param {string} classNAME to add
|
1544
|
+
* @returns {void}
|
1545
|
+
*/
|
1546
|
+
function addClass(element, classNAME) {
|
1547
|
+
element.classList.add(classNAME);
|
1548
|
+
}
|
1549
|
+
|
1550
|
+
/**
|
1551
|
+
* Remove class from `HTMLElement.classList`.
|
1552
|
+
*
|
1553
|
+
* @param {HTMLElement | Element} element target
|
1554
|
+
* @param {string} classNAME to remove
|
1555
|
+
* @returns {void}
|
1556
|
+
*/
|
1557
|
+
function removeClass(element, classNAME) {
|
1558
|
+
element.classList.remove(classNAME);
|
1559
|
+
}
|
1560
|
+
|
1561
|
+
/**
|
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.
|
1571
|
+
* @param {HTMLElement | Element} element target element
|
1572
|
+
* @param {string} attribute attribute name
|
1573
|
+
* @param {string} value attribute value
|
1574
|
+
* @returns {void}
|
1575
|
+
*/
|
1576
|
+
const setAttribute = (element, attribute, value) => element.setAttribute(attribute, value);
|
1577
|
+
|
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);
|
1585
|
+
|
1586
|
+
/**
|
1587
|
+
* Shortcut for `HTMLElement.removeAttribute()` method.
|
1588
|
+
* @param {HTMLElement | Element} element target element
|
1589
|
+
* @param {string} attribute attribute name
|
1590
|
+
* @returns {void}
|
1591
|
+
*/
|
1592
|
+
const removeAttribute = (element, attribute) => element.removeAttribute(attribute);
|
1593
|
+
|
1594
|
+
/**
|
1595
|
+
* Shortcut for `String.toUpperCase()`.
|
1596
|
+
*
|
1597
|
+
* @param {string} source input string
|
1598
|
+
* @returns {string} uppercase output string
|
1599
|
+
*/
|
1600
|
+
const toUpperCase = (source) => source.toUpperCase();
|
1601
|
+
|
1602
|
+
const vHidden = 'v-hidden';
|
1603
|
+
|
1604
|
+
/**
|
1605
|
+
* Returns the color form.
|
1606
|
+
* @param {CP.ColorPicker} self the `ColorPicker` instance
|
1607
|
+
* @returns {HTMLElement | Element}
|
1608
|
+
*/
|
1609
|
+
function getColorForm(self) {
|
1610
|
+
const { format, id } = self;
|
1611
|
+
const colorForm = createElement({
|
1612
|
+
tagName: 'div',
|
1613
|
+
className: `color-form ${format}`,
|
1614
|
+
});
|
1615
|
+
|
1616
|
+
let components = ['hex'];
|
1617
|
+
if (format === 'rgb') components = ['red', 'green', 'blue', 'alpha'];
|
1618
|
+
else if (format === 'hsl') components = ['hue', 'saturation', 'lightness', 'alpha'];
|
1619
|
+
|
1620
|
+
components.forEach((c) => {
|
1621
|
+
const [C] = format === 'hex' ? ['#'] : toUpperCase(c).split('');
|
1622
|
+
const cID = `color_${format}_${c}_${id}`;
|
1623
|
+
const cInputLabel = createElement({ tagName: 'label' });
|
1624
|
+
setAttribute(cInputLabel, 'for', cID);
|
1625
|
+
cInputLabel.append(
|
1626
|
+
createElement({ tagName: 'span', ariaHidden: 'true', innerText: `${C}:` }),
|
1627
|
+
createElement({ tagName: 'span', className: vHidden, innerText: `${c}` }),
|
1628
|
+
);
|
1629
|
+
const cInput = createElement({
|
1630
|
+
tagName: 'input',
|
1631
|
+
id: cID, // name: cID,
|
1632
|
+
type: format === 'hex' ? 'text' : 'number',
|
1633
|
+
value: c === 'alpha' ? '1' : '0',
|
1634
|
+
className: `color-input ${c}`,
|
1635
|
+
autocomplete: 'off',
|
1636
|
+
spellcheck: 'false',
|
1637
|
+
});
|
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'; }
|
1644
|
+
}
|
1645
|
+
ObjectAssign(cInput, {
|
1646
|
+
min: '0',
|
1647
|
+
max,
|
1648
|
+
step,
|
1649
|
+
});
|
1650
|
+
}
|
1651
|
+
colorForm.append(cInputLabel, cInput);
|
1652
|
+
});
|
1653
|
+
return colorForm;
|
1654
|
+
}
|
1655
|
+
|
1656
|
+
/**
|
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}
|
1664
|
+
*/
|
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({
|
1669
|
+
tagName: 'div',
|
1670
|
+
className: 'color-control',
|
1671
|
+
});
|
1672
|
+
setAttribute(control, 'role', 'presentation');
|
1673
|
+
|
1674
|
+
control.append(
|
1675
|
+
createElement({
|
1676
|
+
id: labelID,
|
1677
|
+
tagName: 'label',
|
1678
|
+
className: `color-label ${vHidden}`,
|
1679
|
+
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`,
|
1693
|
+
});
|
1694
|
+
setAttribute(knob, ariaLabelledBy, labelledby || labelID);
|
1695
|
+
setAttribute(knob, 'tabindex', '0');
|
1696
|
+
control.append(knob);
|
1697
|
+
return control;
|
1698
|
+
}
|
1699
|
+
|
1700
|
+
// ColorPicker GC
|
1701
|
+
// ==============
|
1702
|
+
const colorPickerString = 'color-picker';
|
1703
|
+
const colorPickerSelector = `[data-function="${colorPickerString}"]`;
|
1704
|
+
const nonColors = ['transparent', 'currentColor', 'inherit', 'initial'];
|
1705
|
+
const colorNames$1 = ['white', 'black', 'grey', 'red', 'orange', 'brown', 'gold', 'olive', 'yellow', 'lime', 'green', 'teal', 'cyan', 'blue', 'violet', 'magenta', 'pink'];
|
1706
|
+
const colorPickerLabels = {
|
1707
|
+
pickerLabel: 'Colour Picker',
|
1708
|
+
toggleLabel: 'Select colour',
|
1709
|
+
menuLabel: 'Select colour preset',
|
1710
|
+
requiredLabel: 'Required',
|
1711
|
+
formatLabel: 'Colour Format',
|
1712
|
+
formatHEX: 'Hexadecimal Format',
|
1713
|
+
formatRGB: 'RGB Format',
|
1714
|
+
formatHSL: 'HSL Format',
|
1715
|
+
alphaLabel: 'Alpha',
|
1716
|
+
appearanceLabel: 'Colour Appearance',
|
1717
|
+
hexLabel: 'Hexadecimal',
|
1718
|
+
hueLabel: 'Hue',
|
1719
|
+
saturationLabel: 'Saturation',
|
1720
|
+
lightnessLabel: 'Lightness',
|
1721
|
+
redLabel: 'Red',
|
1722
|
+
greenLabel: 'Green',
|
1723
|
+
blueLabel: 'Blue',
|
1724
|
+
};
|
1725
|
+
|
1726
|
+
// ColorPicker Static Methods
|
1727
|
+
// ==========================
|
1728
|
+
|
1729
|
+
/** @type {CP.GetInstance<ColorPicker>} */
|
1730
|
+
const getColorPickerInstance = (element) => getInstance(element, colorPickerString);
|
1731
|
+
|
1732
|
+
/** @type {CP.InitCallback<ColorPicker>} */
|
1733
|
+
const initColorPicker = (element) => new ColorPicker(element);
|
1734
|
+
|
1735
|
+
// ColorPicker Private Methods
|
1736
|
+
// ===========================
|
1737
|
+
|
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
|
+
/**
|
1758
|
+
* Generate HTML markup and update instance properties.
|
1759
|
+
* @param {ColorPicker} self
|
1760
|
+
*/
|
1761
|
+
function initCallback(self) {
|
1762
|
+
const {
|
1763
|
+
input, parent, format, id, componentLabels, keywords,
|
1764
|
+
} = self;
|
1765
|
+
const colorValue = getAttribute(input, 'value') || '#fff';
|
1766
|
+
|
1767
|
+
const {
|
1768
|
+
toggleLabel, menuLabel, formatLabel, pickerLabel, appearanceLabel,
|
1769
|
+
} = componentLabels;
|
1770
|
+
|
1771
|
+
// update color
|
1772
|
+
const color = nonColors.includes(colorValue) ? '#fff' : colorValue;
|
1773
|
+
self.color = new Color(color, { format });
|
1774
|
+
|
1775
|
+
// set initial controls dimensions
|
1776
|
+
// make the controls smaller on mobile
|
1777
|
+
const cv1w = isMobile ? 150 : 230;
|
1778
|
+
const cvh = isMobile ? 150 : 230;
|
1779
|
+
const cv2w = 21;
|
1780
|
+
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}`;
|
1783
|
+
|
1784
|
+
const pickerBtn = createElement({
|
1785
|
+
tagName: 'button',
|
1786
|
+
className: 'picker-toggle button-appearance',
|
1787
|
+
ariaExpanded: 'false',
|
1788
|
+
ariaHasPopup: 'true',
|
1789
|
+
ariaLive: 'polite',
|
1790
|
+
});
|
1791
|
+
setAttribute(pickerBtn, 'tabindex', '-1');
|
1792
|
+
pickerBtn.append(createElement({
|
1793
|
+
tagName: 'span',
|
1794
|
+
className: vHidden,
|
1795
|
+
innerText: 'Open Color Picker',
|
1796
|
+
}));
|
1797
|
+
|
1798
|
+
const colorPickerDropdown = createElement({
|
1799
|
+
tagName: 'div',
|
1800
|
+
className: `color-dropdown picker${dropClass}`,
|
1801
|
+
});
|
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
|
+
}
|
1844
|
+
|
1845
|
+
// @ts-ignore
|
1846
|
+
const colorForm = getColorForm(self);
|
1847
|
+
colorPickerDropdown.append(colorControls, colorForm);
|
1848
|
+
parent.append(pickerBtn, colorPickerDropdown);
|
1849
|
+
|
1850
|
+
// set color key menu template
|
1851
|
+
if (keywords) {
|
1852
|
+
const colorKeys = keywords;
|
1853
|
+
const presetsDropdown = createElement({
|
1854
|
+
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);
|
1881
|
+
});
|
1882
|
+
const presetsBtn = createElement({
|
1883
|
+
tagName: 'button',
|
1884
|
+
className: 'menu-toggle button-appearance',
|
1885
|
+
ariaExpanded: 'false',
|
1886
|
+
ariaHasPopup: 'true',
|
1887
|
+
});
|
1888
|
+
const xmlns = encodeURI('http://www.w3.org/2000/svg');
|
1889
|
+
const presetsIcon = createElementNS(xmlns, { tagName: 'svg' });
|
1890
|
+
setAttribute(presetsIcon, 'xmlns', xmlns);
|
1891
|
+
setAttribute(presetsIcon, ariaHidden, 'true');
|
1892
|
+
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);
|
1897
|
+
presetsBtn.append(createElement({
|
1898
|
+
tagName: 'span',
|
1899
|
+
className: vHidden,
|
1900
|
+
innerText: `${toggleLabel}`,
|
1901
|
+
}), presetsIcon);
|
1902
|
+
|
1903
|
+
parent.append(presetsBtn, presetsDropdown);
|
1904
|
+
}
|
1905
|
+
|
1906
|
+
// solve non-colors after settings save
|
1907
|
+
if (keywords && nonColors.includes(colorValue)) {
|
1908
|
+
self.value = colorValue;
|
1909
|
+
}
|
1910
|
+
}
|
1911
|
+
|
1912
|
+
/**
|
1913
|
+
* Add / remove `ColorPicker` event listeners active only when open.
|
1914
|
+
* @param {ColorPicker} self
|
1915
|
+
* @param {boolean=} action
|
1916
|
+
*/
|
1917
|
+
function toggleEventsOnShown(self, action) {
|
1918
|
+
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' };
|
1922
|
+
|
1923
|
+
fn(self.controls, pointerEvents.down, self.pointerDown);
|
1924
|
+
self.controlKnobs.forEach((x) => fn(x, 'keydown', self.handleKnobs));
|
1925
|
+
|
1926
|
+
fn(window, 'scroll', self.handleScroll);
|
1927
|
+
|
1928
|
+
[self.input, ...self.inputs].forEach((x) => fn(x, 'change', self.changeHandler));
|
1929
|
+
|
1930
|
+
if (self.colorMenu) {
|
1931
|
+
fn(self.colorMenu, 'click', self.menuClickHandler);
|
1932
|
+
fn(self.colorMenu, 'keydown', self.menuKeyHandler);
|
1933
|
+
}
|
1934
|
+
|
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);
|
1939
|
+
}
|
1940
|
+
|
1941
|
+
/**
|
1942
|
+
* Triggers the `ColorPicker` original event.
|
1943
|
+
* @param {ColorPicker} self
|
1944
|
+
*/
|
1945
|
+
function firePickerChange(self) {
|
1946
|
+
dispatchEvent(self.input, new CustomEvent('colorpicker.change'));
|
1947
|
+
}
|
1948
|
+
|
1949
|
+
/**
|
1950
|
+
* Toggles the visibility of a dropdown or returns false if none is visible.
|
1951
|
+
* @param {HTMLElement} element
|
1952
|
+
* @param {boolean=} check
|
1953
|
+
* @returns {void | boolean}
|
1954
|
+
*/
|
1955
|
+
function classToggle(element, check) {
|
1956
|
+
const fn1 = !check ? 'forEach' : 'some';
|
1957
|
+
const fn2 = !check ? removeClass : hasClass;
|
1958
|
+
|
1959
|
+
if (element) {
|
1960
|
+
return ['show', 'show-top'][fn1]((x) => fn2(element, x));
|
1961
|
+
}
|
1962
|
+
|
1963
|
+
return false;
|
1964
|
+
}
|
1965
|
+
|
1966
|
+
/**
|
1967
|
+
* Shows the `ColorPicker` presets menu.
|
1968
|
+
* @param {ColorPicker} self
|
1969
|
+
*/
|
1970
|
+
function showMenu(self) {
|
1971
|
+
classToggle(self.colorPicker);
|
1972
|
+
addClass(self.colorMenu, 'show');
|
1973
|
+
self.show();
|
1974
|
+
setAttribute(self.menuToggle, ariaExpanded, 'true');
|
1975
|
+
}
|
1976
|
+
|
1977
|
+
/**
|
1978
|
+
* Color Picker
|
1979
|
+
* @see http://thednp.github.io/color-picker
|
1980
|
+
*/
|
1981
|
+
class ColorPicker {
|
1982
|
+
/**
|
1983
|
+
* Returns a new ColorPicker instance.
|
1984
|
+
* @param {HTMLInputElement | string} target the target `<input>` element
|
1985
|
+
*/
|
1986
|
+
constructor(target) {
|
1987
|
+
const self = this;
|
1988
|
+
/** @type {HTMLInputElement} */
|
1989
|
+
// @ts-ignore
|
1990
|
+
self.input = querySelector(target);
|
1991
|
+
// invalidate
|
1992
|
+
if (!self.input) throw new TypeError(`ColorPicker target ${target} cannot be found.`);
|
1993
|
+
const { input } = self;
|
1994
|
+
|
1995
|
+
/** @type {HTMLElement} */
|
1996
|
+
// @ts-ignore
|
1997
|
+
self.parent = closest(input, `.${colorPickerString},${colorPickerString}`);
|
1998
|
+
if (!self.parent) throw new TypeError('ColorPicker requires a specific markup to work.');
|
1999
|
+
|
2000
|
+
/** @type {number} */
|
2001
|
+
self.id = getUID(input, colorPickerString);
|
2002
|
+
|
2003
|
+
// set initial state
|
2004
|
+
/** @type {HTMLCanvasElement?} */
|
2005
|
+
self.dragElement = null;
|
2006
|
+
/** @type {boolean} */
|
2007
|
+
self.isOpen = false;
|
2008
|
+
/** @type {Record<string, number>} */
|
2009
|
+
self.controlPositions = {
|
2010
|
+
c1x: 0, c1y: 0, c2y: 0, c3y: 0,
|
2011
|
+
};
|
2012
|
+
/** @type {Record<string, string>} */
|
2013
|
+
self.colorLabels = {};
|
2014
|
+
/** @type {Array<string> | false} */
|
2015
|
+
self.keywords = false;
|
2016
|
+
/** @type {Color} */
|
2017
|
+
self.color = new Color('white', { format: self.format });
|
2018
|
+
/** @type {Record<string, string>} */
|
2019
|
+
self.componentLabels = ObjectAssign({}, colorPickerLabels);
|
2020
|
+
|
2021
|
+
const { componentLabels, colorLabels, keywords } = input.dataset;
|
2022
|
+
const temp = componentLabels ? JSON.parse(componentLabels) : {};
|
2023
|
+
self.componentLabels = ObjectAssign(self.componentLabels, temp);
|
2024
|
+
|
2025
|
+
const translatedColorLabels = colorLabels && colorLabels.split(',').length === 17
|
2026
|
+
? colorLabels.split(',') : colorNames$1;
|
2027
|
+
|
2028
|
+
// expose color labels to all methods
|
2029
|
+
colorNames$1.forEach((c, i) => { self.colorLabels[c] = translatedColorLabels[i]; });
|
2030
|
+
|
2031
|
+
// set colour presets
|
2032
|
+
if (keywords !== 'false') {
|
2033
|
+
self.keywords = keywords ? keywords.split(',') : nonColors;
|
2034
|
+
}
|
2035
|
+
|
2036
|
+
// bind events
|
2037
|
+
self.showPicker = self.showPicker.bind(self);
|
2038
|
+
self.togglePicker = self.togglePicker.bind(self);
|
2039
|
+
self.toggleMenu = self.toggleMenu.bind(self);
|
2040
|
+
self.menuClickHandler = self.menuClickHandler.bind(self);
|
2041
|
+
self.menuKeyHandler = self.menuKeyHandler.bind(self);
|
2042
|
+
self.pointerDown = self.pointerDown.bind(self);
|
2043
|
+
self.pointerMove = self.pointerMove.bind(self);
|
2044
|
+
self.pointerUp = self.pointerUp.bind(self);
|
2045
|
+
self.handleScroll = self.handleScroll.bind(self);
|
2046
|
+
self.handleFocusOut = self.handleFocusOut.bind(self);
|
2047
|
+
self.changeHandler = self.changeHandler.bind(self);
|
2048
|
+
self.handleDismiss = self.handleDismiss.bind(self);
|
2049
|
+
self.keyHandler = self.keyHandler.bind(self);
|
2050
|
+
self.handleKnobs = self.handleKnobs.bind(self);
|
2051
|
+
|
2052
|
+
// generate markup
|
2053
|
+
initCallback(self);
|
2054
|
+
|
2055
|
+
const { parent } = self;
|
2056
|
+
// set main elements
|
2057
|
+
/** @type {HTMLElement} */
|
2058
|
+
// @ts-ignore
|
2059
|
+
self.pickerToggle = querySelector('.picker-toggle', parent);
|
2060
|
+
/** @type {HTMLElement} */
|
2061
|
+
// @ts-ignore
|
2062
|
+
self.menuToggle = querySelector('.menu-toggle', parent);
|
2063
|
+
/** @type {HTMLElement} */
|
2064
|
+
// @ts-ignore
|
2065
|
+
self.colorMenu = querySelector('.color-dropdown.menu', parent);
|
2066
|
+
/** @type {HTMLElement} */
|
2067
|
+
// @ts-ignore
|
2068
|
+
self.colorPicker = querySelector('.color-dropdown.picker', parent);
|
2069
|
+
/** @type {HTMLElement} */
|
2070
|
+
// @ts-ignore
|
2071
|
+
self.controls = querySelector('.color-controls', parent);
|
2072
|
+
/** @type {HTMLInputElement[]} */
|
2073
|
+
// @ts-ignore
|
2074
|
+
self.inputs = [...querySelectorAll('.color-input', parent)];
|
2075
|
+
/** @type {(HTMLElement)[]} */
|
2076
|
+
// @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);
|
2105
|
+
|
2106
|
+
/** @type {number} */
|
2107
|
+
self.width3 = 0;
|
2108
|
+
/** @type {number} */
|
2109
|
+
self.height3 = 0;
|
2110
|
+
|
2111
|
+
// set alpha control except hex
|
2112
|
+
if (self.format !== 'hex') {
|
2113
|
+
self.width3 = v3.width;
|
2114
|
+
self.height3 = v3.height;
|
2115
|
+
/** @type {*} */
|
2116
|
+
this.ctx3 = v3.getContext('2d');
|
2117
|
+
self.ctx3.rect(0, 0, self.width3, self.height3);
|
2118
|
+
}
|
2119
|
+
|
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
|
+
// add main events listeners
|
2128
|
+
toggleEvents(self, true);
|
2129
|
+
|
2130
|
+
// set component data
|
2131
|
+
Data.set(input, colorPickerString, self);
|
2132
|
+
}
|
2133
|
+
|
2134
|
+
/** Returns the current color value */
|
2135
|
+
get value() { return this.input.value; }
|
2136
|
+
|
2137
|
+
/**
|
2138
|
+
* Sets a new color value.
|
2139
|
+
* @param {string} v new color value
|
2140
|
+
*/
|
2141
|
+
set value(v) { this.input.value = v; }
|
2142
|
+
|
2143
|
+
/** Check if the input is required to have a valid value. */
|
2144
|
+
get required() { return hasAttribute(this.input, 'required'); }
|
2145
|
+
|
2146
|
+
/**
|
2147
|
+
* Returns the colour format.
|
2148
|
+
* @returns {CP.ColorFormats | string}
|
2149
|
+
*/
|
2150
|
+
get format() { return getAttribute(this.input, 'format') || 'hex'; }
|
2151
|
+
|
2152
|
+
/** Returns the input name. */
|
2153
|
+
get name() { return getAttribute(this.input, 'name'); }
|
2154
|
+
|
2155
|
+
/**
|
2156
|
+
* Returns the label associated to the input.
|
2157
|
+
* @returns {HTMLLabelElement?}
|
2158
|
+
*/
|
2159
|
+
// @ts-ignore
|
2160
|
+
get label() { return querySelector(`[for="${this.input.id}"]`); }
|
2161
|
+
|
2162
|
+
/** Check if the color presets include any non-color. */
|
2163
|
+
get includeNonColor() {
|
2164
|
+
return this.keywords instanceof Array
|
2165
|
+
&& this.keywords.some((x) => nonColors.includes(x));
|
2166
|
+
}
|
2167
|
+
|
2168
|
+
/** Returns hexadecimal value of the current color. */
|
2169
|
+
get hex() { return this.color.toHex(); }
|
2170
|
+
|
2171
|
+
/** Returns the current color value in {h,s,v,a} object format. */
|
2172
|
+
get hsv() { return this.color.toHsv(); }
|
2173
|
+
|
2174
|
+
/** Returns the current color value in {h,s,l,a} object format. */
|
2175
|
+
get hsl() { return this.color.toHsl(); }
|
2176
|
+
|
2177
|
+
/** Returns the current color value in {r,g,b,a} object format. */
|
2178
|
+
get rgb() { return this.color.toRgb(); }
|
2179
|
+
|
2180
|
+
/** Returns the current color brightness. */
|
2181
|
+
get brightness() { return this.color.brightness; }
|
2182
|
+
|
2183
|
+
/** Returns the current color luminance. */
|
2184
|
+
get luminance() { return this.color.luminance; }
|
2185
|
+
|
2186
|
+
/** Checks if the current colour requires a light text color. */
|
2187
|
+
get isDark() {
|
2188
|
+
const { rgb, brightness } = this;
|
2189
|
+
return brightness < 120 && rgb.a > 0.33;
|
2190
|
+
}
|
2191
|
+
|
2192
|
+
/** Checks if the current input value is a valid color. */
|
2193
|
+
get isValid() {
|
2194
|
+
const inputValue = this.input.value;
|
2195
|
+
return inputValue !== '' && new Color(inputValue).isValid;
|
2196
|
+
}
|
2197
|
+
|
2198
|
+
/** Updates `ColorPicker` visuals. */
|
2199
|
+
updateVisuals() {
|
2200
|
+
const self = this;
|
2201
|
+
const {
|
2202
|
+
color, format, controlPositions,
|
2203
|
+
width1, width2, width3,
|
2204
|
+
height1, height2, height3,
|
2205
|
+
ctx1, ctx2, ctx3,
|
2206
|
+
} = self;
|
2207
|
+
const { r, g, b } = color;
|
2208
|
+
|
2209
|
+
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);
|
2236
|
+
} 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);
|
2280
|
+
}
|
2281
|
+
}
|
2282
|
+
|
2283
|
+
/**
|
2284
|
+
* Handles the `focusout` listener of the `ColorPicker`.
|
2285
|
+
* @param {FocusEvent} e
|
2286
|
+
* @this {ColorPicker}
|
2287
|
+
*/
|
2288
|
+
handleFocusOut({ relatedTarget }) {
|
2289
|
+
// @ts-ignore
|
2290
|
+
if (relatedTarget && !this.parent.contains(relatedTarget)) {
|
2291
|
+
this.hide(true);
|
2292
|
+
}
|
2293
|
+
}
|
2294
|
+
|
2295
|
+
/**
|
2296
|
+
* Handles the `focusout` listener of the `ColorPicker`.
|
2297
|
+
* @param {KeyboardEvent} e
|
2298
|
+
* @this {ColorPicker}
|
2299
|
+
*/
|
2300
|
+
handleDismiss({ code }) {
|
2301
|
+
const self = this;
|
2302
|
+
if (self.isOpen && code === keyEscape) {
|
2303
|
+
self.hide();
|
2304
|
+
}
|
2305
|
+
}
|
2306
|
+
|
2307
|
+
/**
|
2308
|
+
* Handles the `ColorPicker` scroll listener when open.
|
2309
|
+
* @param {Event} e
|
2310
|
+
* @this {ColorPicker}
|
2311
|
+
*/
|
2312
|
+
handleScroll(e) {
|
2313
|
+
const self = this;
|
2314
|
+
/** @type {*} */
|
2315
|
+
const { activeElement } = document;
|
2316
|
+
|
2317
|
+
if ((isMobile && self.dragElement)
|
2318
|
+
|| (activeElement && self.controlKnobs.includes(activeElement))) {
|
2319
|
+
e.stopPropagation();
|
2320
|
+
e.preventDefault();
|
2321
|
+
}
|
2322
|
+
|
2323
|
+
self.updateDropdownPosition();
|
2324
|
+
}
|
2325
|
+
|
2326
|
+
/**
|
2327
|
+
* Handles all `ColorPicker` click listeners.
|
2328
|
+
* @param {KeyboardEvent} e
|
2329
|
+
* @this {ColorPicker}
|
2330
|
+
*/
|
2331
|
+
menuKeyHandler(e) {
|
2332
|
+
const { target, code } = e;
|
2333
|
+
|
2334
|
+
if ([keyArrowDown, keyArrowUp].includes(code)) {
|
2335
|
+
e.preventDefault();
|
2336
|
+
} else if ([keyEnter, keySpace].includes(code)) {
|
2337
|
+
this.menuClickHandler({ target });
|
2338
|
+
}
|
2339
|
+
}
|
2340
|
+
|
2341
|
+
/**
|
2342
|
+
* Handles all `ColorPicker` click listeners.
|
2343
|
+
* @param {Partial<Event>} e
|
2344
|
+
* @this {ColorPicker}
|
2345
|
+
*/
|
2346
|
+
menuClickHandler(e) {
|
2347
|
+
const self = this;
|
2348
|
+
/** @type {*} */
|
2349
|
+
const { target } = e;
|
2350
|
+
const { format } = self;
|
2351
|
+
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();
|
2360
|
+
|
2361
|
+
if (currentActive) {
|
2362
|
+
removeClass(currentActive, 'active');
|
2363
|
+
removeAttribute(currentActive, ariaSelected);
|
2364
|
+
}
|
2365
|
+
|
2366
|
+
if (currentActive !== target) {
|
2367
|
+
addClass(target, 'active');
|
2368
|
+
setAttribute(target, ariaSelected, 'true');
|
2369
|
+
|
2370
|
+
if (nonColors.includes(newOption)) {
|
2371
|
+
self.value = newOption;
|
2372
|
+
firePickerChange(self);
|
2373
|
+
}
|
2374
|
+
}
|
2375
|
+
}
|
2376
|
+
|
2377
|
+
/**
|
2378
|
+
* Handles the `ColorPicker` touchstart / mousedown events listeners.
|
2379
|
+
* @param {TouchEvent} e
|
2380
|
+
* @this {ColorPicker}
|
2381
|
+
*/
|
2382
|
+
pointerDown(e) {
|
2383
|
+
const self = this;
|
2384
|
+
const {
|
2385
|
+
// @ts-ignore
|
2386
|
+
type, target, touches, pageX, pageY,
|
2387
|
+
} = e;
|
2388
|
+
const { visuals, controlKnobs, format } = self;
|
2389
|
+
const [v1, v2, v3] = visuals;
|
2390
|
+
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);
|
2395
|
+
const visualRect = getBoundingClientRect(visual);
|
2396
|
+
const X = type === 'touchstart' ? touches[0].pageX : pageX;
|
2397
|
+
const Y = type === 'touchstart' ? touches[0].pageY : pageY;
|
2398
|
+
const offsetX = X - window.pageXOffset - visualRect.left;
|
2399
|
+
const offsetY = Y - window.pageYOffset - visualRect.top;
|
2400
|
+
|
2401
|
+
if (target === v1 || target === c1) {
|
2402
|
+
self.dragElement = visual;
|
2403
|
+
self.changeControl1({ offsetX, offsetY });
|
2404
|
+
} else if (target === v2 || target === c2) {
|
2405
|
+
self.dragElement = visual;
|
2406
|
+
self.changeControl2({ offsetY });
|
2407
|
+
} else if (format !== 'hex' && (target === v3 || target === c3)) {
|
2408
|
+
self.dragElement = visual;
|
2409
|
+
self.changeAlpha({ offsetY });
|
2410
|
+
}
|
2411
|
+
e.preventDefault();
|
2412
|
+
}
|
2413
|
+
|
2414
|
+
/**
|
2415
|
+
* Handles the `ColorPicker` touchend / mouseup events listeners.
|
2416
|
+
* @param {TouchEvent} e
|
2417
|
+
* @this {ColorPicker}
|
2418
|
+
*/
|
2419
|
+
pointerUp({ target }) {
|
2420
|
+
const self = this;
|
2421
|
+
const selection = document.getSelection();
|
2422
|
+
// @ts-ignore
|
2423
|
+
if (!self.dragElement && !selection.toString().length
|
2424
|
+
// @ts-ignore
|
2425
|
+
&& !self.parent.contains(target)) {
|
2426
|
+
self.hide();
|
2427
|
+
}
|
2428
|
+
|
2429
|
+
self.dragElement = null;
|
2430
|
+
}
|
2431
|
+
|
2432
|
+
/**
|
2433
|
+
* Handles the `ColorPicker` touchmove / mousemove events listeners.
|
2434
|
+
* @param {TouchEvent} e
|
2435
|
+
*/
|
2436
|
+
pointerMove(e) {
|
2437
|
+
const self = this;
|
2438
|
+
const { dragElement, visuals, format } = self;
|
2439
|
+
const [v1, v2, v3] = visuals;
|
2440
|
+
const {
|
2441
|
+
// @ts-ignore
|
2442
|
+
type, touches, pageX, pageY,
|
2443
|
+
} = e;
|
2444
|
+
|
2445
|
+
if (!dragElement) return;
|
2446
|
+
|
2447
|
+
const controlRect = getBoundingClientRect(dragElement);
|
2448
|
+
const X = type === 'touchmove' ? touches[0].pageX : pageX;
|
2449
|
+
const Y = type === 'touchmove' ? touches[0].pageY : pageY;
|
2450
|
+
const offsetX = X - window.pageXOffset - controlRect.left;
|
2451
|
+
const offsetY = Y - window.pageYOffset - controlRect.top;
|
2452
|
+
|
2453
|
+
if (dragElement === v1) {
|
2454
|
+
self.changeControl1({ offsetX, offsetY });
|
2455
|
+
}
|
2456
|
+
|
2457
|
+
if (dragElement === v2) {
|
2458
|
+
self.changeControl2({ offsetY });
|
2459
|
+
}
|
2460
|
+
|
2461
|
+
if (dragElement === v3 && format !== 'hex') {
|
2462
|
+
self.changeAlpha({ offsetY });
|
2463
|
+
}
|
2464
|
+
}
|
2465
|
+
|
2466
|
+
/**
|
2467
|
+
* Handles the `ColorPicker` events listeners associated with the color knobs.
|
2468
|
+
* @param {KeyboardEvent} e
|
2469
|
+
*/
|
2470
|
+
handleKnobs(e) {
|
2471
|
+
const { target, code } = e;
|
2472
|
+
const self = this;
|
2473
|
+
|
2474
|
+
// only react to arrow buttons
|
2475
|
+
if (![keyArrowUp, keyArrowDown, keyArrowLeft, keyArrowRight].includes(code)) return;
|
2476
|
+
e.preventDefault();
|
2477
|
+
|
2478
|
+
const { activeElement } = document;
|
2479
|
+
const { controlKnobs } = self;
|
2480
|
+
const currentKnob = controlKnobs.find((x) => x === activeElement);
|
2481
|
+
const [c1, c2, c3] = controlKnobs;
|
2482
|
+
|
2483
|
+
if (currentKnob) {
|
2484
|
+
let offsetX = 0;
|
2485
|
+
let offsetY = 0;
|
2486
|
+
if (target === c1) {
|
2487
|
+
if ([keyArrowLeft, keyArrowRight].includes(code)) {
|
2488
|
+
self.controlPositions.c1x += code === keyArrowRight ? +1 : -1;
|
2489
|
+
} else if ([keyArrowUp, keyArrowDown].includes(code)) {
|
2490
|
+
self.controlPositions.c1y += code === keyArrowDown ? +1 : -1;
|
2491
|
+
}
|
2492
|
+
|
2493
|
+
offsetX = self.controlPositions.c1x;
|
2494
|
+
offsetY = self.controlPositions.c1y;
|
2495
|
+
self.changeControl1({ offsetX, offsetY });
|
2496
|
+
} else if (target === c2) {
|
2497
|
+
self.controlPositions.c2y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
|
2498
|
+
offsetY = self.controlPositions.c2y;
|
2499
|
+
self.changeControl2({ offsetY });
|
2500
|
+
} else if (target === c3) {
|
2501
|
+
self.controlPositions.c3y += [keyArrowDown, keyArrowRight].includes(code) ? +1 : -1;
|
2502
|
+
offsetY = self.controlPositions.c3y;
|
2503
|
+
self.changeAlpha({ offsetY });
|
2504
|
+
}
|
2505
|
+
|
2506
|
+
self.setColorAppearence();
|
2507
|
+
self.updateInputs();
|
2508
|
+
self.updateControls();
|
2509
|
+
self.updateVisuals();
|
2510
|
+
self.handleScroll(e);
|
2511
|
+
}
|
2512
|
+
}
|
2513
|
+
|
2514
|
+
/** Handles the event listeners of the color form. */
|
2515
|
+
changeHandler() {
|
2516
|
+
const self = this;
|
2517
|
+
let colorSource;
|
2518
|
+
/** @type {HTMLInputElement} */
|
2519
|
+
// @ts-ignore
|
2520
|
+
const { activeElement } = document;
|
2521
|
+
const {
|
2522
|
+
inputs, format, value: currentValue, input,
|
2523
|
+
} = self;
|
2524
|
+
const [i1, i2, i3, i4] = inputs;
|
2525
|
+
const isNonColorValue = self.includeNonColor && nonColors.includes(currentValue);
|
2526
|
+
|
2527
|
+
if (activeElement === input || (activeElement && inputs.includes(activeElement))) {
|
2528
|
+
if (activeElement === input) {
|
2529
|
+
if (isNonColorValue) {
|
2530
|
+
colorSource = 'white';
|
2531
|
+
} else {
|
2532
|
+
colorSource = currentValue;
|
2533
|
+
}
|
2534
|
+
} else if (format === 'hex') {
|
2535
|
+
colorSource = i1.value;
|
2536
|
+
} else if (format === 'hsl') {
|
2537
|
+
colorSource = `hsla(${i1.value},${i2.value}%,${i3.value}%,${i4.value})`;
|
2538
|
+
} else {
|
2539
|
+
colorSource = `rgba(${inputs.map((x) => x.value).join(',')})`;
|
2540
|
+
}
|
2541
|
+
|
2542
|
+
self.color = new Color(colorSource, { format });
|
2543
|
+
self.setControlPositions();
|
2544
|
+
self.setColorAppearence();
|
2545
|
+
self.updateInputs();
|
2546
|
+
self.updateControls();
|
2547
|
+
self.updateVisuals();
|
2548
|
+
|
2549
|
+
// set non-color keyword
|
2550
|
+
if (activeElement === input && isNonColorValue) {
|
2551
|
+
self.value = currentValue;
|
2552
|
+
}
|
2553
|
+
}
|
2554
|
+
}
|
2555
|
+
|
2556
|
+
/**
|
2557
|
+
* Updates `ColorPicker` first control:
|
2558
|
+
* * `lightness` and `saturation` for HEX/RGB;
|
2559
|
+
* * `lightness` and `hue` for HSL.
|
2560
|
+
*
|
2561
|
+
* @param {Record<string, number>} offsets
|
2562
|
+
*/
|
2563
|
+
changeControl1(offsets) {
|
2564
|
+
const self = this;
|
2565
|
+
let [offsetX, offsetY] = [0, 0];
|
2566
|
+
const { offsetX: X, offsetY: Y } = offsets;
|
2567
|
+
const {
|
2568
|
+
format, controlPositions,
|
2569
|
+
height1, height2, height3, width1,
|
2570
|
+
} = self;
|
2571
|
+
|
2572
|
+
if (X > width1) {
|
2573
|
+
offsetX = width1;
|
2574
|
+
} else if (X >= 0) {
|
2575
|
+
offsetX = X;
|
2576
|
+
}
|
2577
|
+
|
2578
|
+
if (Y > height1) {
|
2579
|
+
offsetY = height1;
|
2580
|
+
} else if (Y >= 0) {
|
2581
|
+
offsetY = Y;
|
2582
|
+
}
|
2583
|
+
|
2584
|
+
const hue = format !== 'hsl'
|
2585
|
+
? Math.round((controlPositions.c2y / height2) * 360)
|
2586
|
+
: Math.round((offsetX / width1) * 360);
|
2587
|
+
|
2588
|
+
const saturation = format !== 'hsl'
|
2589
|
+
? Math.round((offsetX / width1) * 100)
|
2590
|
+
: Math.round((1 - controlPositions.c2y / height2) * 100);
|
2591
|
+
|
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';
|
2595
|
+
|
2596
|
+
// new color
|
2597
|
+
self.color = new Color(`${tempFormat}(${hue},${saturation}%,${lightness}%,${alpha})`, { format });
|
2598
|
+
// new positions
|
2599
|
+
self.controlPositions.c1x = offsetX;
|
2600
|
+
self.controlPositions.c1y = offsetY;
|
2601
|
+
|
2602
|
+
// update color picker
|
2603
|
+
self.setColorAppearence();
|
2604
|
+
self.updateInputs();
|
2605
|
+
self.updateControls();
|
2606
|
+
self.updateVisuals();
|
2607
|
+
}
|
2608
|
+
|
2609
|
+
/**
|
2610
|
+
* Updates `ColorPicker` second control:
|
2611
|
+
* * `hue` for HEX/RGB;
|
2612
|
+
* * `saturation` for HSL.
|
2613
|
+
*
|
2614
|
+
* @param {Record<string, number>} offset
|
2615
|
+
*/
|
2616
|
+
changeControl2(offset) {
|
2617
|
+
const self = this;
|
2618
|
+
const { offsetY: Y } = offset;
|
2619
|
+
const {
|
2620
|
+
format, width1, height1, height2, height3, controlPositions,
|
2621
|
+
} = self;
|
2622
|
+
let offsetY = 0;
|
2623
|
+
|
2624
|
+
if (Y > height2) {
|
2625
|
+
offsetY = height2;
|
2626
|
+
} else if (Y >= 0) {
|
2627
|
+
offsetY = Y;
|
2628
|
+
}
|
2629
|
+
|
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';
|
2635
|
+
|
2636
|
+
// new color
|
2637
|
+
self.color = new Color(`${colorFormat}(${hue},${saturation}%,${lightness}%,${alpha})`, { format });
|
2638
|
+
// new position
|
2639
|
+
self.controlPositions.c2y = offsetY;
|
2640
|
+
// update color picker
|
2641
|
+
self.setColorAppearence();
|
2642
|
+
self.updateInputs();
|
2643
|
+
self.updateControls();
|
2644
|
+
self.updateVisuals();
|
2645
|
+
}
|
2646
|
+
|
2647
|
+
/**
|
2648
|
+
* Updates `ColorPicker` last control,
|
2649
|
+
* the `alpha` channel for RGB/HSL.
|
2650
|
+
*
|
2651
|
+
* @param {Record<string, number>} offset
|
2652
|
+
*/
|
2653
|
+
changeAlpha(offset) {
|
2654
|
+
const self = this;
|
2655
|
+
const { height3 } = self;
|
2656
|
+
const { offsetY: Y } = offset;
|
2657
|
+
let offsetY = 0;
|
2658
|
+
|
2659
|
+
if (Y > height3) {
|
2660
|
+
offsetY = height3;
|
2661
|
+
} else if (Y >= 0) {
|
2662
|
+
offsetY = Y;
|
2663
|
+
}
|
2664
|
+
|
2665
|
+
// update color alpha
|
2666
|
+
const alpha = Math.round((1 - offsetY / height3) * 100);
|
2667
|
+
self.color.setAlpha(alpha / 100);
|
2668
|
+
// update position
|
2669
|
+
self.controlPositions.c3y = offsetY;
|
2670
|
+
// update color picker
|
2671
|
+
self.updateInputs();
|
2672
|
+
self.updateControls();
|
2673
|
+
// alpha?
|
2674
|
+
self.updateVisuals();
|
2675
|
+
}
|
2676
|
+
|
2677
|
+
/** Update opened dropdown position on scroll. */
|
2678
|
+
updateDropdownPosition() {
|
2679
|
+
const self = this;
|
2680
|
+
const { input, colorPicker, colorMenu } = self;
|
2681
|
+
const elRect = getBoundingClientRect(input);
|
2682
|
+
const { offsetHeight: elHeight } = input;
|
2683
|
+
const windowHeight = document.documentElement.clientHeight;
|
2684
|
+
const isPicker = classToggle(colorPicker, true);
|
2685
|
+
const dropdown = isPicker ? colorPicker : colorMenu;
|
2686
|
+
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');
|
2699
|
+
}
|
2700
|
+
}
|
2701
|
+
|
2702
|
+
/** Update control knobs' positions. */
|
2703
|
+
setControlPositions() {
|
2704
|
+
const self = this;
|
2705
|
+
const {
|
2706
|
+
hsv, hsl, format, height1, height2, height3, width1,
|
2707
|
+
} = self;
|
2708
|
+
const hue = hsl.h;
|
2709
|
+
const saturation = format !== 'hsl' ? hsv.s : hsl.s;
|
2710
|
+
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
|
+
|
2717
|
+
if (format !== 'hex') {
|
2718
|
+
self.controlPositions.c3y = (1 - alpha) * height3;
|
2719
|
+
}
|
2720
|
+
}
|
2721
|
+
|
2722
|
+
/** Update the visual appearance label. */
|
2723
|
+
setColorAppearence() {
|
2724
|
+
const self = this;
|
2725
|
+
const {
|
2726
|
+
componentLabels, colorLabels, hsl, hsv, hex, format, knobLabels,
|
2727
|
+
} = self;
|
2728
|
+
const {
|
2729
|
+
lightnessLabel, saturationLabel, hueLabel, alphaLabel, appearanceLabel, hexLabel,
|
2730
|
+
} = componentLabels;
|
2731
|
+
let { requiredLabel } = componentLabels;
|
2732
|
+
const [knob1Lbl, knob2Lbl, knob3Lbl] = knobLabels;
|
2733
|
+
const hue = Math.round(hsl.h);
|
2734
|
+
const alpha = hsv.a;
|
2735
|
+
const saturationSource = format === 'hsl' ? hsl.s : hsv.s;
|
2736
|
+
const saturation = Math.round(saturationSource * 100);
|
2737
|
+
const lightness = Math.round(hsl.l * 100);
|
2738
|
+
const hsvl = hsv.v * 100;
|
2739
|
+
let colorName;
|
2740
|
+
|
2741
|
+
// determine color appearance
|
2742
|
+
if (lightness === 100 && saturation === 0) {
|
2743
|
+
colorName = colorLabels.white;
|
2744
|
+
} else if (lightness === 0) {
|
2745
|
+
colorName = colorLabels.black;
|
2746
|
+
} else if (saturation === 0) {
|
2747
|
+
colorName = colorLabels.grey;
|
2748
|
+
} else if (hue < 15 || hue >= 345) {
|
2749
|
+
colorName = colorLabels.red;
|
2750
|
+
} else if (hue >= 15 && hue < 45) {
|
2751
|
+
colorName = hsvl > 80 && saturation > 80 ? colorLabels.orange : colorLabels.brown;
|
2752
|
+
} else if (hue >= 45 && hue < 75) {
|
2753
|
+
const isGold = hue > 46 && hue < 54 && hsvl < 80 && saturation > 90;
|
2754
|
+
const isOlive = hue >= 54 && hue < 75 && hsvl < 80;
|
2755
|
+
colorName = isGold ? colorLabels.gold : colorLabels.yellow;
|
2756
|
+
colorName = isOlive ? colorLabels.olive : colorName;
|
2757
|
+
} else if (hue >= 75 && hue < 155) {
|
2758
|
+
colorName = hsvl < 68 ? colorLabels.green : colorLabels.lime;
|
2759
|
+
} else if (hue >= 155 && hue < 175) {
|
2760
|
+
colorName = colorLabels.teal;
|
2761
|
+
} else if (hue >= 175 && hue < 195) {
|
2762
|
+
colorName = colorLabels.cyan;
|
2763
|
+
} else if (hue >= 195 && hue < 255) {
|
2764
|
+
colorName = colorLabels.blue;
|
2765
|
+
} else if (hue >= 255 && hue < 270) {
|
2766
|
+
colorName = colorLabels.violet;
|
2767
|
+
} else if (hue >= 270 && hue < 295) {
|
2768
|
+
colorName = colorLabels.magenta;
|
2769
|
+
} else if (hue >= 295 && hue < 345) {
|
2770
|
+
colorName = colorLabels.pink;
|
2771
|
+
}
|
2772
|
+
|
2773
|
+
if (format === 'hsl') {
|
2774
|
+
knob1Lbl.innerText = `${hueLabel}: ${hue}°. ${lightnessLabel}: ${lightness}%`;
|
2775
|
+
knob2Lbl.innerText = `${saturationLabel}: ${saturation}%`;
|
2776
|
+
} else {
|
2777
|
+
knob1Lbl.innerText = `${lightnessLabel}: ${lightness}%. ${saturationLabel}: ${saturation}%`;
|
2778
|
+
knob2Lbl.innerText = `${hueLabel}: ${hue}°`;
|
2779
|
+
}
|
2780
|
+
|
2781
|
+
if (format !== 'hex') {
|
2782
|
+
const alphaValue = Math.round(alpha * 100);
|
2783
|
+
knob3Lbl.innerText = `${alphaLabel}: ${alphaValue}%`;
|
2784
|
+
}
|
2785
|
+
|
2786
|
+
// update color labels
|
2787
|
+
self.appearance.innerText = `${appearanceLabel}: ${colorName}.`;
|
2788
|
+
const colorLabel = format === 'hex'
|
2789
|
+
? `${hexLabel} ${hex.split('').join(' ')}.`
|
2790
|
+
: self.value.toUpperCase();
|
2791
|
+
|
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}`;
|
2799
|
+
}
|
2800
|
+
}
|
2801
|
+
|
2802
|
+
/** Updates the control knobs positions. */
|
2803
|
+
updateControls() {
|
2804
|
+
const { format, controlKnobs, controlPositions } = this;
|
2805
|
+
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
|
+
}
|
2812
|
+
}
|
2813
|
+
|
2814
|
+
/**
|
2815
|
+
* Update all color form inputs.
|
2816
|
+
* @param {boolean=} isPrevented when `true`, the component original event is prevented
|
2817
|
+
*/
|
2818
|
+
updateInputs(isPrevented) {
|
2819
|
+
const self = this;
|
2820
|
+
const {
|
2821
|
+
value: oldColor, rgb, hsl, hsv, format, parent, input, inputs,
|
2822
|
+
} = self;
|
2823
|
+
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);
|
2830
|
+
let newColor;
|
2831
|
+
|
2832
|
+
if (format === 'hex') {
|
2833
|
+
newColor = self.color.toHexString();
|
2834
|
+
i1.value = self.hex;
|
2835
|
+
} else if (format === 'hsl') {
|
2836
|
+
newColor = self.color.toHslString();
|
2837
|
+
i1.value = `${hue}`;
|
2838
|
+
i2.value = `${saturation}`;
|
2839
|
+
i3.value = `${lightness}`;
|
2840
|
+
i4.value = `${alpha}`;
|
2841
|
+
} else if (format === 'rgb') {
|
2842
|
+
newColor = self.color.toRgbString();
|
2843
|
+
i1.value = `${rgb.r}`;
|
2844
|
+
i2.value = `${rgb.g}`;
|
2845
|
+
i3.value = `${rgb.b}`;
|
2846
|
+
i4.value = `${alpha}`;
|
2847
|
+
}
|
2848
|
+
|
2849
|
+
// update the color value
|
2850
|
+
self.value = `${newColor}`;
|
2851
|
+
|
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
|
+
// don't trigger the custom event unless it's really changed
|
2867
|
+
if (!isPrevented && newColor !== oldColor) {
|
2868
|
+
firePickerChange(self);
|
2869
|
+
}
|
2870
|
+
}
|
2871
|
+
|
2872
|
+
/**
|
2873
|
+
* Handles the `Space` and `Enter` keys inputs.
|
2874
|
+
* @param {KeyboardEvent} e
|
2875
|
+
* @this {ColorPicker}
|
2876
|
+
*/
|
2877
|
+
keyHandler(e) {
|
2878
|
+
const self = this;
|
2879
|
+
const { menuToggle } = self;
|
2880
|
+
const { activeElement } = document;
|
2881
|
+
const { code } = e;
|
2882
|
+
|
2883
|
+
if ([keyEnter, keySpace].includes(code)) {
|
2884
|
+
if ((menuToggle && activeElement === menuToggle) || !activeElement) {
|
2885
|
+
e.preventDefault();
|
2886
|
+
if (!activeElement) {
|
2887
|
+
self.togglePicker(e);
|
2888
|
+
} else {
|
2889
|
+
self.toggleMenu();
|
2890
|
+
}
|
2891
|
+
}
|
2892
|
+
}
|
2893
|
+
}
|
2894
|
+
|
2895
|
+
/**
|
2896
|
+
* Toggle the `ColorPicker` dropdown visibility.
|
2897
|
+
* @param {Event} e
|
2898
|
+
* @this {ColorPicker}
|
2899
|
+
*/
|
2900
|
+
togglePicker(e) {
|
2901
|
+
e.preventDefault();
|
2902
|
+
const self = this;
|
2903
|
+
const pickerIsOpen = classToggle(self.colorPicker, true);
|
2904
|
+
|
2905
|
+
if (self.isOpen && pickerIsOpen) {
|
2906
|
+
self.hide(true);
|
2907
|
+
} else {
|
2908
|
+
self.showPicker();
|
2909
|
+
}
|
2910
|
+
}
|
2911
|
+
|
2912
|
+
/** Shows the `ColorPicker` dropdown. */
|
2913
|
+
showPicker() {
|
2914
|
+
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');
|
2920
|
+
}
|
2921
|
+
|
2922
|
+
/** Toggles the visibility of the `ColorPicker` presets menu. */
|
2923
|
+
toggleMenu() {
|
2924
|
+
const self = this;
|
2925
|
+
const menuIsOpen = classToggle(self.colorMenu, true);
|
2926
|
+
|
2927
|
+
if (self.isOpen && menuIsOpen) {
|
2928
|
+
self.hide(true);
|
2929
|
+
} else {
|
2930
|
+
showMenu(self);
|
2931
|
+
}
|
2932
|
+
}
|
2933
|
+
|
2934
|
+
/** Show the dropdown. */
|
2935
|
+
show() {
|
2936
|
+
const self = this;
|
2937
|
+
if (!self.isOpen) {
|
2938
|
+
addClass(self.parent, 'open');
|
2939
|
+
toggleEventsOnShown(self, true);
|
2940
|
+
self.updateDropdownPosition();
|
2941
|
+
self.isOpen = true;
|
2942
|
+
}
|
2943
|
+
}
|
2944
|
+
|
2945
|
+
/**
|
2946
|
+
* Hides the currently opened dropdown.
|
2947
|
+
* @param {boolean=} focusPrevented
|
2948
|
+
*/
|
2949
|
+
hide(focusPrevented) {
|
2950
|
+
const self = this;
|
2951
|
+
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');
|
2963
|
+
}
|
2964
|
+
|
2965
|
+
if (!self.isValid) {
|
2966
|
+
self.value = self.color.toString();
|
2967
|
+
}
|
2968
|
+
|
2969
|
+
self.isOpen = false;
|
2970
|
+
|
2971
|
+
if (!focusPrevented) {
|
2972
|
+
pickerToggle.focus();
|
2973
|
+
}
|
2974
|
+
}
|
2975
|
+
}
|
2976
|
+
|
2977
|
+
dispose() {
|
2978
|
+
const self = this;
|
2979
|
+
const { input, parent } = self;
|
2980
|
+
self.hide(true);
|
2981
|
+
toggleEvents(self);
|
2982
|
+
[...parent.children].forEach((el) => {
|
2983
|
+
if (el !== input) el.remove();
|
2984
|
+
});
|
2985
|
+
Data.remove(input, colorPickerString);
|
2986
|
+
}
|
2987
|
+
}
|
2988
|
+
|
2989
|
+
ObjectAssign(ColorPicker, {
|
2990
|
+
Color,
|
2991
|
+
getInstance: getColorPickerInstance,
|
2992
|
+
init: initColorPicker,
|
2993
|
+
selector: colorPickerSelector,
|
2994
|
+
});
|
2995
|
+
|
2996
|
+
/**
|
2997
|
+
* `ColorPickerElement` Web Component.
|
2998
|
+
* @example
|
2999
|
+
* <color-picker>
|
3000
|
+
* <input type="text">
|
3001
|
+
* </color-picker>
|
3002
|
+
*/
|
3003
|
+
class ColorPickerElement extends HTMLElement {
|
3004
|
+
constructor() {
|
3005
|
+
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
|
+
/** @type {boolean} */
|
3012
|
+
this.isDisconnected = true;
|
3013
|
+
this.attachShadow({ mode: 'open' });
|
3014
|
+
}
|
3015
|
+
|
3016
|
+
get value() { return this.input.value; }
|
3017
|
+
|
3018
|
+
get color() { return this.colorPicker && this.colorPicker.color; }
|
3019
|
+
|
3020
|
+
connectedCallback() {
|
3021
|
+
if (this.colorPicker) {
|
3022
|
+
if (this.isDisconnected) {
|
3023
|
+
this.isDisconnected = false;
|
3024
|
+
}
|
3025
|
+
return;
|
3026
|
+
}
|
3027
|
+
|
3028
|
+
this.colorPicker = new ColorPicker(this.input);
|
3029
|
+
this.isDisconnected = false;
|
3030
|
+
|
3031
|
+
if (this.shadowRoot) {
|
3032
|
+
this.shadowRoot.append(createElement('slot'));
|
3033
|
+
}
|
3034
|
+
}
|
3035
|
+
|
3036
|
+
disconnectedCallback() {
|
3037
|
+
if (this.colorPicker) this.colorPicker.dispose();
|
3038
|
+
this.isDisconnected = true;
|
3039
|
+
}
|
3040
|
+
}
|
3041
|
+
|
3042
|
+
ObjectAssign(ColorPickerElement, {
|
3043
|
+
Color,
|
3044
|
+
ColorPicker,
|
3045
|
+
});
|
3046
|
+
|
3047
|
+
customElements.define('color-picker', ColorPickerElement);
|
3048
|
+
|
3049
|
+
return ColorPickerElement;
|
3050
|
+
|
3051
|
+
})));
|