coloris 0.0.2 → 0.0.4
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.
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2021 Momo Bassit.
|
|
3
|
+
* Licensed under the MIT License (MIT)
|
|
4
|
+
* https://github.com/mdbassit/Coloris
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
((window, document, Math, undefined) => {
|
|
8
|
+
const ctx = document.createElement('canvas').getContext('2d');
|
|
9
|
+
const currentColor = { r: 0, g: 0, b: 0, h: 0, s: 0, v: 0, a: 1 };
|
|
10
|
+
let container, picker, colorArea, colorMarker, colorPreview, colorValue, clearButton, closeButton,
|
|
11
|
+
hueSlider, hueMarker, alphaSlider, alphaMarker, currentEl, currentFormat, oldColor, keyboardNav,
|
|
12
|
+
colorAreaDims = {};
|
|
13
|
+
|
|
14
|
+
// Default settings
|
|
15
|
+
const settings = {
|
|
16
|
+
el: '[data-coloris]',
|
|
17
|
+
parent: 'body',
|
|
18
|
+
theme: 'default',
|
|
19
|
+
themeMode: 'light',
|
|
20
|
+
rtl: false,
|
|
21
|
+
wrap: true,
|
|
22
|
+
margin: 2,
|
|
23
|
+
format: 'hex',
|
|
24
|
+
formatToggle : false,
|
|
25
|
+
swatches: [],
|
|
26
|
+
swatchesOnly: false,
|
|
27
|
+
alpha: true,
|
|
28
|
+
forceAlpha: false,
|
|
29
|
+
focusInput: true,
|
|
30
|
+
selectInput: false,
|
|
31
|
+
inline: false,
|
|
32
|
+
defaultColor: '#000000',
|
|
33
|
+
clearButton: false,
|
|
34
|
+
clearLabel: 'Clear',
|
|
35
|
+
closeButton: false,
|
|
36
|
+
closeLabel: 'Close',
|
|
37
|
+
onChange: () => undefined,
|
|
38
|
+
a11y: {
|
|
39
|
+
open: 'Open color picker',
|
|
40
|
+
close: 'Close color picker',
|
|
41
|
+
clear: 'Clear the selected color',
|
|
42
|
+
marker: 'Saturation: {s}. Brightness: {v}.',
|
|
43
|
+
hueSlider: 'Hue slider',
|
|
44
|
+
alphaSlider: 'Opacity slider',
|
|
45
|
+
input: 'Color value field',
|
|
46
|
+
format: 'Color format',
|
|
47
|
+
swatch: 'Color swatch',
|
|
48
|
+
instruction: 'Saturation and brightness selector. Use up, down, left and right arrow keys to select.'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Virtual instances cache
|
|
53
|
+
const instances = {};
|
|
54
|
+
let currentInstanceId = '';
|
|
55
|
+
let defaultInstance = {};
|
|
56
|
+
let hasInstance = false;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Configure the color picker.
|
|
60
|
+
* @param {object} options Configuration options.
|
|
61
|
+
*/
|
|
62
|
+
function configure(options) {
|
|
63
|
+
if (typeof options !== 'object') {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const key in options) {
|
|
68
|
+
switch (key) {
|
|
69
|
+
case 'el':
|
|
70
|
+
bindFields(options.el);
|
|
71
|
+
if (options.wrap !== false) {
|
|
72
|
+
wrapFields(options.el);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
case 'parent':
|
|
76
|
+
container = options.parent instanceof HTMLElement ? options.parent : document.querySelector(options.parent);
|
|
77
|
+
if (container) {
|
|
78
|
+
container.appendChild(picker);
|
|
79
|
+
settings.parent = options.parent;
|
|
80
|
+
|
|
81
|
+
// document.body is special
|
|
82
|
+
if (container === document.body) {
|
|
83
|
+
container = undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
break;
|
|
87
|
+
case 'themeMode':
|
|
88
|
+
settings.themeMode = options.themeMode;
|
|
89
|
+
if (options.themeMode === 'auto' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
90
|
+
settings.themeMode = 'dark';
|
|
91
|
+
}
|
|
92
|
+
// The lack of a break statement is intentional
|
|
93
|
+
case 'theme':
|
|
94
|
+
if (options.theme) {
|
|
95
|
+
settings.theme = options.theme;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Set the theme and color scheme
|
|
99
|
+
picker.className = `clr-picker clr-${settings.theme} clr-${settings.themeMode}`;
|
|
100
|
+
|
|
101
|
+
// Update the color picker's position if inline mode is in use
|
|
102
|
+
if (settings.inline) {
|
|
103
|
+
updatePickerPosition();
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case 'rtl':
|
|
107
|
+
settings.rtl = !!options.rtl;
|
|
108
|
+
Array.from(document.getElementsByClassName('clr-field')).forEach(field => field.classList.toggle('clr-rtl', settings.rtl));
|
|
109
|
+
break;
|
|
110
|
+
case 'margin':
|
|
111
|
+
options.margin *= 1;
|
|
112
|
+
settings.margin = !isNaN(options.margin) ? options.margin : settings.margin;
|
|
113
|
+
break;
|
|
114
|
+
case 'wrap':
|
|
115
|
+
if (options.el && options.wrap) {
|
|
116
|
+
wrapFields(options.el);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
case 'formatToggle':
|
|
120
|
+
settings.formatToggle = !!options.formatToggle;
|
|
121
|
+
getEl('clr-format').style.display = settings.formatToggle ? 'block' : 'none';
|
|
122
|
+
if (settings.formatToggle) {
|
|
123
|
+
settings.format = 'auto';
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
case 'swatches':
|
|
127
|
+
if (Array.isArray(options.swatches)) {
|
|
128
|
+
const swatchesContainer = getEl('clr-swatches');
|
|
129
|
+
const swatches = document.createElement('div');
|
|
130
|
+
|
|
131
|
+
// Clear current swatches
|
|
132
|
+
swatchesContainer.textContent = '';
|
|
133
|
+
|
|
134
|
+
// Build new swatches
|
|
135
|
+
options.swatches.forEach((swatch, i) => {
|
|
136
|
+
const button = document.createElement('button');
|
|
137
|
+
|
|
138
|
+
button.setAttribute('type', `button`);
|
|
139
|
+
button.setAttribute('id', `clr-swatch-${i}`);
|
|
140
|
+
button.setAttribute('aria-labelledby', `clr-swatch-label clr-swatch-${i}`);
|
|
141
|
+
button.style.color = swatch;
|
|
142
|
+
button.textContent = swatch;
|
|
143
|
+
|
|
144
|
+
swatches.appendChild(button);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Append new swatches if any
|
|
148
|
+
if (options.swatches.length) {
|
|
149
|
+
swatchesContainer.appendChild(swatches);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
settings.swatches = options.swatches.slice();
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
case 'swatchesOnly':
|
|
156
|
+
settings.swatchesOnly = !!options.swatchesOnly;
|
|
157
|
+
picker.setAttribute('data-minimal', settings.swatchesOnly);
|
|
158
|
+
break;
|
|
159
|
+
case 'alpha':
|
|
160
|
+
settings.alpha = !!options.alpha;
|
|
161
|
+
picker.setAttribute('data-alpha', settings.alpha);
|
|
162
|
+
break;
|
|
163
|
+
case 'inline':
|
|
164
|
+
settings.inline = !!options.inline;
|
|
165
|
+
picker.setAttribute('data-inline', settings.inline);
|
|
166
|
+
|
|
167
|
+
if (settings.inline) {
|
|
168
|
+
const defaultColor = options.defaultColor || settings.defaultColor;
|
|
169
|
+
|
|
170
|
+
currentFormat = getColorFormatFromStr(defaultColor);
|
|
171
|
+
updatePickerPosition();
|
|
172
|
+
setColorFromStr(defaultColor);
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
case 'clearButton':
|
|
176
|
+
// Backward compatibility
|
|
177
|
+
if (typeof options.clearButton === 'object') {
|
|
178
|
+
if (options.clearButton.label) {
|
|
179
|
+
settings.clearLabel = options.clearButton.label;
|
|
180
|
+
clearButton.innerHTML = settings.clearLabel;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
options.clearButton = options.clearButton.show;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
settings.clearButton = !!options.clearButton;
|
|
187
|
+
clearButton.style.display = settings.clearButton ? 'block' : 'none';
|
|
188
|
+
break;
|
|
189
|
+
case 'clearLabel':
|
|
190
|
+
settings.clearLabel = options.clearLabel;
|
|
191
|
+
clearButton.innerHTML = settings.clearLabel;
|
|
192
|
+
break;
|
|
193
|
+
case 'closeButton':
|
|
194
|
+
settings.closeButton = !!options.closeButton;
|
|
195
|
+
|
|
196
|
+
if (settings.closeButton) {
|
|
197
|
+
picker.insertBefore(closeButton, colorPreview);
|
|
198
|
+
} else {
|
|
199
|
+
colorPreview.appendChild(closeButton);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
break;
|
|
203
|
+
case 'closeLabel':
|
|
204
|
+
settings.closeLabel = options.closeLabel;
|
|
205
|
+
closeButton.innerHTML = settings.closeLabel;
|
|
206
|
+
break;
|
|
207
|
+
case 'a11y':
|
|
208
|
+
const labels = options.a11y;
|
|
209
|
+
let update = false;
|
|
210
|
+
|
|
211
|
+
if (typeof labels === 'object') {
|
|
212
|
+
for (const label in labels) {
|
|
213
|
+
if (labels[label] && settings.a11y[label]) {
|
|
214
|
+
settings.a11y[label] = labels[label];
|
|
215
|
+
update = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (update) {
|
|
221
|
+
const openLabel = getEl('clr-open-label');
|
|
222
|
+
const swatchLabel = getEl('clr-swatch-label');
|
|
223
|
+
|
|
224
|
+
openLabel.innerHTML = settings.a11y.open;
|
|
225
|
+
swatchLabel.innerHTML = settings.a11y.swatch;
|
|
226
|
+
closeButton.setAttribute('aria-label', settings.a11y.close);
|
|
227
|
+
clearButton.setAttribute('aria-label', settings.a11y.clear);
|
|
228
|
+
hueSlider.setAttribute('aria-label', settings.a11y.hueSlider);
|
|
229
|
+
alphaSlider.setAttribute('aria-label', settings.a11y.alphaSlider);
|
|
230
|
+
colorValue.setAttribute('aria-label', settings.a11y.input);
|
|
231
|
+
colorArea.setAttribute('aria-label', settings.a11y.instruction);
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
default:
|
|
235
|
+
settings[key] = options[key];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Add or update a virtual instance.
|
|
242
|
+
* @param {String} selector The CSS selector of the elements to which the instance is attached.
|
|
243
|
+
* @param {Object} options Per-instance options to apply.
|
|
244
|
+
*/
|
|
245
|
+
function setVirtualInstance(selector, options) {
|
|
246
|
+
if (typeof selector === 'string' && typeof options === 'object') {
|
|
247
|
+
instances[selector] = options;
|
|
248
|
+
hasInstance = true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Remove a virtual instance.
|
|
254
|
+
* @param {String} selector The CSS selector of the elements to which the instance is attached.
|
|
255
|
+
*/
|
|
256
|
+
function removeVirtualInstance(selector) {
|
|
257
|
+
delete instances[selector];
|
|
258
|
+
|
|
259
|
+
if (Object.keys(instances).length === 0) {
|
|
260
|
+
hasInstance = false;
|
|
261
|
+
|
|
262
|
+
if (selector === currentInstanceId) {
|
|
263
|
+
resetVirtualInstance();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Attach a virtual instance to an element if it matches a selector.
|
|
270
|
+
* @param {Object} element Target element that will receive a virtual instance if applicable.
|
|
271
|
+
*/
|
|
272
|
+
function attachVirtualInstance(element) {
|
|
273
|
+
if (hasInstance) {
|
|
274
|
+
// These options can only be set globally, not per instance
|
|
275
|
+
const unsupportedOptions = ['el', 'wrap', 'rtl', 'inline', 'defaultColor', 'a11y'];
|
|
276
|
+
|
|
277
|
+
for (let selector in instances) {
|
|
278
|
+
const options = instances[selector];
|
|
279
|
+
|
|
280
|
+
// If the element matches an instance's CSS selector
|
|
281
|
+
if (element.matches(selector)) {
|
|
282
|
+
currentInstanceId = selector;
|
|
283
|
+
defaultInstance = {};
|
|
284
|
+
|
|
285
|
+
// Delete unsupported options
|
|
286
|
+
unsupportedOptions.forEach(option => delete options[option]);
|
|
287
|
+
|
|
288
|
+
// Back up the default options so we can restore them later
|
|
289
|
+
for (let option in options) {
|
|
290
|
+
defaultInstance[option] = Array.isArray(settings[option]) ? settings[option].slice() : settings[option];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Set the instance's options
|
|
294
|
+
configure(options);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Revert any per-instance options that were previously applied.
|
|
303
|
+
*/
|
|
304
|
+
function resetVirtualInstance() {
|
|
305
|
+
if (Object.keys(defaultInstance).length > 0) {
|
|
306
|
+
configure(defaultInstance);
|
|
307
|
+
currentInstanceId = '';
|
|
308
|
+
defaultInstance = {};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Bind the color picker to input fields that match the selector.
|
|
314
|
+
* @param {(string|HTMLElement|HTMLElement[])} selector A CSS selector string, a DOM element or a list of DOM elements.
|
|
315
|
+
*/
|
|
316
|
+
function bindFields(selector) {
|
|
317
|
+
if (selector instanceof HTMLElement) {
|
|
318
|
+
selector = [selector];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (Array.isArray(selector)) {
|
|
322
|
+
selector.forEach(field => {
|
|
323
|
+
addListener(field, 'click', openPicker);
|
|
324
|
+
addListener(field, 'input', updateColorPreview);
|
|
325
|
+
});
|
|
326
|
+
} else {
|
|
327
|
+
addListener(document, 'click', selector, openPicker);
|
|
328
|
+
addListener(document, 'input', selector, updateColorPreview);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Open the color picker.
|
|
334
|
+
* @param {object} event The event that opens the color picker.
|
|
335
|
+
*/
|
|
336
|
+
function openPicker(event) {
|
|
337
|
+
// Skip if inline mode is in use
|
|
338
|
+
if (settings.inline) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Apply any per-instance options first
|
|
343
|
+
attachVirtualInstance(event.target);
|
|
344
|
+
|
|
345
|
+
currentEl = event.target;
|
|
346
|
+
oldColor = currentEl.value;
|
|
347
|
+
currentFormat = getColorFormatFromStr(oldColor);
|
|
348
|
+
picker.classList.add('clr-open');
|
|
349
|
+
|
|
350
|
+
updatePickerPosition();
|
|
351
|
+
setColorFromStr(oldColor);
|
|
352
|
+
|
|
353
|
+
if (settings.focusInput || settings.selectInput) {
|
|
354
|
+
colorValue.focus({ preventScroll: true });
|
|
355
|
+
colorValue.setSelectionRange(currentEl.selectionStart, currentEl.selectionEnd);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (settings.selectInput) {
|
|
359
|
+
colorValue.select();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Always focus the first element when using keyboard navigation
|
|
363
|
+
if (keyboardNav || settings.swatchesOnly) {
|
|
364
|
+
getFocusableElements().shift().focus();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Trigger an "open" event
|
|
368
|
+
currentEl.dispatchEvent(new Event('open', { bubbles: false }));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Update the color picker's position and the color gradient's offset
|
|
373
|
+
*/
|
|
374
|
+
function updatePickerPosition() {
|
|
375
|
+
const parent = container;
|
|
376
|
+
const scrollY = window.scrollY;
|
|
377
|
+
const pickerWidth = picker.offsetWidth;
|
|
378
|
+
const pickerHeight = picker.offsetHeight;
|
|
379
|
+
const reposition = { left: false, top: false };
|
|
380
|
+
let parentStyle, parentMarginTop, parentBorderTop;
|
|
381
|
+
let offset = { x: 0, y: 0 };
|
|
382
|
+
|
|
383
|
+
if (parent) {
|
|
384
|
+
parentStyle = window.getComputedStyle(parent);
|
|
385
|
+
parentMarginTop = parseFloat(parentStyle.marginTop);
|
|
386
|
+
parentBorderTop = parseFloat(parentStyle.borderTopWidth);
|
|
387
|
+
|
|
388
|
+
offset = parent.getBoundingClientRect();
|
|
389
|
+
offset.y += parentBorderTop + scrollY;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!settings.inline) {
|
|
393
|
+
const coords = currentEl.getBoundingClientRect();
|
|
394
|
+
let left = coords.x;
|
|
395
|
+
let top = scrollY + coords.y + coords.height + settings.margin;
|
|
396
|
+
|
|
397
|
+
// If the color picker is inside a custom container
|
|
398
|
+
// set the position relative to it
|
|
399
|
+
if (parent) {
|
|
400
|
+
left -= offset.x;
|
|
401
|
+
top -= offset.y;
|
|
402
|
+
|
|
403
|
+
if (left + pickerWidth > parent.clientWidth) {
|
|
404
|
+
left += coords.width - pickerWidth;
|
|
405
|
+
reposition.left = true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (top + pickerHeight > parent.clientHeight - parentMarginTop) {
|
|
409
|
+
if (pickerHeight + settings.margin <= coords.top - (offset.y - scrollY)) {
|
|
410
|
+
top -= coords.height + pickerHeight + settings.margin * 2;
|
|
411
|
+
reposition.top = true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
top += parent.scrollTop;
|
|
416
|
+
|
|
417
|
+
// Otherwise set the position relative to the whole document
|
|
418
|
+
} else {
|
|
419
|
+
if (left + pickerWidth > document.documentElement.clientWidth) {
|
|
420
|
+
left += coords.width - pickerWidth;
|
|
421
|
+
reposition.left = true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (top + pickerHeight - scrollY > document.documentElement.clientHeight) {
|
|
425
|
+
if (pickerHeight + settings.margin <= coords.top) {
|
|
426
|
+
top = scrollY + coords.y - pickerHeight - settings.margin;
|
|
427
|
+
reposition.top = true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
picker.classList.toggle('clr-left', reposition.left);
|
|
433
|
+
picker.classList.toggle('clr-top', reposition.top);
|
|
434
|
+
picker.style.left = `${left}px`;
|
|
435
|
+
picker.style.top = `${top}px`;
|
|
436
|
+
offset.x += picker.offsetLeft;
|
|
437
|
+
offset.y += picker.offsetTop;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
colorAreaDims = {
|
|
441
|
+
width: colorArea.offsetWidth,
|
|
442
|
+
height: colorArea.offsetHeight,
|
|
443
|
+
x: colorArea.offsetLeft + offset.x,
|
|
444
|
+
y: colorArea.offsetTop + offset.y
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Wrap the linked input fields in a div that adds a color preview.
|
|
450
|
+
* @param {(string|HTMLElement|HTMLElement[])} selector A CSS selector string, a DOM element or a list of DOM elements.
|
|
451
|
+
*/
|
|
452
|
+
function wrapFields(selector) {
|
|
453
|
+
if (selector instanceof HTMLElement) {
|
|
454
|
+
wrapColorField(selector);
|
|
455
|
+
} else if (Array.isArray(selector)) {
|
|
456
|
+
selector.forEach(wrapColorField);
|
|
457
|
+
} else {
|
|
458
|
+
document.querySelectorAll(selector).forEach(wrapColorField);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Wrap an input field in a div that adds a color preview.
|
|
464
|
+
* @param {object} field The input field.
|
|
465
|
+
*/
|
|
466
|
+
function wrapColorField(field) {
|
|
467
|
+
const parentNode = field.parentNode;
|
|
468
|
+
|
|
469
|
+
if (!parentNode.classList.contains('clr-field')) {
|
|
470
|
+
const wrapper = document.createElement('div');
|
|
471
|
+
let classes = 'clr-field';
|
|
472
|
+
|
|
473
|
+
if (settings.rtl || field.classList.contains('clr-rtl')) {
|
|
474
|
+
classes += ' clr-rtl';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
wrapper.innerHTML = '<button type="button" aria-labelledby="clr-open-label"></button>';
|
|
478
|
+
parentNode.insertBefore(wrapper, field);
|
|
479
|
+
wrapper.className = classes;
|
|
480
|
+
wrapper.style.color = field.value;
|
|
481
|
+
wrapper.appendChild(field);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Update the color preview of an input field
|
|
487
|
+
* @param {object} event The "input" event that triggers the color change.
|
|
488
|
+
*/
|
|
489
|
+
function updateColorPreview(event) {
|
|
490
|
+
const parent = event.target.parentNode;
|
|
491
|
+
|
|
492
|
+
// Only update the preview if the field has been previously wrapped
|
|
493
|
+
if (parent.classList.contains('clr-field')) {
|
|
494
|
+
parent.style.color = event.target.value;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Close the color picker.
|
|
500
|
+
* @param {boolean} [revert] If true, revert the color to the original value.
|
|
501
|
+
*/
|
|
502
|
+
function closePicker(revert) {
|
|
503
|
+
if (currentEl && !settings.inline) {
|
|
504
|
+
const prevEl = currentEl;
|
|
505
|
+
|
|
506
|
+
// Revert the color to the original value if needed
|
|
507
|
+
if (revert) {
|
|
508
|
+
// This will prevent the "change" event on the colorValue input to execute its handler
|
|
509
|
+
currentEl = undefined;
|
|
510
|
+
|
|
511
|
+
if (oldColor !== prevEl.value) {
|
|
512
|
+
prevEl.value = oldColor;
|
|
513
|
+
|
|
514
|
+
// Trigger an "input" event to force update the thumbnail next to the input field
|
|
515
|
+
prevEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Trigger a "change" event if needed
|
|
520
|
+
setTimeout(() => { // Add this to the end of the event loop
|
|
521
|
+
if (oldColor !== prevEl.value) {
|
|
522
|
+
prevEl.dispatchEvent(new Event('change', { bubbles: true }));
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Hide the picker dialog
|
|
527
|
+
picker.classList.remove('clr-open');
|
|
528
|
+
|
|
529
|
+
// Reset any previously set per-instance options
|
|
530
|
+
if (hasInstance) {
|
|
531
|
+
resetVirtualInstance();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Trigger a "close" event
|
|
535
|
+
prevEl.dispatchEvent(new Event('close', { bubbles: false }));
|
|
536
|
+
|
|
537
|
+
if (settings.focusInput) {
|
|
538
|
+
prevEl.focus({ preventScroll: true });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// This essentially marks the picker as closed
|
|
542
|
+
currentEl = undefined;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Set the active color from a string.
|
|
548
|
+
* @param {string} str String representing a color.
|
|
549
|
+
*/
|
|
550
|
+
function setColorFromStr(str) {
|
|
551
|
+
const rgba = strToRGBA(str);
|
|
552
|
+
const hsva = RGBAtoHSVA(rgba);
|
|
553
|
+
|
|
554
|
+
updateMarkerA11yLabel(hsva.s, hsva.v);
|
|
555
|
+
updateColor(rgba, hsva);
|
|
556
|
+
|
|
557
|
+
// Update the UI
|
|
558
|
+
hueSlider.value = hsva.h;
|
|
559
|
+
picker.style.color = `hsl(${hsva.h}, 100%, 50%)`;
|
|
560
|
+
hueMarker.style.left = `${hsva.h / 360 * 100}%`;
|
|
561
|
+
|
|
562
|
+
colorMarker.style.left = `${colorAreaDims.width * hsva.s / 100}px`;
|
|
563
|
+
colorMarker.style.top = `${colorAreaDims.height - (colorAreaDims.height * hsva.v / 100)}px`;
|
|
564
|
+
|
|
565
|
+
alphaSlider.value = hsva.a * 100;
|
|
566
|
+
alphaMarker.style.left = `${hsva.a * 100}%`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Guess the color format from a string.
|
|
571
|
+
* @param {string} str String representing a color.
|
|
572
|
+
* @return {string} The color format.
|
|
573
|
+
*/
|
|
574
|
+
function getColorFormatFromStr(str) {
|
|
575
|
+
const format = str.substring(0, 3).toLowerCase();
|
|
576
|
+
|
|
577
|
+
if (format === 'rgb' || format === 'hsl' ) {
|
|
578
|
+
return format;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return 'hex';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Copy the active color to the linked input field.
|
|
586
|
+
* @param {number} [color] Color value to override the active color.
|
|
587
|
+
*/
|
|
588
|
+
function pickColor(color) {
|
|
589
|
+
color = color !== undefined ? color : colorValue.value;
|
|
590
|
+
|
|
591
|
+
if (currentEl) {
|
|
592
|
+
currentEl.value = color;
|
|
593
|
+
currentEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (settings.onChange) {
|
|
597
|
+
settings.onChange.call(window, color, currentEl);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
document.dispatchEvent(new CustomEvent('coloris:pick', { detail: { color, currentEl } }));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Set the active color based on a specific point in the color gradient.
|
|
605
|
+
* @param {number} x Left position.
|
|
606
|
+
* @param {number} y Top position.
|
|
607
|
+
*/
|
|
608
|
+
function setColorAtPosition(x, y) {
|
|
609
|
+
const hsva = {
|
|
610
|
+
h: hueSlider.value * 1,
|
|
611
|
+
s: x / colorAreaDims.width * 100,
|
|
612
|
+
v: 100 - (y / colorAreaDims.height * 100),
|
|
613
|
+
a: alphaSlider.value / 100
|
|
614
|
+
};
|
|
615
|
+
const rgba = HSVAtoRGBA(hsva);
|
|
616
|
+
|
|
617
|
+
updateMarkerA11yLabel(hsva.s, hsva.v);
|
|
618
|
+
updateColor(rgba, hsva);
|
|
619
|
+
pickColor();
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Update the color marker's accessibility label.
|
|
624
|
+
* @param {number} saturation
|
|
625
|
+
* @param {number} value
|
|
626
|
+
*/
|
|
627
|
+
function updateMarkerA11yLabel(saturation, value) {
|
|
628
|
+
let label = settings.a11y.marker;
|
|
629
|
+
|
|
630
|
+
saturation = saturation.toFixed(1) * 1;
|
|
631
|
+
value = value.toFixed(1) * 1;
|
|
632
|
+
label = label.replace('{s}', saturation);
|
|
633
|
+
label = label.replace('{v}', value);
|
|
634
|
+
colorMarker.setAttribute('aria-label', label);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
//
|
|
638
|
+
/**
|
|
639
|
+
* Get the pageX and pageY positions of the pointer.
|
|
640
|
+
* @param {object} event The MouseEvent or TouchEvent object.
|
|
641
|
+
* @return {object} The pageX and pageY positions.
|
|
642
|
+
*/
|
|
643
|
+
function getPointerPosition(event) {
|
|
644
|
+
return {
|
|
645
|
+
pageX: event.changedTouches ? event.changedTouches[0].pageX : event.pageX,
|
|
646
|
+
pageY: event.changedTouches ? event.changedTouches[0].pageY : event.pageY
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Move the color marker when dragged.
|
|
652
|
+
* @param {object} event The MouseEvent object.
|
|
653
|
+
*/
|
|
654
|
+
function moveMarker(event) {
|
|
655
|
+
const pointer = getPointerPosition(event);
|
|
656
|
+
let x = pointer.pageX - colorAreaDims.x;
|
|
657
|
+
let y = pointer.pageY - colorAreaDims.y;
|
|
658
|
+
|
|
659
|
+
if (container) {
|
|
660
|
+
y += container.scrollTop;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
setMarkerPosition(x, y);
|
|
664
|
+
|
|
665
|
+
// Prevent scrolling while dragging the marker
|
|
666
|
+
event.preventDefault();
|
|
667
|
+
event.stopPropagation();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Move the color marker when the arrow keys are pressed.
|
|
672
|
+
* @param {number} offsetX The horizontal amount to move.
|
|
673
|
+
* @param {number} offsetY The vertical amount to move.
|
|
674
|
+
*/
|
|
675
|
+
function moveMarkerOnKeydown(offsetX, offsetY) {
|
|
676
|
+
let x = colorMarker.style.left.replace('px', '') * 1 + offsetX;
|
|
677
|
+
let y = colorMarker.style.top.replace('px', '') * 1 + offsetY;
|
|
678
|
+
|
|
679
|
+
setMarkerPosition(x, y);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Set the color marker's position.
|
|
684
|
+
* @param {number} x Left position.
|
|
685
|
+
* @param {number} y Top position.
|
|
686
|
+
*/
|
|
687
|
+
function setMarkerPosition(x, y) {
|
|
688
|
+
// Make sure the marker doesn't go out of bounds
|
|
689
|
+
x = (x < 0) ? 0 : (x > colorAreaDims.width) ? colorAreaDims.width : x;
|
|
690
|
+
y = (y < 0) ? 0 : (y > colorAreaDims.height) ? colorAreaDims.height : y;
|
|
691
|
+
|
|
692
|
+
// Set the position
|
|
693
|
+
colorMarker.style.left = `${x}px`;
|
|
694
|
+
colorMarker.style.top = `${y}px`;
|
|
695
|
+
|
|
696
|
+
// Update the color
|
|
697
|
+
setColorAtPosition(x, y);
|
|
698
|
+
|
|
699
|
+
// Make sure the marker is focused
|
|
700
|
+
colorMarker.focus();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Update the color picker's input field and preview thumb.
|
|
705
|
+
* @param {Object} rgba Red, green, blue and alpha values.
|
|
706
|
+
* @param {Object} [hsva] Hue, saturation, value and alpha values.
|
|
707
|
+
*/
|
|
708
|
+
function updateColor(rgba = {}, hsva = {}) {
|
|
709
|
+
let format = settings.format;
|
|
710
|
+
|
|
711
|
+
for (const key in rgba) {
|
|
712
|
+
currentColor[key] = rgba[key];
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
for (const key in hsva) {
|
|
716
|
+
currentColor[key] = hsva[key];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const hex = RGBAToHex(currentColor);
|
|
720
|
+
const opaqueHex = hex.substring(0, 7);
|
|
721
|
+
|
|
722
|
+
colorMarker.style.color = opaqueHex;
|
|
723
|
+
alphaMarker.parentNode.style.color = opaqueHex;
|
|
724
|
+
alphaMarker.style.color = hex;
|
|
725
|
+
colorPreview.style.color = hex;
|
|
726
|
+
|
|
727
|
+
// Force repaint the color and alpha gradients as a workaround for a Google Chrome bug
|
|
728
|
+
colorArea.style.display = 'none';
|
|
729
|
+
colorArea.offsetHeight;
|
|
730
|
+
colorArea.style.display = '';
|
|
731
|
+
alphaMarker.nextElementSibling.style.display = 'none';
|
|
732
|
+
alphaMarker.nextElementSibling.offsetHeight;
|
|
733
|
+
alphaMarker.nextElementSibling.style.display = '';
|
|
734
|
+
|
|
735
|
+
if (format === 'mixed') {
|
|
736
|
+
format = currentColor.a === 1 ? 'hex' : 'rgb';
|
|
737
|
+
} else if (format === 'auto') {
|
|
738
|
+
format = currentFormat;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
switch (format) {
|
|
742
|
+
case 'hex':
|
|
743
|
+
colorValue.value = hex;
|
|
744
|
+
break;
|
|
745
|
+
case 'rgb':
|
|
746
|
+
colorValue.value = RGBAToStr(currentColor);
|
|
747
|
+
break;
|
|
748
|
+
case 'hsl':
|
|
749
|
+
colorValue.value = HSLAToStr(HSVAtoHSLA(currentColor));
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Select the current format in the format switcher
|
|
754
|
+
document.querySelector(`.clr-format [value="${format}"]`).checked = true;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Set the hue when its slider is moved.
|
|
759
|
+
*/
|
|
760
|
+
function setHue() {
|
|
761
|
+
const hue = hueSlider.value * 1;
|
|
762
|
+
const x = colorMarker.style.left.replace('px', '') * 1;
|
|
763
|
+
const y = colorMarker.style.top.replace('px', '') * 1;
|
|
764
|
+
|
|
765
|
+
picker.style.color = `hsl(${hue}, 100%, 50%)`;
|
|
766
|
+
hueMarker.style.left = `${hue / 360 * 100}%`;
|
|
767
|
+
|
|
768
|
+
setColorAtPosition(x, y);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Set the alpha when its slider is moved.
|
|
773
|
+
*/
|
|
774
|
+
function setAlpha() {
|
|
775
|
+
const alpha = alphaSlider.value / 100;
|
|
776
|
+
|
|
777
|
+
alphaMarker.style.left = `${alpha * 100}%`;
|
|
778
|
+
updateColor({ a: alpha });
|
|
779
|
+
pickColor();
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Convert HSVA to RGBA.
|
|
784
|
+
* @param {object} hsva Hue, saturation, value and alpha values.
|
|
785
|
+
* @return {object} Red, green, blue and alpha values.
|
|
786
|
+
*/
|
|
787
|
+
function HSVAtoRGBA(hsva) {
|
|
788
|
+
const saturation = hsva.s / 100;
|
|
789
|
+
const value = hsva.v / 100;
|
|
790
|
+
let chroma = saturation * value;
|
|
791
|
+
let hueBy60 = hsva.h / 60;
|
|
792
|
+
let x = chroma * (1 - Math.abs(hueBy60 % 2 - 1));
|
|
793
|
+
let m = value - chroma;
|
|
794
|
+
|
|
795
|
+
chroma = (chroma + m);
|
|
796
|
+
x = (x + m);
|
|
797
|
+
|
|
798
|
+
const index = Math.floor(hueBy60) % 6;
|
|
799
|
+
const red = [chroma, x, m, m, x, chroma][index];
|
|
800
|
+
const green = [x, chroma, chroma, x, m, m][index];
|
|
801
|
+
const blue = [m, m, x, chroma, chroma, x][index];
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
r: Math.round(red * 255),
|
|
805
|
+
g: Math.round(green * 255),
|
|
806
|
+
b: Math.round(blue * 255),
|
|
807
|
+
a: hsva.a
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Convert HSVA to HSLA.
|
|
813
|
+
* @param {object} hsva Hue, saturation, value and alpha values.
|
|
814
|
+
* @return {object} Hue, saturation, lightness and alpha values.
|
|
815
|
+
*/
|
|
816
|
+
function HSVAtoHSLA(hsva) {
|
|
817
|
+
const value = hsva.v / 100;
|
|
818
|
+
const lightness = value * (1 - (hsva.s / 100) / 2);
|
|
819
|
+
let saturation;
|
|
820
|
+
|
|
821
|
+
if (lightness > 0 && lightness < 1) {
|
|
822
|
+
saturation = Math.round((value - lightness) / Math.min(lightness, 1 - lightness) * 100);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return {
|
|
826
|
+
h: hsva.h,
|
|
827
|
+
s: saturation || 0,
|
|
828
|
+
l: Math.round(lightness * 100),
|
|
829
|
+
a: hsva.a
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Convert RGBA to HSVA.
|
|
835
|
+
* @param {object} rgba Red, green, blue and alpha values.
|
|
836
|
+
* @return {object} Hue, saturation, value and alpha values.
|
|
837
|
+
*/
|
|
838
|
+
function RGBAtoHSVA(rgba) {
|
|
839
|
+
const red = rgba.r / 255;
|
|
840
|
+
const green = rgba.g / 255;
|
|
841
|
+
const blue = rgba.b / 255;
|
|
842
|
+
const xmax = Math.max(red, green, blue);
|
|
843
|
+
const xmin = Math.min(red, green, blue);
|
|
844
|
+
const chroma = xmax - xmin;
|
|
845
|
+
const value = xmax;
|
|
846
|
+
let hue = 0;
|
|
847
|
+
let saturation = 0;
|
|
848
|
+
|
|
849
|
+
if (chroma) {
|
|
850
|
+
if (xmax === red ) { hue = ((green - blue) / chroma); }
|
|
851
|
+
if (xmax === green ) { hue = 2 + (blue - red) / chroma; }
|
|
852
|
+
if (xmax === blue ) { hue = 4 + (red - green) / chroma; }
|
|
853
|
+
if (xmax) { saturation = chroma / xmax; }
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
hue = Math.floor(hue * 60);
|
|
857
|
+
|
|
858
|
+
return {
|
|
859
|
+
h: hue < 0 ? hue + 360 : hue,
|
|
860
|
+
s: Math.round(saturation * 100),
|
|
861
|
+
v: Math.round(value * 100),
|
|
862
|
+
a: rgba.a
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Parse a string to RGBA.
|
|
868
|
+
* @param {string} str String representing a color.
|
|
869
|
+
* @return {object} Red, green, blue and alpha values.
|
|
870
|
+
*/
|
|
871
|
+
function strToRGBA(str) {
|
|
872
|
+
const regex = /^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i;
|
|
873
|
+
let match, rgba;
|
|
874
|
+
|
|
875
|
+
// Default to black for invalid color strings
|
|
876
|
+
ctx.fillStyle = '#000';
|
|
877
|
+
|
|
878
|
+
// Use canvas to convert the string to a valid color string
|
|
879
|
+
ctx.fillStyle = str;
|
|
880
|
+
match = regex.exec(ctx.fillStyle);
|
|
881
|
+
|
|
882
|
+
if (match) {
|
|
883
|
+
rgba = {
|
|
884
|
+
r: match[3] * 1,
|
|
885
|
+
g: match[4] * 1,
|
|
886
|
+
b: match[5] * 1,
|
|
887
|
+
a: match[6] * 1
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
} else {
|
|
891
|
+
match = ctx.fillStyle.replace('#', '').match(/.{2}/g).map(h => parseInt(h, 16));
|
|
892
|
+
rgba = {
|
|
893
|
+
r: match[0],
|
|
894
|
+
g: match[1],
|
|
895
|
+
b: match[2],
|
|
896
|
+
a: 1
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return rgba;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Convert RGBA to Hex.
|
|
905
|
+
* @param {object} rgba Red, green, blue and alpha values.
|
|
906
|
+
* @return {string} Hex color string.
|
|
907
|
+
*/
|
|
908
|
+
function RGBAToHex(rgba) {
|
|
909
|
+
let R = rgba.r.toString(16);
|
|
910
|
+
let G = rgba.g.toString(16);
|
|
911
|
+
let B = rgba.b.toString(16);
|
|
912
|
+
let A = '';
|
|
913
|
+
|
|
914
|
+
if (rgba.r < 16) {
|
|
915
|
+
R = '0' + R;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (rgba.g < 16) {
|
|
919
|
+
G = '0' + G;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (rgba.b < 16) {
|
|
923
|
+
B = '0' + B;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (settings.alpha && (rgba.a < 1 || settings.forceAlpha)) {
|
|
927
|
+
const alpha = rgba.a * 255 | 0;
|
|
928
|
+
A = alpha.toString(16);
|
|
929
|
+
|
|
930
|
+
if (alpha < 16) {
|
|
931
|
+
A = '0' + A;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return '#' + R + G + B + A;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Convert RGBA values to a CSS rgb/rgba string.
|
|
940
|
+
* @param {object} rgba Red, green, blue and alpha values.
|
|
941
|
+
* @return {string} CSS color string.
|
|
942
|
+
*/
|
|
943
|
+
function RGBAToStr(rgba) {
|
|
944
|
+
if (!settings.alpha || (rgba.a === 1 && !settings.forceAlpha)) {
|
|
945
|
+
return `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})`;
|
|
946
|
+
} else {
|
|
947
|
+
return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Convert HSLA values to a CSS hsl/hsla string.
|
|
953
|
+
* @param {object} hsla Hue, saturation, lightness and alpha values.
|
|
954
|
+
* @return {string} CSS color string.
|
|
955
|
+
*/
|
|
956
|
+
function HSLAToStr(hsla) {
|
|
957
|
+
if (!settings.alpha || (hsla.a === 1 && !settings.forceAlpha)) {
|
|
958
|
+
return `hsl(${hsla.h}, ${hsla.s}%, ${hsla.l}%)`;
|
|
959
|
+
} else {
|
|
960
|
+
return `hsla(${hsla.h}, ${hsla.s}%, ${hsla.l}%, ${hsla.a})`;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Init the color picker.
|
|
966
|
+
*/
|
|
967
|
+
function init() {
|
|
968
|
+
// Render the UI
|
|
969
|
+
container = undefined;
|
|
970
|
+
picker = document.createElement('div');
|
|
971
|
+
picker.setAttribute('id', 'clr-picker');
|
|
972
|
+
picker.className = 'clr-picker';
|
|
973
|
+
picker.innerHTML =
|
|
974
|
+
`<input id="clr-color-value" name="clr-color-value" class="clr-color" type="text" value="" spellcheck="false" aria-label="${settings.a11y.input}">`+
|
|
975
|
+
`<div id="clr-color-area" class="clr-gradient" role="application" aria-label="${settings.a11y.instruction}">`+
|
|
976
|
+
'<div id="clr-color-marker" class="clr-marker" tabindex="0"></div>'+
|
|
977
|
+
'</div>'+
|
|
978
|
+
'<div class="clr-hue">'+
|
|
979
|
+
`<input id="clr-hue-slider" name="clr-hue-slider" type="range" min="0" max="360" step="1" aria-label="${settings.a11y.hueSlider}">`+
|
|
980
|
+
'<div id="clr-hue-marker"></div>'+
|
|
981
|
+
'</div>'+
|
|
982
|
+
'<div class="clr-alpha">'+
|
|
983
|
+
`<input id="clr-alpha-slider" name="clr-alpha-slider" type="range" min="0" max="100" step="1" aria-label="${settings.a11y.alphaSlider}">`+
|
|
984
|
+
'<div id="clr-alpha-marker"></div>'+
|
|
985
|
+
'<span></span>'+
|
|
986
|
+
'</div>'+
|
|
987
|
+
'<div id="clr-format" class="clr-format">'+
|
|
988
|
+
'<fieldset class="clr-segmented">'+
|
|
989
|
+
`<legend>${settings.a11y.format}</legend>`+
|
|
990
|
+
'<input id="clr-f1" type="radio" name="clr-format" value="hex">'+
|
|
991
|
+
'<label for="clr-f1">Hex</label>'+
|
|
992
|
+
'<input id="clr-f2" type="radio" name="clr-format" value="rgb">'+
|
|
993
|
+
'<label for="clr-f2">RGB</label>'+
|
|
994
|
+
'<input id="clr-f3" type="radio" name="clr-format" value="hsl">'+
|
|
995
|
+
'<label for="clr-f3">HSL</label>'+
|
|
996
|
+
'<span></span>'+
|
|
997
|
+
'</fieldset>'+
|
|
998
|
+
'</div>'+
|
|
999
|
+
'<div id="clr-swatches" class="clr-swatches"></div>'+
|
|
1000
|
+
`<button type="button" id="clr-clear" class="clr-clear" aria-label="${settings.a11y.clear}">${settings.clearLabel}</button>`+
|
|
1001
|
+
'<div id="clr-color-preview" class="clr-preview">'+
|
|
1002
|
+
`<button type="button" id="clr-close" class="clr-close" aria-label="${settings.a11y.close}">${settings.closeLabel}</button>`+
|
|
1003
|
+
'</div>'+
|
|
1004
|
+
`<span id="clr-open-label" hidden>${settings.a11y.open}</span>`+
|
|
1005
|
+
`<span id="clr-swatch-label" hidden>${settings.a11y.swatch}</span>`;
|
|
1006
|
+
|
|
1007
|
+
// Append the color picker to the DOM
|
|
1008
|
+
document.body.appendChild(picker);
|
|
1009
|
+
|
|
1010
|
+
// Reference the UI elements
|
|
1011
|
+
colorArea = getEl('clr-color-area');
|
|
1012
|
+
colorMarker = getEl('clr-color-marker');
|
|
1013
|
+
clearButton = getEl('clr-clear');
|
|
1014
|
+
closeButton = getEl('clr-close');
|
|
1015
|
+
colorPreview = getEl('clr-color-preview');
|
|
1016
|
+
colorValue = getEl('clr-color-value');
|
|
1017
|
+
hueSlider = getEl('clr-hue-slider');
|
|
1018
|
+
hueMarker = getEl('clr-hue-marker');
|
|
1019
|
+
alphaSlider = getEl('clr-alpha-slider');
|
|
1020
|
+
alphaMarker = getEl('clr-alpha-marker');
|
|
1021
|
+
|
|
1022
|
+
// Bind the picker to the default selector
|
|
1023
|
+
bindFields(settings.el);
|
|
1024
|
+
wrapFields(settings.el);
|
|
1025
|
+
|
|
1026
|
+
addListener(picker, 'mousedown', event => {
|
|
1027
|
+
picker.classList.remove('clr-keyboard-nav');
|
|
1028
|
+
event.stopPropagation();
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
addListener(colorArea, 'mousedown', event => {
|
|
1032
|
+
addListener(document, 'mousemove', moveMarker);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
addListener(colorArea, 'contextmenu', event => {
|
|
1036
|
+
event.preventDefault();
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
addListener(colorArea, 'touchstart', event => {
|
|
1040
|
+
document.addEventListener('touchmove', moveMarker, { passive: false });
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
addListener(colorMarker, 'mousedown', event => {
|
|
1044
|
+
addListener(document, 'mousemove', moveMarker);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
addListener(colorMarker, 'touchstart', event => {
|
|
1048
|
+
document.addEventListener('touchmove', moveMarker, { passive: false });
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
addListener(colorValue, 'change', event => {
|
|
1052
|
+
const value = colorValue.value;
|
|
1053
|
+
|
|
1054
|
+
if (currentEl || settings.inline) {
|
|
1055
|
+
const color = value === '' ? value : setColorFromStr(value);
|
|
1056
|
+
pickColor(color);
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
addListener(clearButton, 'click', event => {
|
|
1061
|
+
pickColor('');
|
|
1062
|
+
closePicker();
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
addListener(closeButton, 'click', event => {
|
|
1066
|
+
pickColor();
|
|
1067
|
+
closePicker();
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
addListener(getEl('clr-format'), 'click', '.clr-format input', event => {
|
|
1071
|
+
currentFormat = event.target.value;
|
|
1072
|
+
updateColor();
|
|
1073
|
+
pickColor();
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
addListener(picker, 'click', '.clr-swatches button', event => {
|
|
1077
|
+
setColorFromStr(event.target.textContent);
|
|
1078
|
+
pickColor();
|
|
1079
|
+
|
|
1080
|
+
if (settings.swatchesOnly) {
|
|
1081
|
+
closePicker();
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
addListener(document, 'mouseup', event => {
|
|
1086
|
+
document.removeEventListener('mousemove', moveMarker);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
addListener(document, 'touchend', event => {
|
|
1090
|
+
document.removeEventListener('touchmove', moveMarker);
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
addListener(document, 'mousedown', event => {
|
|
1094
|
+
keyboardNav = false;
|
|
1095
|
+
picker.classList.remove('clr-keyboard-nav');
|
|
1096
|
+
closePicker();
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
addListener(document, 'keydown', event => {
|
|
1100
|
+
const key = event.key;
|
|
1101
|
+
const target = event.target;
|
|
1102
|
+
const shiftKey = event.shiftKey;
|
|
1103
|
+
const navKeys = ['Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'];
|
|
1104
|
+
|
|
1105
|
+
if (key === 'Escape') {
|
|
1106
|
+
closePicker(true);
|
|
1107
|
+
return;
|
|
1108
|
+
|
|
1109
|
+
// Close the color picker and keep the selected color on press on Enter
|
|
1110
|
+
} else if (key === 'Enter' && target.tagName !== 'BUTTON') {
|
|
1111
|
+
closePicker();
|
|
1112
|
+
return;
|
|
1113
|
+
|
|
1114
|
+
// Display focus rings when using the keyboard
|
|
1115
|
+
} else if (navKeys.includes(key)) {
|
|
1116
|
+
keyboardNav = true;
|
|
1117
|
+
picker.classList.add('clr-keyboard-nav');
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Trap the focus within the color picker while it's open
|
|
1121
|
+
if (key === 'Tab' && target.matches('.clr-picker *')) {
|
|
1122
|
+
const focusables = getFocusableElements();
|
|
1123
|
+
const firstFocusable = focusables.shift();
|
|
1124
|
+
const lastFocusable = focusables.pop();
|
|
1125
|
+
|
|
1126
|
+
if (shiftKey && target === firstFocusable) {
|
|
1127
|
+
lastFocusable.focus();
|
|
1128
|
+
event.preventDefault();
|
|
1129
|
+
} else if (!shiftKey && target === lastFocusable) {
|
|
1130
|
+
firstFocusable.focus();
|
|
1131
|
+
event.preventDefault();
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
addListener(document, 'click', '.clr-field button', event => {
|
|
1137
|
+
// Reset any previously set per-instance options
|
|
1138
|
+
if (hasInstance) {
|
|
1139
|
+
resetVirtualInstance();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Open the color picker
|
|
1143
|
+
event.target.nextElementSibling.dispatchEvent(new Event('click', { bubbles: true }));
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
addListener(colorMarker, 'keydown', event => {
|
|
1147
|
+
const movements = {
|
|
1148
|
+
ArrowUp: [0, -1],
|
|
1149
|
+
ArrowDown: [0, 1],
|
|
1150
|
+
ArrowLeft: [-1, 0],
|
|
1151
|
+
ArrowRight: [1, 0]
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
if (Object.keys(movements).includes(event.key)) {
|
|
1155
|
+
moveMarkerOnKeydown(...movements[event.key]);
|
|
1156
|
+
event.preventDefault();
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
addListener(colorArea, 'click', moveMarker);
|
|
1161
|
+
addListener(hueSlider, 'input', setHue);
|
|
1162
|
+
addListener(alphaSlider, 'input', setAlpha);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Return a list of focusable elements within the color picker.
|
|
1167
|
+
* @return {array} The list of focusable DOM elemnts.
|
|
1168
|
+
*/
|
|
1169
|
+
function getFocusableElements() {
|
|
1170
|
+
const controls = Array.from(picker.querySelectorAll('input, button'));
|
|
1171
|
+
const focusables = controls.filter(node => !!node.offsetWidth);
|
|
1172
|
+
|
|
1173
|
+
return focusables;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Shortcut for getElementById to optimize the minified JS.
|
|
1178
|
+
* @param {string} id The element id.
|
|
1179
|
+
* @return {object} The DOM element with the provided id.
|
|
1180
|
+
*/
|
|
1181
|
+
function getEl(id) {
|
|
1182
|
+
return document.getElementById(id);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Shortcut for addEventListener to optimize the minified JS.
|
|
1187
|
+
* @param {object} context The context to which the listener is attached.
|
|
1188
|
+
* @param {string} type Event type.
|
|
1189
|
+
* @param {(string|function)} selector Event target if delegation is used, event handler if not.
|
|
1190
|
+
* @param {function} [fn] Event handler if delegation is used.
|
|
1191
|
+
*/
|
|
1192
|
+
function addListener(context, type, selector, fn) {
|
|
1193
|
+
const matches = Element.prototype.matches || Element.prototype.msMatchesSelector;
|
|
1194
|
+
|
|
1195
|
+
// Delegate event to the target of the selector
|
|
1196
|
+
if (typeof selector === 'string') {
|
|
1197
|
+
context.addEventListener(type, event => {
|
|
1198
|
+
if (matches.call(event.target, selector)) {
|
|
1199
|
+
fn.call(event.target, event);
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
// If the selector is not a string then it's a function
|
|
1204
|
+
// in which case we need a regular event listener
|
|
1205
|
+
} else {
|
|
1206
|
+
fn = selector;
|
|
1207
|
+
context.addEventListener(type, fn);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Call a function only when the DOM is ready.
|
|
1213
|
+
* @param {function} fn The function to call.
|
|
1214
|
+
* @param {array} [args] Arguments to pass to the function.
|
|
1215
|
+
*/
|
|
1216
|
+
function DOMReady(fn, args) {
|
|
1217
|
+
args = args !== undefined ? args : [];
|
|
1218
|
+
|
|
1219
|
+
if (document.readyState !== 'loading') {
|
|
1220
|
+
fn(...args);
|
|
1221
|
+
} else {
|
|
1222
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1223
|
+
fn(...args);
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Polyfill for Nodelist.forEach
|
|
1229
|
+
if (NodeList !== undefined && NodeList.prototype && !NodeList.prototype.forEach) {
|
|
1230
|
+
NodeList.prototype.forEach = Array.prototype.forEach;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// Expose the color picker to the global scope
|
|
1234
|
+
window.Coloris = (() => {
|
|
1235
|
+
const methods = {
|
|
1236
|
+
set: configure,
|
|
1237
|
+
wrap: wrapFields,
|
|
1238
|
+
close: closePicker,
|
|
1239
|
+
setInstance: setVirtualInstance,
|
|
1240
|
+
removeInstance: removeVirtualInstance,
|
|
1241
|
+
updatePosition: updatePickerPosition,
|
|
1242
|
+
ready: DOMReady
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
function Coloris(options) {
|
|
1246
|
+
DOMReady(() => {
|
|
1247
|
+
if (options) {
|
|
1248
|
+
if (typeof options === 'string') {
|
|
1249
|
+
bindFields(options);
|
|
1250
|
+
} else {
|
|
1251
|
+
configure(options);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
for (const key in methods) {
|
|
1258
|
+
Coloris[key] = (...args) => {
|
|
1259
|
+
DOMReady(methods[key], args);
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
return Coloris;
|
|
1264
|
+
})();
|
|
1265
|
+
|
|
1266
|
+
// Init the color picker when the DOM is ready
|
|
1267
|
+
document.addEventListener("turbo:load", () => {
|
|
1268
|
+
DOMReady(init);
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
})(window, document, Math);
|