@bornova/colorpicker 1.0.0

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,886 @@
1
+ /* @bornova/colorpicker v1.0.0 */
2
+ const settings = {
3
+ el: "[data-colorpicker]",
4
+ parent: "body",
5
+ theme: "default",
6
+ themeMode: "light",
7
+ rtl: false,
8
+ wrap: true,
9
+ margin: 2,
10
+ format: "hex",
11
+ formatToggle: false,
12
+ swatches: [],
13
+ swatchesOnly: false,
14
+ alpha: true,
15
+ forceAlpha: false,
16
+ focusInput: true,
17
+ selectInput: false,
18
+ inline: false,
19
+ defaultColor: "#000000",
20
+ clearButton: false,
21
+ clearLabel: "Clear",
22
+ closeButton: false,
23
+ closeLabel: "Close",
24
+ onChange: () => undefined,
25
+ a11y: {
26
+ open: "Open color picker",
27
+ close: "Close color picker",
28
+ clear: "Clear the selected color",
29
+ marker: "Saturation: {s}. Brightness: {v}.",
30
+ hueSlider: "Hue slider",
31
+ alphaSlider: "Opacity slider",
32
+ input: "Color value field",
33
+ format: "Color format",
34
+ swatch: "Color swatch",
35
+ instruction: "Saturation and brightness selector. Use up, down, left and right arrow keys to select."
36
+ }
37
+ };
38
+
39
+ const ctx = document.createElement("canvas").getContext("2d");
40
+
41
+ function HSVAtoRGBA(hsva) {
42
+ const saturation = hsva.s / 100;
43
+ const value = hsva.v / 100;
44
+ let chroma = saturation * value;
45
+ let hueBy60 = hsva.h / 60;
46
+ let x = chroma * (1 - Math.abs(hueBy60 % 2 - 1));
47
+ let m = value - chroma;
48
+ chroma += m;
49
+ x += m;
50
+ const index = Math.floor(hueBy60) % 6;
51
+ const red = [ chroma, x, m, m, x, chroma ][index];
52
+ const green = [ x, chroma, chroma, x, m, m ][index];
53
+ const blue = [ m, m, x, chroma, chroma, x ][index];
54
+ return {
55
+ r: Math.round(red * 255),
56
+ g: Math.round(green * 255),
57
+ b: Math.round(blue * 255),
58
+ a: hsva.a
59
+ };
60
+ }
61
+
62
+ function HSVAtoHSLA(hsva) {
63
+ const value = hsva.v / 100;
64
+ const lightness = value * (1 - hsva.s / 100 / 2);
65
+ let saturation;
66
+ if (lightness > 0 && lightness < 1) {
67
+ saturation = Math.round((value - lightness) / Math.min(lightness, 1 - lightness) * 100);
68
+ }
69
+ return {
70
+ h: hsva.h,
71
+ s: saturation || 0,
72
+ l: Math.round(lightness * 100),
73
+ a: hsva.a
74
+ };
75
+ }
76
+
77
+ function RGBAtoHSVA(rgba) {
78
+ const red = rgba.r / 255;
79
+ const green = rgba.g / 255;
80
+ const blue = rgba.b / 255;
81
+ const xmax = Math.max(red, green, blue);
82
+ const xmin = Math.min(red, green, blue);
83
+ const chroma = xmax - xmin;
84
+ const value = xmax;
85
+ let hue = 0;
86
+ let saturation = 0;
87
+ if (chroma) {
88
+ saturation = chroma / xmax;
89
+ if (xmax === red) {
90
+ hue = (green - blue) / chroma;
91
+ } else if (xmax === green) {
92
+ hue = 2 + (blue - red) / chroma;
93
+ } else {
94
+ hue = 4 + (red - green) / chroma;
95
+ }
96
+ }
97
+ hue = Math.floor(hue * 60);
98
+ return {
99
+ h: hue < 0 ? hue + 360 : hue,
100
+ s: Math.round(saturation * 100),
101
+ v: Math.round(value * 100),
102
+ a: rgba.a
103
+ };
104
+ }
105
+
106
+ function strToRGBA(str) {
107
+ const regex = /^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i;
108
+ let match, rgba;
109
+ ctx.fillStyle = "#000";
110
+ ctx.fillStyle = str;
111
+ match = regex.exec(ctx.fillStyle);
112
+ if (match) {
113
+ rgba = {
114
+ r: Number(match[3]),
115
+ g: Number(match[4]),
116
+ b: Number(match[5]),
117
+ a: Number(match[6])
118
+ };
119
+ } else {
120
+ match = ctx.fillStyle.replace("#", "").match(/.{2}/g).map(h => parseInt(h, 16));
121
+ rgba = {
122
+ r: match[0],
123
+ g: match[1],
124
+ b: match[2],
125
+ a: 1
126
+ };
127
+ }
128
+ return rgba;
129
+ }
130
+
131
+ function RGBAToHex(rgba) {
132
+ const R = rgba.r.toString(16).padStart(2, "0");
133
+ const G = rgba.g.toString(16).padStart(2, "0");
134
+ const B = rgba.b.toString(16).padStart(2, "0");
135
+ let A = "";
136
+ if (settings.alpha && (rgba.a < 1 || settings.forceAlpha)) {
137
+ A = (rgba.a * 255 | 0).toString(16).padStart(2, "0");
138
+ }
139
+ return "#" + R + G + B + A;
140
+ }
141
+
142
+ function RGBAToStr(rgba) {
143
+ return !settings.alpha || rgba.a === 1 && !settings.forceAlpha ? `rgb(${rgba.r}, ${rgba.g}, ${rgba.b})` : `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`;
144
+ }
145
+
146
+ function HSLAToStr(hsla) {
147
+ return !settings.alpha || hsla.a === 1 && !settings.forceAlpha ? `hsl(${hsla.h}, ${hsla.s}%, ${hsla.l}%)` : `hsla(${hsla.h}, ${hsla.s}%, ${hsla.l}%, ${hsla.a})`;
148
+ }
149
+
150
+ function getEl(id) {
151
+ return document.getElementById(id);
152
+ }
153
+
154
+ function addListener(context, type, selector, fn) {
155
+ if (typeof selector === "string") {
156
+ context.addEventListener(type, event => {
157
+ if (event.target.matches(selector)) {
158
+ fn.call(event.target, event);
159
+ }
160
+ });
161
+ } else {
162
+ context.addEventListener(type, selector);
163
+ }
164
+ }
165
+
166
+ function DOMReady(fn, args = []) {
167
+ if (document.readyState !== "loading") {
168
+ fn(...args);
169
+ } else {
170
+ document.addEventListener("DOMContentLoaded", () => {
171
+ fn(...args);
172
+ });
173
+ }
174
+ }
175
+
176
+ const currentColor = {
177
+ r: 0,
178
+ g: 0,
179
+ b: 0,
180
+ h: 0,
181
+ s: 0,
182
+ v: 0,
183
+ a: 1
184
+ };
185
+
186
+ let container, picker, colorArea, colorMarker, colorPreview, colorValue, clearButton, closeButton, hueSlider, hueMarker, alphaSlider, alphaMarker, currentEl, currentFormat, oldColor, keyboardNav, colorAreaDims = {};
187
+
188
+ const instances = {};
189
+
190
+ let currentInstanceId = "";
191
+
192
+ let defaultInstance = {};
193
+
194
+ let hasInstance = false;
195
+
196
+ const MARKER_MOVEMENTS = {
197
+ ArrowUp: [ 0, -1 ],
198
+ ArrowDown: [ 0, 1 ],
199
+ ArrowLeft: [ -1, 0 ],
200
+ ArrowRight: [ 1, 0 ]
201
+ };
202
+
203
+ function configure(options) {
204
+ if (typeof options !== "object") {
205
+ return;
206
+ }
207
+ for (const key of Object.keys(options)) {
208
+ switch (key) {
209
+ case "el":
210
+ bindFields(options.el);
211
+ if (options.wrap !== false) {
212
+ wrapFields(options.el);
213
+ }
214
+ break;
215
+
216
+ case "parent":
217
+ container = options.parent instanceof HTMLElement ? options.parent : document.querySelector(options.parent);
218
+ if (container) {
219
+ container.appendChild(picker);
220
+ settings.parent = options.parent;
221
+ if (container === document.body) {
222
+ container = undefined;
223
+ }
224
+ }
225
+ break;
226
+
227
+ case "themeMode":
228
+ settings.themeMode = options.themeMode;
229
+ if (options.themeMode === "auto" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
230
+ settings.themeMode = "dark";
231
+ }
232
+
233
+ case "theme":
234
+ if (options.theme) {
235
+ settings.theme = options.theme;
236
+ }
237
+ picker.className = `clr-picker clr-${settings.theme} clr-${settings.themeMode}`;
238
+ if (settings.inline) {
239
+ updatePickerPosition();
240
+ }
241
+ break;
242
+
243
+ case "rtl":
244
+ settings.rtl = !!options.rtl;
245
+ for (const field of document.getElementsByClassName("clr-field")) {
246
+ field.classList.toggle("clr-rtl", settings.rtl);
247
+ }
248
+ break;
249
+
250
+ case "margin":
251
+ {
252
+ const margin = Number(options.margin);
253
+ settings.margin = !isNaN(margin) ? margin : settings.margin;
254
+ break;
255
+ }
256
+
257
+ case "wrap":
258
+ if (options.el && options.wrap) {
259
+ wrapFields(options.el);
260
+ }
261
+ break;
262
+
263
+ case "formatToggle":
264
+ settings.formatToggle = !!options.formatToggle;
265
+ getEl("clr-format").style.display = settings.formatToggle ? "block" : "none";
266
+ if (settings.formatToggle) {
267
+ settings.format = "auto";
268
+ }
269
+ break;
270
+
271
+ case "swatches":
272
+ if (Array.isArray(options.swatches)) {
273
+ const swatchesContainer = getEl("clr-swatches");
274
+ swatchesContainer.textContent = "";
275
+ if (options.swatches.length) {
276
+ const swatches = document.createElement("div");
277
+ options.swatches.forEach((swatch, i) => {
278
+ const button = document.createElement("button");
279
+ button.setAttribute("type", "button");
280
+ button.setAttribute("id", `clr-swatch-${i}`);
281
+ button.setAttribute("aria-labelledby", `clr-swatch-label clr-swatch-${i}`);
282
+ button.style.color = swatch;
283
+ button.textContent = swatch;
284
+ swatches.appendChild(button);
285
+ });
286
+ swatchesContainer.appendChild(swatches);
287
+ }
288
+ settings.swatches = [ ...options.swatches ];
289
+ }
290
+ break;
291
+
292
+ case "swatchesOnly":
293
+ settings.swatchesOnly = !!options.swatchesOnly;
294
+ picker.setAttribute("data-minimal", settings.swatchesOnly);
295
+ break;
296
+
297
+ case "alpha":
298
+ settings.alpha = !!options.alpha;
299
+ picker.setAttribute("data-alpha", settings.alpha);
300
+ break;
301
+
302
+ case "inline":
303
+ settings.inline = !!options.inline;
304
+ picker.setAttribute("data-inline", settings.inline);
305
+ if (settings.inline) {
306
+ const defaultColor = options.defaultColor || settings.defaultColor;
307
+ currentFormat = getColorFormatFromStr(defaultColor);
308
+ updatePickerPosition();
309
+ setColorFromStr(defaultColor);
310
+ }
311
+ break;
312
+
313
+ case "clearButton":
314
+ {
315
+ if (typeof options.clearButton === "object" && options.clearButton.label) {
316
+ settings.clearLabel = options.clearButton.label;
317
+ clearButton.innerHTML = settings.clearLabel;
318
+ }
319
+ const show = typeof options.clearButton === "object" ? options.clearButton.show : options.clearButton;
320
+ settings.clearButton = !!show;
321
+ clearButton.style.display = settings.clearButton ? "block" : "none";
322
+ break;
323
+ }
324
+
325
+ case "clearLabel":
326
+ settings.clearLabel = options.clearLabel;
327
+ clearButton.innerHTML = settings.clearLabel;
328
+ break;
329
+
330
+ case "closeButton":
331
+ settings.closeButton = !!options.closeButton;
332
+ if (settings.closeButton) {
333
+ picker.insertBefore(closeButton, colorPreview);
334
+ } else {
335
+ colorPreview.appendChild(closeButton);
336
+ }
337
+ break;
338
+
339
+ case "closeLabel":
340
+ settings.closeLabel = options.closeLabel;
341
+ closeButton.innerHTML = settings.closeLabel;
342
+ break;
343
+
344
+ case "a11y":
345
+ {
346
+ const labels = options.a11y;
347
+ let update = false;
348
+ if (typeof labels === "object") {
349
+ for (const label of Object.keys(labels)) {
350
+ if (labels[label] && settings.a11y[label]) {
351
+ settings.a11y[label] = labels[label];
352
+ update = true;
353
+ }
354
+ }
355
+ }
356
+ if (update) {
357
+ const openLabel = getEl("clr-open-label");
358
+ const swatchLabel = getEl("clr-swatch-label");
359
+ openLabel.innerHTML = settings.a11y.open;
360
+ swatchLabel.innerHTML = settings.a11y.swatch;
361
+ closeButton.setAttribute("aria-label", settings.a11y.close);
362
+ clearButton.setAttribute("aria-label", settings.a11y.clear);
363
+ hueSlider.setAttribute("aria-label", settings.a11y.hueSlider);
364
+ alphaSlider.setAttribute("aria-label", settings.a11y.alphaSlider);
365
+ colorValue.setAttribute("aria-label", settings.a11y.input);
366
+ colorArea.setAttribute("aria-label", settings.a11y.instruction);
367
+ }
368
+ break;
369
+ }
370
+
371
+ default:
372
+ settings[key] = options[key];
373
+ }
374
+ }
375
+ }
376
+
377
+ function setVirtualInstance(selector, options) {
378
+ if (typeof selector === "string" && typeof options === "object") {
379
+ instances[selector] = options;
380
+ hasInstance = true;
381
+ }
382
+ }
383
+
384
+ function removeVirtualInstance(selector) {
385
+ delete instances[selector];
386
+ if (Object.keys(instances).length === 0) {
387
+ hasInstance = false;
388
+ if (selector === currentInstanceId) {
389
+ resetVirtualInstance();
390
+ }
391
+ }
392
+ }
393
+
394
+ function attachVirtualInstance(element) {
395
+ if (hasInstance) {
396
+ const unsupportedOptions = [ "el", "wrap", "rtl", "inline", "defaultColor", "a11y" ];
397
+ for (const selector of Object.keys(instances)) {
398
+ const options = instances[selector];
399
+ if (element.matches(selector)) {
400
+ currentInstanceId = selector;
401
+ defaultInstance = {};
402
+ unsupportedOptions.forEach(option => delete options[option]);
403
+ for (const option of Object.keys(options)) {
404
+ defaultInstance[option] = Array.isArray(settings[option]) ? [ ...settings[option] ] : settings[option];
405
+ }
406
+ configure(options);
407
+ break;
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ function resetVirtualInstance() {
414
+ if (Object.keys(defaultInstance).length > 0) {
415
+ configure(defaultInstance);
416
+ currentInstanceId = "";
417
+ defaultInstance = {};
418
+ }
419
+ }
420
+
421
+ function bindFields(selector) {
422
+ const fields = selector instanceof HTMLElement ? [ selector ] : selector;
423
+ if (Array.isArray(fields)) {
424
+ fields.forEach(field => {
425
+ addListener(field, "click", openPicker);
426
+ addListener(field, "input", updateColorPreview);
427
+ });
428
+ } else {
429
+ addListener(document, "click", fields, openPicker);
430
+ addListener(document, "input", fields, updateColorPreview);
431
+ }
432
+ }
433
+
434
+ function openPicker(event) {
435
+ if (settings.inline) {
436
+ return;
437
+ }
438
+ attachVirtualInstance(event.target);
439
+ currentEl = event.target;
440
+ oldColor = currentEl.value;
441
+ currentFormat = getColorFormatFromStr(oldColor);
442
+ picker.classList.add("clr-open");
443
+ updatePickerPosition();
444
+ setColorFromStr(oldColor);
445
+ if (settings.focusInput || settings.selectInput) {
446
+ colorValue.focus({
447
+ preventScroll: true
448
+ });
449
+ colorValue.setSelectionRange(currentEl.selectionStart, currentEl.selectionEnd);
450
+ }
451
+ if (settings.selectInput) {
452
+ colorValue.select();
453
+ }
454
+ if (keyboardNav || settings.swatchesOnly) {
455
+ getFocusableElements().shift().focus();
456
+ }
457
+ currentEl.dispatchEvent(new Event("open", {
458
+ bubbles: false
459
+ }));
460
+ }
461
+
462
+ function updatePickerPosition() {
463
+ const parent = container;
464
+ const scrollY = window.scrollY;
465
+ const pickerWidth = picker.offsetWidth;
466
+ const pickerHeight = picker.offsetHeight;
467
+ const reposition = {
468
+ left: false,
469
+ top: false
470
+ };
471
+ let parentStyle, parentMarginTop, parentBorderTop;
472
+ let offset = {
473
+ x: 0,
474
+ y: 0
475
+ };
476
+ if (parent) {
477
+ parentStyle = window.getComputedStyle(parent);
478
+ parentMarginTop = parseFloat(parentStyle.marginTop);
479
+ parentBorderTop = parseFloat(parentStyle.borderTopWidth);
480
+ offset = parent.getBoundingClientRect();
481
+ offset.y += parentBorderTop + scrollY;
482
+ }
483
+ if (!settings.inline) {
484
+ const coords = currentEl.getBoundingClientRect();
485
+ let left = coords.x;
486
+ let top = scrollY + coords.y + coords.height + settings.margin;
487
+ if (parent) {
488
+ left -= offset.x;
489
+ top -= offset.y;
490
+ if (left + pickerWidth > parent.clientWidth) {
491
+ left += coords.width - pickerWidth;
492
+ reposition.left = true;
493
+ }
494
+ if (top + pickerHeight > parent.clientHeight - parentMarginTop) {
495
+ if (pickerHeight + settings.margin <= coords.top - (offset.y - scrollY)) {
496
+ top -= coords.height + pickerHeight + settings.margin * 2;
497
+ reposition.top = true;
498
+ }
499
+ }
500
+ top += parent.scrollTop;
501
+ } else {
502
+ if (left + pickerWidth > document.documentElement.clientWidth) {
503
+ left += coords.width - pickerWidth;
504
+ reposition.left = true;
505
+ }
506
+ if (top + pickerHeight - scrollY > document.documentElement.clientHeight) {
507
+ if (pickerHeight + settings.margin <= coords.top) {
508
+ top = scrollY + coords.y - pickerHeight - settings.margin;
509
+ reposition.top = true;
510
+ }
511
+ }
512
+ }
513
+ picker.classList.toggle("clr-left", reposition.left);
514
+ picker.classList.toggle("clr-top", reposition.top);
515
+ picker.style.left = `${left}px`;
516
+ picker.style.top = `${top}px`;
517
+ offset.x += picker.offsetLeft;
518
+ offset.y += picker.offsetTop;
519
+ }
520
+ colorAreaDims = {
521
+ width: colorArea.offsetWidth,
522
+ height: colorArea.offsetHeight,
523
+ x: colorArea.offsetLeft + offset.x,
524
+ y: colorArea.offsetTop + offset.y
525
+ };
526
+ }
527
+
528
+ function wrapFields(selector) {
529
+ const fields = selector instanceof HTMLElement ? [ selector ] : Array.isArray(selector) ? selector : [ ...document.querySelectorAll(selector) ];
530
+ fields.forEach(wrapColorField);
531
+ }
532
+
533
+ function wrapColorField(field) {
534
+ const parentNode = field.parentNode;
535
+ if (!parentNode.classList.contains("clr-field")) {
536
+ const wrapper = document.createElement("div");
537
+ let classes = "clr-field";
538
+ if (settings.rtl || field.classList.contains("clr-rtl")) {
539
+ classes += " clr-rtl";
540
+ }
541
+ wrapper.innerHTML = '<button type="button" aria-labelledby="clr-open-label"></button>';
542
+ parentNode.insertBefore(wrapper, field);
543
+ wrapper.className = classes;
544
+ wrapper.style.color = field.value;
545
+ wrapper.appendChild(field);
546
+ }
547
+ }
548
+
549
+ function updateColorPreview(event) {
550
+ const parent = event.target.parentNode;
551
+ if (parent.classList.contains("clr-field")) {
552
+ parent.style.color = event.target.value;
553
+ }
554
+ }
555
+
556
+ function closePicker(revert) {
557
+ if (currentEl && !settings.inline) {
558
+ const prevEl = currentEl;
559
+ if (revert) {
560
+ currentEl = undefined;
561
+ if (oldColor !== prevEl.value) {
562
+ prevEl.value = oldColor;
563
+ prevEl.dispatchEvent(new Event("input", {
564
+ bubbles: true
565
+ }));
566
+ }
567
+ }
568
+ setTimeout(() => {
569
+ if (oldColor !== prevEl.value) {
570
+ prevEl.dispatchEvent(new Event("change", {
571
+ bubbles: true
572
+ }));
573
+ }
574
+ });
575
+ picker.classList.remove("clr-open");
576
+ if (hasInstance) {
577
+ resetVirtualInstance();
578
+ }
579
+ prevEl.dispatchEvent(new Event("close", {
580
+ bubbles: false
581
+ }));
582
+ if (settings.focusInput) {
583
+ prevEl.focus({
584
+ preventScroll: true
585
+ });
586
+ }
587
+ currentEl = undefined;
588
+ }
589
+ }
590
+
591
+ function setColorFromStr(str) {
592
+ const rgba = strToRGBA(str);
593
+ const hsva = RGBAtoHSVA(rgba);
594
+ updateMarkerA11yLabel(hsva.s, hsva.v);
595
+ updateColor(rgba, hsva);
596
+ hueSlider.value = hsva.h;
597
+ picker.style.color = `hsl(${hsva.h}, 100%, 50%)`;
598
+ hueMarker.style.left = `${hsva.h / 360 * 100}%`;
599
+ colorMarker.style.left = `${colorAreaDims.width * hsva.s / 100}px`;
600
+ colorMarker.style.top = `${colorAreaDims.height - colorAreaDims.height * hsva.v / 100}px`;
601
+ alphaSlider.value = hsva.a * 100;
602
+ alphaMarker.style.left = `${hsva.a * 100}%`;
603
+ }
604
+
605
+ function getColorFormatFromStr(str) {
606
+ const fmt = str.slice(0, 3).toLowerCase();
607
+ return fmt === "rgb" || fmt === "hsl" ? fmt : "hex";
608
+ }
609
+
610
+ function pickColor(color) {
611
+ color = color ?? colorValue.value;
612
+ if (currentEl) {
613
+ currentEl.value = color;
614
+ currentEl.dispatchEvent(new Event("input", {
615
+ bubbles: true
616
+ }));
617
+ }
618
+ if (settings.onChange) {
619
+ settings.onChange.call(window, color, currentEl);
620
+ }
621
+ document.dispatchEvent(new CustomEvent("colorpicker:pick", {
622
+ detail: {
623
+ color: color,
624
+ currentEl: currentEl
625
+ }
626
+ }));
627
+ }
628
+
629
+ function setColorAtPosition(x, y) {
630
+ const hsva = {
631
+ h: Number(hueSlider.value),
632
+ s: x / colorAreaDims.width * 100,
633
+ v: 100 - y / colorAreaDims.height * 100,
634
+ a: alphaSlider.value / 100
635
+ };
636
+ const rgba = HSVAtoRGBA(hsva);
637
+ updateMarkerA11yLabel(hsva.s, hsva.v);
638
+ updateColor(rgba, hsva);
639
+ pickColor();
640
+ }
641
+
642
+ function updateMarkerA11yLabel(saturation, value) {
643
+ colorMarker.setAttribute("aria-label", settings.a11y.marker.replace("{s}", Number(saturation.toFixed(1))).replace("{v}", Number(value.toFixed(1))));
644
+ }
645
+
646
+ function moveMarker(event) {
647
+ const touch = event.changedTouches?.[0];
648
+ let x = (touch?.pageX ?? event.pageX) - colorAreaDims.x;
649
+ let y = (touch?.pageY ?? event.pageY) - colorAreaDims.y;
650
+ if (container) {
651
+ y += container.scrollTop;
652
+ }
653
+ setMarkerPosition(x, y);
654
+ event.preventDefault();
655
+ event.stopPropagation();
656
+ }
657
+
658
+ function moveMarkerOnKeydown(offsetX, offsetY) {
659
+ const x = parseFloat(colorMarker.style.left) + offsetX;
660
+ const y = parseFloat(colorMarker.style.top) + offsetY;
661
+ setMarkerPosition(x, y);
662
+ }
663
+
664
+ function setMarkerPosition(x, y) {
665
+ x = Math.max(0, Math.min(x, colorAreaDims.width));
666
+ y = Math.max(0, Math.min(y, colorAreaDims.height));
667
+ colorMarker.style.left = `${x}px`;
668
+ colorMarker.style.top = `${y}px`;
669
+ setColorAtPosition(x, y);
670
+ colorMarker.focus();
671
+ }
672
+
673
+ function updateColor(rgba = {}, hsva = {}) {
674
+ let format = settings.format;
675
+ Object.assign(currentColor, rgba, hsva);
676
+ const hex = RGBAToHex(currentColor);
677
+ const opaqueHex = hex.substring(0, 7);
678
+ colorMarker.style.color = opaqueHex;
679
+ alphaMarker.parentNode.style.color = opaqueHex;
680
+ alphaMarker.style.color = hex;
681
+ colorPreview.style.color = hex;
682
+ colorArea.style.display = "none";
683
+ colorArea.offsetHeight;
684
+ colorArea.style.display = "";
685
+ alphaMarker.nextElementSibling.style.display = "none";
686
+ alphaMarker.nextElementSibling.offsetHeight;
687
+ alphaMarker.nextElementSibling.style.display = "";
688
+ if (format === "mixed") {
689
+ format = currentColor.a === 1 ? "hex" : "rgb";
690
+ } else if (format === "auto") {
691
+ format = currentFormat;
692
+ }
693
+ switch (format) {
694
+ case "hex":
695
+ colorValue.value = hex;
696
+ break;
697
+
698
+ case "rgb":
699
+ colorValue.value = RGBAToStr(currentColor);
700
+ break;
701
+
702
+ case "hsl":
703
+ colorValue.value = HSLAToStr(HSVAtoHSLA(currentColor));
704
+ break;
705
+ }
706
+ document.querySelector(`.clr-format [value="${format}"]`).checked = true;
707
+ }
708
+
709
+ function setHue() {
710
+ const hue = Number(hueSlider.value);
711
+ const x = parseFloat(colorMarker.style.left);
712
+ const y = parseFloat(colorMarker.style.top);
713
+ picker.style.color = `hsl(${hue}, 100%, 50%)`;
714
+ hueMarker.style.left = `${hue / 360 * 100}%`;
715
+ setColorAtPosition(x, y);
716
+ }
717
+
718
+ function setAlpha() {
719
+ const alpha = alphaSlider.value / 100;
720
+ alphaMarker.style.left = `${alpha * 100}%`;
721
+ updateColor({
722
+ a: alpha
723
+ });
724
+ pickColor();
725
+ }
726
+
727
+ function getFocusableElements() {
728
+ return [ ...picker.querySelectorAll("input, button") ].filter(node => !!node.offsetWidth);
729
+ }
730
+
731
+ function init() {
732
+ container = undefined;
733
+ picker = document.createElement("div");
734
+ picker.setAttribute("id", "clr-picker");
735
+ picker.className = "clr-picker";
736
+ picker.innerHTML = `\n <input id="clr-color-value" name="clr-color-value" class="clr-color" type="text" value="" spellcheck="false" aria-label="${settings.a11y.input}">\n <div id="clr-color-area" class="clr-gradient" role="application" aria-label="${settings.a11y.instruction}">\n <div id="clr-color-marker" class="clr-marker" tabindex="0"></div>\n </div>\n <div class="clr-hue">\n <input id="clr-hue-slider" name="clr-hue-slider" type="range" min="0" max="360" step="1" aria-label="${settings.a11y.hueSlider}">\n <div id="clr-hue-marker"></div>\n </div>\n <div class="clr-alpha">\n <input id="clr-alpha-slider" name="clr-alpha-slider" type="range" min="0" max="100" step="1" aria-label="${settings.a11y.alphaSlider}">\n <div id="clr-alpha-marker"></div>\n <span></span>\n </div>\n <div id="clr-format" class="clr-format">\n <fieldset class="clr-segmented">\n <legend>${settings.a11y.format}</legend>\n <input id="clr-f1" type="radio" name="clr-format" value="hex">\n <label for="clr-f1">Hex</label>\n <input id="clr-f2" type="radio" name="clr-format" value="rgb">\n <label for="clr-f2">RGB</label>\n <input id="clr-f3" type="radio" name="clr-format" value="hsl">\n <label for="clr-f3">HSL</label>\n <span></span>\n </fieldset>\n </div>\n <div id="clr-swatches" class="clr-swatches"></div>\n <button type="button" id="clr-clear" class="clr-clear" aria-label="${settings.a11y.clear}">${settings.clearLabel}</button>\n <div id="clr-color-preview" class="clr-preview">\n <button type="button" id="clr-close" class="clr-close" aria-label="${settings.a11y.close}">${settings.closeLabel}</button>\n </div>\n <span id="clr-open-label" hidden>${settings.a11y.open}</span>\n <span id="clr-swatch-label" hidden>${settings.a11y.swatch}</span>\n `;
737
+ document.body.appendChild(picker);
738
+ colorArea = getEl("clr-color-area");
739
+ colorMarker = getEl("clr-color-marker");
740
+ clearButton = getEl("clr-clear");
741
+ closeButton = getEl("clr-close");
742
+ colorPreview = getEl("clr-color-preview");
743
+ colorValue = getEl("clr-color-value");
744
+ hueSlider = getEl("clr-hue-slider");
745
+ hueMarker = getEl("clr-hue-marker");
746
+ alphaSlider = getEl("clr-alpha-slider");
747
+ alphaMarker = getEl("clr-alpha-marker");
748
+ bindFields(settings.el);
749
+ wrapFields(settings.el);
750
+ addListener(picker, "mousedown", event => {
751
+ picker.classList.remove("clr-keyboard-nav");
752
+ event.stopPropagation();
753
+ });
754
+ addListener(colorArea, "mousedown", () => {
755
+ addListener(document, "mousemove", moveMarker);
756
+ });
757
+ addListener(colorArea, "contextmenu", event => {
758
+ event.preventDefault();
759
+ });
760
+ addListener(colorArea, "touchstart", () => {
761
+ document.addEventListener("touchmove", moveMarker, {
762
+ passive: false
763
+ });
764
+ });
765
+ addListener(colorMarker, "mousedown", () => {
766
+ addListener(document, "mousemove", moveMarker);
767
+ });
768
+ addListener(colorMarker, "touchstart", () => {
769
+ document.addEventListener("touchmove", moveMarker, {
770
+ passive: false
771
+ });
772
+ });
773
+ addListener(colorValue, "change", () => {
774
+ const value = colorValue.value;
775
+ if (currentEl || settings.inline) {
776
+ const color = value === "" ? value : setColorFromStr(value);
777
+ pickColor(color);
778
+ }
779
+ });
780
+ addListener(clearButton, "click", () => {
781
+ pickColor("");
782
+ closePicker();
783
+ });
784
+ addListener(closeButton, "click", () => {
785
+ pickColor();
786
+ closePicker();
787
+ });
788
+ addListener(getEl("clr-format"), "click", ".clr-format input", event => {
789
+ currentFormat = event.target.value;
790
+ updateColor();
791
+ pickColor();
792
+ });
793
+ addListener(picker, "click", ".clr-swatches button", event => {
794
+ setColorFromStr(event.target.textContent);
795
+ pickColor();
796
+ if (settings.swatchesOnly) {
797
+ closePicker();
798
+ }
799
+ });
800
+ addListener(document, "mouseup", () => {
801
+ document.removeEventListener("mousemove", moveMarker);
802
+ });
803
+ addListener(document, "touchend", () => {
804
+ document.removeEventListener("touchmove", moveMarker);
805
+ });
806
+ addListener(document, "mousedown", () => {
807
+ keyboardNav = false;
808
+ picker.classList.remove("clr-keyboard-nav");
809
+ closePicker();
810
+ });
811
+ addListener(document, "keydown", event => {
812
+ const key = event.key;
813
+ const target = event.target;
814
+ const shiftKey = event.shiftKey;
815
+ const navKeys = [ "Tab", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight" ];
816
+ if (key === "Escape") {
817
+ closePicker(true);
818
+ return;
819
+ } else if (key === "Enter" && target.tagName !== "BUTTON") {
820
+ closePicker();
821
+ return;
822
+ } else if (navKeys.includes(key)) {
823
+ keyboardNav = true;
824
+ picker.classList.add("clr-keyboard-nav");
825
+ }
826
+ if (key === "Tab" && target.matches(".clr-picker *")) {
827
+ const focusables = getFocusableElements();
828
+ const firstFocusable = focusables.shift();
829
+ const lastFocusable = focusables.pop();
830
+ if (shiftKey && target === firstFocusable) {
831
+ lastFocusable.focus();
832
+ event.preventDefault();
833
+ } else if (!shiftKey && target === lastFocusable) {
834
+ firstFocusable.focus();
835
+ event.preventDefault();
836
+ }
837
+ }
838
+ });
839
+ addListener(document, "click", ".clr-field button", event => {
840
+ if (hasInstance) {
841
+ resetVirtualInstance();
842
+ }
843
+ event.target.nextElementSibling.dispatchEvent(new Event("click", {
844
+ bubbles: true
845
+ }));
846
+ });
847
+ addListener(colorMarker, "keydown", event => {
848
+ if (event.key in MARKER_MOVEMENTS) {
849
+ moveMarkerOnKeydown(...MARKER_MOVEMENTS[event.key]);
850
+ event.preventDefault();
851
+ }
852
+ });
853
+ addListener(colorArea, "click", moveMarker);
854
+ addListener(hueSlider, "input", setHue);
855
+ addListener(alphaSlider, "input", setAlpha);
856
+ }
857
+
858
+ function ColorPicker(options) {
859
+ DOMReady(() => {
860
+ if (options) {
861
+ if (typeof options === "string") {
862
+ bindFields(options);
863
+ } else {
864
+ configure(options);
865
+ }
866
+ }
867
+ });
868
+ }
869
+
870
+ ColorPicker.set = (...args) => DOMReady(configure, args);
871
+
872
+ ColorPicker.wrap = (...args) => DOMReady(wrapFields, args);
873
+
874
+ ColorPicker.close = (...args) => DOMReady(closePicker, args);
875
+
876
+ ColorPicker.setInstance = (...args) => DOMReady(setVirtualInstance, args);
877
+
878
+ ColorPicker.removeInstance = (...args) => DOMReady(removeVirtualInstance, args);
879
+
880
+ ColorPicker.updatePosition = (...args) => DOMReady(updatePickerPosition, args);
881
+
882
+ ColorPicker.ready = DOMReady;
883
+
884
+ DOMReady(init);
885
+
886
+ export { ColorPicker as default };