@bornova/colorpicker 1.0.0 → 1.1.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.
@@ -1,4 +1,4 @@
1
- /* @bornova/colorpicker v1.0.0 */
1
+ /* @bornova/colorpicker v1.1.0 */
2
2
  const settings = {
3
3
  el: "[data-colorpicker]",
4
4
  parent: "body",
@@ -36,7 +36,21 @@ const settings = {
36
36
  }
37
37
  };
38
38
 
39
- const ctx = document.createElement("canvas").getContext("2d");
39
+ let ctx;
40
+
41
+ function getCtx() {
42
+ if (!ctx && typeof document !== "undefined") {
43
+ try {
44
+ const canvas = document.createElement("canvas");
45
+ canvas.width = 1;
46
+ canvas.height = 1;
47
+ ctx = canvas.getContext("2d", {
48
+ willReadFrequently: true
49
+ });
50
+ } catch {}
51
+ }
52
+ return ctx;
53
+ }
40
54
 
41
55
  function HSVAtoRGBA(hsva) {
42
56
  const saturation = hsva.s / 100;
@@ -94,9 +108,9 @@ function RGBAtoHSVA(rgba) {
94
108
  hue = 4 + (red - green) / chroma;
95
109
  }
96
110
  }
97
- hue = Math.floor(hue * 60);
111
+ hue = Math.round(hue * 60);
98
112
  return {
99
- h: hue < 0 ? hue + 360 : hue,
113
+ h: (hue + 360) % 360,
100
114
  s: Math.round(saturation * 100),
101
115
  v: Math.round(value * 100),
102
116
  a: rgba.a
@@ -104,28 +118,62 @@ function RGBAtoHSVA(rgba) {
104
118
  }
105
119
 
106
120
  function strToRGBA(str) {
121
+ if (typeof str !== "string" || !str.trim()) {
122
+ return {
123
+ r: 0,
124
+ g: 0,
125
+ b: 0,
126
+ a: 1
127
+ };
128
+ }
107
129
  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);
130
+ const canvasCtx = getCtx();
131
+ let normalized = str.trim();
132
+ if (canvasCtx) {
133
+ canvasCtx.fillStyle = "#000";
134
+ canvasCtx.fillStyle = str;
135
+ normalized = canvasCtx.fillStyle;
136
+ }
137
+ const match = regex.exec(normalized);
112
138
  if (match) {
113
- rgba = {
114
- r: Number(match[3]),
115
- g: Number(match[4]),
116
- b: Number(match[5]),
117
- a: Number(match[6])
139
+ return {
140
+ r: Math.min(255, Math.max(0, Number(match[3]))),
141
+ g: Math.min(255, Math.max(0, Number(match[4]))),
142
+ b: Math.min(255, Math.max(0, Number(match[5]))),
143
+ a: match[6] === "" ? 1 : Math.min(1, Math.max(0, Number(Number(match[6]).toFixed(2))))
118
144
  };
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
145
+ }
146
+ if (/^#?(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(normalized)) {
147
+ let hex = normalized.replace("#", "");
148
+ if (hex.length === 3 || hex.length === 4) {
149
+ hex = hex.split("").map(c => c + c).join("");
150
+ }
151
+ return {
152
+ r: parseInt(hex.slice(0, 2), 16),
153
+ g: parseInt(hex.slice(2, 4), 16),
154
+ b: parseInt(hex.slice(4, 6), 16),
155
+ a: hex.length === 8 ? Number((parseInt(hex.slice(6, 8), 16) / 255).toFixed(2)) : 1
126
156
  };
127
157
  }
128
- return rgba;
158
+ if (canvasCtx) {
159
+ try {
160
+ canvasCtx.clearRect(0, 0, 1, 1);
161
+ canvasCtx.fillRect(0, 0, 1, 1);
162
+ const [r, g, b, a] = canvasCtx.getImageData(0, 0, 1, 1).data;
163
+ return {
164
+ r: r,
165
+ g: g,
166
+ b: b,
167
+ a: Number((a / 255).toFixed(2))
168
+ };
169
+ } catch {}
170
+ }
171
+ return {
172
+ r: 0,
173
+ g: 0,
174
+ b: 0,
175
+ a: 1
176
+ };
129
177
  }
130
178
 
131
179
  function RGBAToHex(rgba) {
@@ -134,7 +182,7 @@ function RGBAToHex(rgba) {
134
182
  const B = rgba.b.toString(16).padStart(2, "0");
135
183
  let A = "";
136
184
  if (settings.alpha && (rgba.a < 1 || settings.forceAlpha)) {
137
- A = (rgba.a * 255 | 0).toString(16).padStart(2, "0");
185
+ A = Math.round(rgba.a * 255).toString(16).padStart(2, "0");
138
186
  }
139
187
  return "#" + R + G + B + A;
140
188
  }
@@ -154,7 +202,7 @@ function getEl(id) {
154
202
  function addListener(context, type, selector, fn) {
155
203
  if (typeof selector === "string") {
156
204
  context.addEventListener(type, event => {
157
- if (event.target.matches(selector)) {
205
+ if (event.target instanceof Element && event.target.matches(selector)) {
158
206
  fn.call(event.target, event);
159
207
  }
160
208
  });
@@ -183,7 +231,7 @@ const currentColor = {
183
231
  a: 1
184
232
  };
185
233
 
186
- let container, picker, colorArea, colorMarker, colorPreview, colorValue, clearButton, closeButton, hueSlider, hueMarker, alphaSlider, alphaMarker, currentEl, currentFormat, oldColor, keyboardNav, colorAreaDims = {};
234
+ let container, picker, colorArea, colorMarker, colorPreview, colorValue, clearButton, closeButton, hueSlider, hueMarker, alphaSlider, alphaMarker, currentEl, currentFormat, oldColor, initialFormatColor, colorAreaDims = {};
187
235
 
188
236
  const instances = {};
189
237
 
@@ -191,7 +239,9 @@ let currentInstanceId = "";
191
239
 
192
240
  let defaultInstance = {};
193
241
 
194
- let hasInstance = false;
242
+ const boundSelectors = new Set;
243
+
244
+ const boundElements = new WeakSet;
195
245
 
196
246
  const MARKER_MOVEMENTS = {
197
247
  ArrowUp: [ 0, -1 ],
@@ -201,28 +251,28 @@ const MARKER_MOVEMENTS = {
201
251
  };
202
252
 
203
253
  function configure(options) {
204
- if (typeof options !== "object") {
254
+ if (!options || typeof options !== "object") {
205
255
  return;
206
256
  }
207
257
  for (const key of Object.keys(options)) {
208
258
  switch (key) {
209
259
  case "el":
210
260
  bindFields(options.el);
211
- if (options.wrap !== false) {
261
+ if (options.wrap !== false && (options.wrap || settings.wrap)) {
212
262
  wrapFields(options.el);
213
263
  }
214
264
  break;
215
265
 
216
266
  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;
267
+ {
268
+ const newContainer = options.parent instanceof HTMLElement ? options.parent : typeof options.parent === "string" ? document.querySelector(options.parent) : null;
269
+ if (newContainer) {
270
+ newContainer.appendChild(picker);
271
+ settings.parent = options.parent;
272
+ container = newContainer === document.body ? undefined : newContainer;
223
273
  }
274
+ break;
224
275
  }
225
- break;
226
276
 
227
277
  case "themeMode":
228
278
  settings.themeMode = options.themeMode;
@@ -231,14 +281,22 @@ function configure(options) {
231
281
  }
232
282
 
233
283
  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();
284
+ {
285
+ if (options.theme) {
286
+ settings.theme = options.theme;
287
+ }
288
+ const isOpen = picker.classList.contains("clr-open");
289
+ const isLeft = picker.classList.contains("clr-left");
290
+ const isTop = picker.classList.contains("clr-top");
291
+ picker.className = `clr-picker clr-${settings.theme} clr-${settings.themeMode}`;
292
+ picker.classList.toggle("clr-open", isOpen);
293
+ picker.classList.toggle("clr-left", isLeft);
294
+ picker.classList.toggle("clr-top", isTop);
295
+ if (settings.inline) {
296
+ updatePickerPosition();
297
+ }
298
+ break;
240
299
  }
241
- break;
242
300
 
243
301
  case "rtl":
244
302
  settings.rtl = !!options.rtl;
@@ -255,8 +313,9 @@ function configure(options) {
255
313
  }
256
314
 
257
315
  case "wrap":
258
- if (options.el && options.wrap) {
259
- wrapFields(options.el);
316
+ settings.wrap = !!options.wrap;
317
+ if (!("el" in options) && options.wrap) {
318
+ wrapFields(settings.el);
260
319
  }
261
320
  break;
262
321
 
@@ -302,29 +361,16 @@ function configure(options) {
302
361
  case "inline":
303
362
  settings.inline = !!options.inline;
304
363
  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
364
  break;
312
365
 
313
366
  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
- }
367
+ settings.clearButton = !!options.clearButton;
368
+ clearButton.style.display = settings.clearButton ? "block" : "none";
369
+ break;
324
370
 
325
371
  case "clearLabel":
326
372
  settings.clearLabel = options.clearLabel;
327
- clearButton.innerHTML = settings.clearLabel;
373
+ clearButton.textContent = settings.clearLabel;
328
374
  break;
329
375
 
330
376
  case "closeButton":
@@ -338,14 +384,14 @@ function configure(options) {
338
384
 
339
385
  case "closeLabel":
340
386
  settings.closeLabel = options.closeLabel;
341
- closeButton.innerHTML = settings.closeLabel;
387
+ closeButton.textContent = settings.closeLabel;
342
388
  break;
343
389
 
344
390
  case "a11y":
345
391
  {
346
392
  const labels = options.a11y;
347
393
  let update = false;
348
- if (typeof labels === "object") {
394
+ if (labels && typeof labels === "object") {
349
395
  for (const label of Object.keys(labels)) {
350
396
  if (labels[label] && settings.a11y[label]) {
351
397
  settings.a11y[label] = labels[label];
@@ -356,14 +402,19 @@ function configure(options) {
356
402
  if (update) {
357
403
  const openLabel = getEl("clr-open-label");
358
404
  const swatchLabel = getEl("clr-swatch-label");
359
- openLabel.innerHTML = settings.a11y.open;
360
- swatchLabel.innerHTML = settings.a11y.swatch;
405
+ const formatLegend = picker.querySelector(".clr-format legend");
406
+ openLabel.textContent = settings.a11y.open;
407
+ swatchLabel.textContent = settings.a11y.swatch;
408
+ if (formatLegend) {
409
+ formatLegend.textContent = settings.a11y.format;
410
+ }
361
411
  closeButton.setAttribute("aria-label", settings.a11y.close);
362
412
  clearButton.setAttribute("aria-label", settings.a11y.clear);
363
413
  hueSlider.setAttribute("aria-label", settings.a11y.hueSlider);
364
414
  alphaSlider.setAttribute("aria-label", settings.a11y.alphaSlider);
365
415
  colorValue.setAttribute("aria-label", settings.a11y.input);
366
416
  colorArea.setAttribute("aria-label", settings.a11y.instruction);
417
+ updateMarkerA11yLabel(currentColor.s, currentColor.v);
367
418
  }
368
419
  break;
369
420
  }
@@ -372,40 +423,52 @@ function configure(options) {
372
423
  settings[key] = options[key];
373
424
  }
374
425
  }
426
+ if (settings.formatToggle) {
427
+ settings.format = "auto";
428
+ }
429
+ if (settings.inline && ("inline" in options || "defaultColor" in options || "format" in options || "formatToggle" in options || "alpha" in options || "forceAlpha" in options)) {
430
+ const defaultColor = options.defaultColor || settings.defaultColor;
431
+ currentFormat = getColorFormatFromStr(defaultColor);
432
+ updatePickerPosition();
433
+ setColorFromStr(defaultColor);
434
+ }
375
435
  }
376
436
 
377
437
  function setVirtualInstance(selector, options) {
378
- if (typeof selector === "string" && typeof options === "object") {
438
+ if (typeof selector === "string" && options && typeof options === "object") {
379
439
  instances[selector] = options;
380
- hasInstance = true;
381
440
  }
382
441
  }
383
442
 
384
443
  function removeVirtualInstance(selector) {
385
444
  delete instances[selector];
386
- if (Object.keys(instances).length === 0) {
387
- hasInstance = false;
388
- if (selector === currentInstanceId) {
389
- resetVirtualInstance();
390
- }
445
+ if (selector === currentInstanceId) {
446
+ resetVirtualInstance();
391
447
  }
392
448
  }
393
449
 
394
450
  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];
451
+ if (currentInstanceId) {
452
+ resetVirtualInstance();
453
+ }
454
+ const unsupportedOptions = [ "el", "wrap", "rtl", "inline", "defaultColor", "a11y" ];
455
+ for (const selector of Object.keys(instances)) {
456
+ const options = instances[selector];
457
+ if (element.matches(selector)) {
458
+ currentInstanceId = selector;
459
+ defaultInstance = {};
460
+ const clonedOptions = {
461
+ ...options
462
+ };
463
+ unsupportedOptions.forEach(option => delete clonedOptions[option]);
464
+ for (const option of Object.keys(clonedOptions)) {
465
+ defaultInstance[option] = Array.isArray(settings[option]) ? [ ...settings[option] ] : settings[option];
466
+ if (option === "formatToggle" && !("format" in clonedOptions)) {
467
+ defaultInstance.format = settings.format;
405
468
  }
406
- configure(options);
407
- break;
408
469
  }
470
+ configure(clonedOptions);
471
+ break;
409
472
  }
410
473
  }
411
474
  }
@@ -419,13 +482,17 @@ function resetVirtualInstance() {
419
482
  }
420
483
 
421
484
  function bindFields(selector) {
422
- const fields = selector instanceof HTMLElement ? [ selector ] : selector;
485
+ const fields = selector instanceof HTMLElement ? [ selector ] : selector instanceof NodeList ? [ ...selector ] : selector;
423
486
  if (Array.isArray(fields)) {
424
487
  fields.forEach(field => {
425
- addListener(field, "click", openPicker);
426
- addListener(field, "input", updateColorPreview);
488
+ if (field instanceof HTMLElement && !boundElements.has(field)) {
489
+ boundElements.add(field);
490
+ addListener(field, "click", openPicker);
491
+ addListener(field, "input", updateColorPreview);
492
+ }
427
493
  });
428
- } else {
494
+ } else if (typeof fields === "string" && !boundSelectors.has(fields)) {
495
+ boundSelectors.add(fields);
429
496
  addListener(document, "click", fields, openPicker);
430
497
  addListener(document, "input", fields, updateColorPreview);
431
498
  }
@@ -435,6 +502,9 @@ function openPicker(event) {
435
502
  if (settings.inline) {
436
503
  return;
437
504
  }
505
+ if (currentEl && currentEl !== event.target) {
506
+ closePicker();
507
+ }
438
508
  attachVirtualInstance(event.target);
439
509
  currentEl = event.target;
440
510
  oldColor = currentEl.value;
@@ -442,49 +512,68 @@ function openPicker(event) {
442
512
  picker.classList.add("clr-open");
443
513
  updatePickerPosition();
444
514
  setColorFromStr(oldColor);
515
+ initialFormatColor = colorValue.value;
445
516
  if (settings.focusInput || settings.selectInput) {
446
517
  colorValue.focus({
447
518
  preventScroll: true
448
519
  });
449
- colorValue.setSelectionRange(currentEl.selectionStart, currentEl.selectionEnd);
520
+ let selectionStart = 0;
521
+ let selectionEnd = 0;
522
+ try {
523
+ selectionStart = currentEl.selectionStart;
524
+ selectionEnd = currentEl.selectionEnd;
525
+ } catch {}
526
+ colorValue.setSelectionRange(selectionStart, selectionEnd);
450
527
  }
451
528
  if (settings.selectInput) {
452
529
  colorValue.select();
453
530
  }
454
- if (keyboardNav || settings.swatchesOnly) {
455
- getFocusableElements().shift().focus();
531
+ if (settings.swatchesOnly) {
532
+ const firstFocusable = getFocusableElements().shift();
533
+ if (firstFocusable) {
534
+ firstFocusable.focus();
535
+ }
456
536
  }
457
537
  currentEl.dispatchEvent(new Event("open", {
458
538
  bubbles: false
459
539
  }));
460
540
  }
461
541
 
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
542
+ function updateColorAreaDims() {
543
+ const rect = colorArea.getBoundingClientRect();
544
+ colorAreaDims = {
545
+ width: rect.width || colorArea.offsetWidth,
546
+ height: rect.height || colorArea.offsetHeight,
547
+ x: rect.x + window.scrollX,
548
+ y: rect.y + window.scrollY
475
549
  };
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;
550
+ }
551
+
552
+ function updatePickerPosition() {
553
+ if (!picker || !currentEl && !settings.inline) {
554
+ return;
482
555
  }
483
556
  if (!settings.inline) {
557
+ const parent = container;
558
+ const scrollX = window.scrollX;
559
+ const scrollY = window.scrollY;
560
+ const pickerWidth = picker.offsetWidth;
561
+ const pickerHeight = picker.offsetHeight;
562
+ const reposition = {
563
+ left: false,
564
+ top: false
565
+ };
484
566
  const coords = currentEl.getBoundingClientRect();
485
- let left = coords.x;
567
+ let left = scrollX + coords.x;
486
568
  let top = scrollY + coords.y + coords.height + settings.margin;
487
569
  if (parent) {
570
+ const parentStyle = window.getComputedStyle(parent);
571
+ const parentMarginTop = parseFloat(parentStyle.marginTop);
572
+ const parentBorderTop = parseFloat(parentStyle.borderTopWidth);
573
+ const parentBorderLeft = parseFloat(parentStyle.borderLeftWidth);
574
+ const offset = parent.getBoundingClientRect();
575
+ offset.x += parentBorderLeft + scrollX;
576
+ offset.y += parentBorderTop + scrollY;
488
577
  left -= offset.x;
489
578
  top -= offset.y;
490
579
  if (left + pickerWidth > parent.clientWidth) {
@@ -497,9 +586,10 @@ function updatePickerPosition() {
497
586
  reposition.top = true;
498
587
  }
499
588
  }
589
+ left += parent.scrollLeft;
500
590
  top += parent.scrollTop;
501
591
  } else {
502
- if (left + pickerWidth > document.documentElement.clientWidth) {
592
+ if (left - scrollX + pickerWidth > document.documentElement.clientWidth) {
503
593
  left += coords.width - pickerWidth;
504
594
  reposition.left = true;
505
595
  }
@@ -514,25 +604,18 @@ function updatePickerPosition() {
514
604
  picker.classList.toggle("clr-top", reposition.top);
515
605
  picker.style.left = `${left}px`;
516
606
  picker.style.top = `${top}px`;
517
- offset.x += picker.offsetLeft;
518
- offset.y += picker.offsetTop;
519
607
  }
520
- colorAreaDims = {
521
- width: colorArea.offsetWidth,
522
- height: colorArea.offsetHeight,
523
- x: colorArea.offsetLeft + offset.x,
524
- y: colorArea.offsetTop + offset.y
525
- };
608
+ updateColorAreaDims();
526
609
  }
527
610
 
528
611
  function wrapFields(selector) {
529
- const fields = selector instanceof HTMLElement ? [ selector ] : Array.isArray(selector) ? selector : [ ...document.querySelectorAll(selector) ];
612
+ const fields = selector instanceof HTMLElement ? [ selector ] : Array.isArray(selector) || selector instanceof NodeList ? [ ...selector ] : typeof selector === "string" ? [ ...document.querySelectorAll(selector) ] : [];
530
613
  fields.forEach(wrapColorField);
531
614
  }
532
615
 
533
616
  function wrapColorField(field) {
534
- const parentNode = field.parentNode;
535
- if (!parentNode.classList.contains("clr-field")) {
617
+ const parentNode = field?.parentNode;
618
+ if (parentNode && parentNode.classList && !parentNode.classList.contains("clr-field")) {
536
619
  const wrapper = document.createElement("div");
537
620
  let classes = "clr-field";
538
621
  if (settings.rtl || field.classList.contains("clr-rtl")) {
@@ -548,7 +631,7 @@ function wrapColorField(field) {
548
631
 
549
632
  function updateColorPreview(event) {
550
633
  const parent = event.target.parentNode;
551
- if (parent.classList.contains("clr-field")) {
634
+ if (parent && parent.classList && parent.classList.contains("clr-field")) {
552
635
  parent.style.color = event.target.value;
553
636
  }
554
637
  }
@@ -564,7 +647,16 @@ function closePicker(revert) {
564
647
  bubbles: true
565
648
  }));
566
649
  }
650
+ } else {
651
+ const value = colorValue.value;
652
+ if (value !== initialFormatColor) {
653
+ if (value !== "") {
654
+ setColorFromStr(value);
655
+ }
656
+ pickColor(value === "" ? "" : colorValue.value);
657
+ }
567
658
  }
659
+ currentEl = undefined;
568
660
  setTimeout(() => {
569
661
  if (oldColor !== prevEl.value) {
570
662
  prevEl.dispatchEvent(new Event("change", {
@@ -573,9 +665,7 @@ function closePicker(revert) {
573
665
  }
574
666
  });
575
667
  picker.classList.remove("clr-open");
576
- if (hasInstance) {
577
- resetVirtualInstance();
578
- }
668
+ resetVirtualInstance();
579
669
  prevEl.dispatchEvent(new Event("close", {
580
670
  bubbles: false
581
671
  }));
@@ -584,7 +674,6 @@ function closePicker(revert) {
584
674
  preventScroll: true
585
675
  });
586
676
  }
587
- currentEl = undefined;
588
677
  }
589
678
  }
590
679
 
@@ -603,12 +692,13 @@ function setColorFromStr(str) {
603
692
  }
604
693
 
605
694
  function getColorFormatFromStr(str) {
606
- const fmt = str.slice(0, 3).toLowerCase();
695
+ const fmt = str.trim().slice(0, 3).toLowerCase();
607
696
  return fmt === "rgb" || fmt === "hsl" ? fmt : "hex";
608
697
  }
609
698
 
610
699
  function pickColor(color) {
611
700
  color = color ?? colorValue.value;
701
+ initialFormatColor = colorValue.value;
612
702
  if (currentEl) {
613
703
  currentEl.value = color;
614
704
  currentEl.dispatchEvent(new Event("input", {
@@ -644,13 +734,7 @@ function updateMarkerA11yLabel(saturation, value) {
644
734
  }
645
735
 
646
736
  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);
737
+ setMarkerPosition(event.pageX - colorAreaDims.x, event.pageY - colorAreaDims.y);
654
738
  event.preventDefault();
655
739
  event.stopPropagation();
656
740
  }
@@ -667,7 +751,9 @@ function setMarkerPosition(x, y) {
667
751
  colorMarker.style.left = `${x}px`;
668
752
  colorMarker.style.top = `${y}px`;
669
753
  setColorAtPosition(x, y);
670
- colorMarker.focus();
754
+ colorMarker.focus({
755
+ preventScroll: true
756
+ });
671
757
  }
672
758
 
673
759
  function updateColor(rgba = {}, hsva = {}) {
@@ -679,12 +765,6 @@ function updateColor(rgba = {}, hsva = {}) {
679
765
  alphaMarker.parentNode.style.color = opaqueHex;
680
766
  alphaMarker.style.color = hex;
681
767
  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
768
  if (format === "mixed") {
689
769
  format = currentColor.a === 1 ? "hex" : "rgb";
690
770
  } else if (format === "auto") {
@@ -703,7 +783,10 @@ function updateColor(rgba = {}, hsva = {}) {
703
783
  colorValue.value = HSLAToStr(HSVAtoHSLA(currentColor));
704
784
  break;
705
785
  }
706
- document.querySelector(`.clr-format [value="${format}"]`).checked = true;
786
+ const formatRadio = picker.querySelector(`.clr-format [value="${format}"]`);
787
+ if (formatRadio) {
788
+ formatRadio.checked = true;
789
+ }
707
790
  }
708
791
 
709
792
  function setHue() {
@@ -725,15 +808,16 @@ function setAlpha() {
725
808
  }
726
809
 
727
810
  function getFocusableElements() {
728
- return [ ...picker.querySelectorAll("input, button") ].filter(node => !!node.offsetWidth);
811
+ return [ ...picker.querySelectorAll("input, button, [tabindex]") ].filter(node => !!node.offsetWidth && (node.type !== "radio" || node.checked));
729
812
  }
730
813
 
731
814
  function init() {
732
815
  container = undefined;
733
816
  picker = document.createElement("div");
734
817
  picker.setAttribute("id", "clr-picker");
818
+ picker.setAttribute("role", "dialog");
735
819
  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 `;
820
+ picker.innerHTML = `\n <input id="clr-color-value" name="clr-color-value" class="clr-color" type="text" value="" spellcheck="false">\n <div id="clr-color-area" class="clr-gradient" role="application">\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">\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">\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></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 </fieldset>\n </div>\n <div id="clr-swatches" class="clr-swatches"></div>\n <button type="button" id="clr-clear" class="clr-clear"></button>\n <div id="clr-color-preview" class="clr-preview">\n <button type="button" id="clr-close" class="clr-close"></button>\n </div>\n <span id="clr-open-label" hidden></span>\n <span id="clr-swatch-label" hidden></span>\n `;
737
821
  document.body.appendChild(picker);
738
822
  colorArea = getEl("clr-color-area");
739
823
  colorMarker = getEl("clr-color-marker");
@@ -745,36 +829,36 @@ function init() {
745
829
  hueMarker = getEl("clr-hue-marker");
746
830
  alphaSlider = getEl("clr-alpha-slider");
747
831
  alphaMarker = getEl("clr-alpha-marker");
832
+ colorValue.setAttribute("aria-label", settings.a11y.input);
833
+ colorArea.setAttribute("aria-label", settings.a11y.instruction);
834
+ hueSlider.setAttribute("aria-label", settings.a11y.hueSlider);
835
+ alphaSlider.setAttribute("aria-label", settings.a11y.alphaSlider);
836
+ clearButton.setAttribute("aria-label", settings.a11y.clear);
837
+ clearButton.textContent = settings.clearLabel;
838
+ closeButton.setAttribute("aria-label", settings.a11y.close);
839
+ closeButton.textContent = settings.closeLabel;
840
+ picker.querySelector(".clr-format legend").textContent = settings.a11y.format;
841
+ getEl("clr-open-label").textContent = settings.a11y.open;
842
+ getEl("clr-swatch-label").textContent = settings.a11y.swatch;
748
843
  bindFields(settings.el);
749
844
  wrapFields(settings.el);
750
- addListener(picker, "mousedown", event => {
751
- picker.classList.remove("clr-keyboard-nav");
845
+ addListener(picker, "pointerdown", event => {
752
846
  event.stopPropagation();
753
847
  });
754
- addListener(colorArea, "mousedown", () => {
755
- addListener(document, "mousemove", moveMarker);
848
+ addListener(colorArea, "pointerdown", () => {
849
+ updateColorAreaDims();
850
+ addListener(document, "pointermove", moveMarker);
756
851
  });
757
852
  addListener(colorArea, "contextmenu", event => {
758
853
  event.preventDefault();
759
854
  });
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
855
  addListener(colorValue, "change", () => {
774
856
  const value = colorValue.value;
775
857
  if (currentEl || settings.inline) {
776
- const color = value === "" ? value : setColorFromStr(value);
777
- pickColor(color);
858
+ if (value !== "") {
859
+ setColorFromStr(value);
860
+ }
861
+ pickColor(value === "" ? "" : colorValue.value);
778
862
  }
779
863
  });
780
864
  addListener(clearButton, "click", () => {
@@ -797,50 +881,48 @@ function init() {
797
881
  closePicker();
798
882
  }
799
883
  });
800
- addListener(document, "mouseup", () => {
801
- document.removeEventListener("mousemove", moveMarker);
802
- });
803
- addListener(document, "touchend", () => {
804
- document.removeEventListener("touchmove", moveMarker);
884
+ addListener(document, "pointerup", () => {
885
+ document.removeEventListener("pointermove", moveMarker);
805
886
  });
806
- addListener(document, "mousedown", () => {
807
- keyboardNav = false;
808
- picker.classList.remove("clr-keyboard-nav");
887
+ addListener(document, "pointerdown", () => {
809
888
  closePicker();
810
889
  });
811
890
  addListener(document, "keydown", event => {
812
891
  const key = event.key;
813
892
  const target = event.target;
814
893
  const shiftKey = event.shiftKey;
815
- const navKeys = [ "Tab", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight" ];
816
894
  if (key === "Escape") {
817
- closePicker(true);
895
+ if (currentEl) {
896
+ closePicker(true);
897
+ }
818
898
  return;
819
899
  } else if (key === "Enter" && target.tagName !== "BUTTON") {
820
- closePicker();
900
+ if (currentEl) {
901
+ event.preventDefault();
902
+ closePicker();
903
+ }
821
904
  return;
822
- } else if (navKeys.includes(key)) {
823
- keyboardNav = true;
824
- picker.classList.add("clr-keyboard-nav");
825
905
  }
826
- if (key === "Tab" && target.matches(".clr-picker *")) {
906
+ if (key === "Tab" && target instanceof Element && target.matches(".clr-picker *")) {
827
907
  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();
908
+ if (focusables.length > 1) {
909
+ const firstFocusable = focusables[0];
910
+ const lastFocusable = focusables[focusables.length - 1];
911
+ if (shiftKey && target === firstFocusable) {
912
+ lastFocusable.focus();
913
+ event.preventDefault();
914
+ } else if (!shiftKey && target === lastFocusable) {
915
+ firstFocusable.focus();
916
+ event.preventDefault();
917
+ }
918
+ } else if (focusables.length === 1) {
835
919
  event.preventDefault();
836
920
  }
837
921
  }
838
922
  });
839
923
  addListener(document, "click", ".clr-field button", event => {
840
- if (hasInstance) {
841
- resetVirtualInstance();
842
- }
843
- event.target.nextElementSibling.dispatchEvent(new Event("click", {
924
+ resetVirtualInstance();
925
+ event.target.nextElementSibling?.dispatchEvent(new Event("click", {
844
926
  bubbles: true
845
927
  }));
846
928
  });