@bornova/colorpicker 1.0.1 → 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.1 */
1
+ /* @bornova/colorpicker v1.1.0 */
2
2
  const settings = {
3
3
  el: "[data-colorpicker]",
4
4
  parent: "body",
@@ -41,7 +41,12 @@ let ctx;
41
41
  function getCtx() {
42
42
  if (!ctx && typeof document !== "undefined") {
43
43
  try {
44
- ctx = document.createElement("canvas").getContext("2d");
44
+ const canvas = document.createElement("canvas");
45
+ canvas.width = 1;
46
+ canvas.height = 1;
47
+ ctx = canvas.getContext("2d", {
48
+ willReadFrequently: true
49
+ });
45
50
  } catch {}
46
51
  }
47
52
  return ctx;
@@ -103,9 +108,9 @@ function RGBAtoHSVA(rgba) {
103
108
  hue = 4 + (red - green) / chroma;
104
109
  }
105
110
  }
106
- hue = Math.floor(hue * 60);
111
+ hue = Math.round(hue * 60);
107
112
  return {
108
- h: hue < 0 ? hue + 360 : hue,
113
+ h: (hue + 360) % 360,
109
114
  s: Math.round(saturation * 100),
110
115
  v: Math.round(value * 100),
111
116
  a: rgba.a
@@ -113,73 +118,62 @@ function RGBAtoHSVA(rgba) {
113
118
  }
114
119
 
115
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
+ }
116
129
  const regex = /^((rgba)|rgb)[\D]+([\d.]+)[\D]+([\d.]+)[\D]+([\d.]+)[\D]*?([\d.]+|$)/i;
117
130
  const canvasCtx = getCtx();
118
- let match, rgba;
131
+ let normalized = str.trim();
119
132
  if (canvasCtx) {
120
133
  canvasCtx.fillStyle = "#000";
121
134
  canvasCtx.fillStyle = str;
122
- match = regex.exec(canvasCtx.fillStyle);
123
- if (match) {
124
- rgba = {
125
- r: Number(match[3]),
126
- g: Number(match[4]),
127
- b: Number(match[5]),
128
- a: match[6] === "" ? 1 : Number(match[6])
129
- };
130
- } else {
131
- match = canvasCtx.fillStyle.replace("#", "").match(/.{2}/g).map(h => parseInt(h, 16));
132
- rgba = {
133
- r: match[0],
134
- g: match[1],
135
- b: match[2],
136
- a: match[3] !== undefined ? Number((match[3] / 255).toFixed(2)) : 1
137
- };
135
+ normalized = canvasCtx.fillStyle;
136
+ }
137
+ const match = regex.exec(normalized);
138
+ if (match) {
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))))
144
+ };
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("");
138
150
  }
139
- } else {
140
- match = regex.exec(str);
141
- if (match) {
142
- rgba = {
143
- r: Number(match[3]),
144
- g: Number(match[4]),
145
- b: Number(match[5]),
146
- a: match[6] === "" ? 1 : Number(match[6])
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
156
+ };
157
+ }
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))
147
168
  };
148
- } else {
149
- const hex = str.replace("#", "");
150
- if (hex.length === 3 || hex.length === 4) {
151
- const r = parseInt(hex[0] + hex[0], 16);
152
- const g = parseInt(hex[1] + hex[1], 16);
153
- const b = parseInt(hex[2] + hex[2], 16);
154
- const a = hex.length === 4 ? Number((parseInt(hex[3] + hex[3], 16) / 255).toFixed(2)) : 1;
155
- rgba = {
156
- r: r,
157
- g: g,
158
- b: b,
159
- a: a
160
- };
161
- } else if (hex.length === 6 || hex.length === 8) {
162
- const r = parseInt(hex.slice(0, 2), 16);
163
- const g = parseInt(hex.slice(2, 4), 16);
164
- const b = parseInt(hex.slice(4, 6), 16);
165
- const a = hex.length === 8 ? Number((parseInt(hex.slice(6, 8), 16) / 255).toFixed(2)) : 1;
166
- rgba = {
167
- r: r,
168
- g: g,
169
- b: b,
170
- a: a
171
- };
172
- } else {
173
- rgba = {
174
- r: 0,
175
- g: 0,
176
- b: 0,
177
- a: 1
178
- };
179
- }
180
- }
169
+ } catch {}
181
170
  }
182
- return rgba;
171
+ return {
172
+ r: 0,
173
+ g: 0,
174
+ b: 0,
175
+ a: 1
176
+ };
183
177
  }
184
178
 
185
179
  function RGBAToHex(rgba) {
@@ -208,7 +202,7 @@ function getEl(id) {
208
202
  function addListener(context, type, selector, fn) {
209
203
  if (typeof selector === "string") {
210
204
  context.addEventListener(type, event => {
211
- if (event.target.matches(selector)) {
205
+ if (event.target instanceof Element && event.target.matches(selector)) {
212
206
  fn.call(event.target, event);
213
207
  }
214
208
  });
@@ -237,7 +231,7 @@ const currentColor = {
237
231
  a: 1
238
232
  };
239
233
 
240
- 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 = {};
241
235
 
242
236
  const instances = {};
243
237
 
@@ -245,7 +239,9 @@ let currentInstanceId = "";
245
239
 
246
240
  let defaultInstance = {};
247
241
 
248
- let hasInstance = false;
242
+ const boundSelectors = new Set;
243
+
244
+ const boundElements = new WeakSet;
249
245
 
250
246
  const MARKER_MOVEMENTS = {
251
247
  ArrowUp: [ 0, -1 ],
@@ -255,28 +251,28 @@ const MARKER_MOVEMENTS = {
255
251
  };
256
252
 
257
253
  function configure(options) {
258
- if (typeof options !== "object") {
254
+ if (!options || typeof options !== "object") {
259
255
  return;
260
256
  }
261
257
  for (const key of Object.keys(options)) {
262
258
  switch (key) {
263
259
  case "el":
264
260
  bindFields(options.el);
265
- if (options.wrap !== false) {
261
+ if (options.wrap !== false && (options.wrap || settings.wrap)) {
266
262
  wrapFields(options.el);
267
263
  }
268
264
  break;
269
265
 
270
266
  case "parent":
271
- container = options.parent instanceof HTMLElement ? options.parent : document.querySelector(options.parent);
272
- if (container) {
273
- container.appendChild(picker);
274
- settings.parent = options.parent;
275
- if (container === document.body) {
276
- 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;
277
273
  }
274
+ break;
278
275
  }
279
- break;
280
276
 
281
277
  case "themeMode":
282
278
  settings.themeMode = options.themeMode;
@@ -285,14 +281,22 @@ function configure(options) {
285
281
  }
286
282
 
287
283
  case "theme":
288
- if (options.theme) {
289
- settings.theme = options.theme;
290
- }
291
- picker.className = `clr-picker clr-${settings.theme} clr-${settings.themeMode}`;
292
- if (settings.inline) {
293
- 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;
294
299
  }
295
- break;
296
300
 
297
301
  case "rtl":
298
302
  settings.rtl = !!options.rtl;
@@ -309,8 +313,9 @@ function configure(options) {
309
313
  }
310
314
 
311
315
  case "wrap":
312
- if (options.el && options.wrap) {
313
- wrapFields(options.el);
316
+ settings.wrap = !!options.wrap;
317
+ if (!("el" in options) && options.wrap) {
318
+ wrapFields(settings.el);
314
319
  }
315
320
  break;
316
321
 
@@ -356,29 +361,16 @@ function configure(options) {
356
361
  case "inline":
357
362
  settings.inline = !!options.inline;
358
363
  picker.setAttribute("data-inline", settings.inline);
359
- if (settings.inline) {
360
- const defaultColor = options.defaultColor || settings.defaultColor;
361
- currentFormat = getColorFormatFromStr(defaultColor);
362
- updatePickerPosition();
363
- setColorFromStr(defaultColor);
364
- }
365
364
  break;
366
365
 
367
366
  case "clearButton":
368
- {
369
- if (typeof options.clearButton === "object" && options.clearButton.label) {
370
- settings.clearLabel = options.clearButton.label;
371
- clearButton.innerHTML = settings.clearLabel;
372
- }
373
- const show = typeof options.clearButton === "object" ? options.clearButton.show : options.clearButton;
374
- settings.clearButton = !!show;
375
- clearButton.style.display = settings.clearButton ? "block" : "none";
376
- break;
377
- }
367
+ settings.clearButton = !!options.clearButton;
368
+ clearButton.style.display = settings.clearButton ? "block" : "none";
369
+ break;
378
370
 
379
371
  case "clearLabel":
380
372
  settings.clearLabel = options.clearLabel;
381
- clearButton.innerHTML = settings.clearLabel;
373
+ clearButton.textContent = settings.clearLabel;
382
374
  break;
383
375
 
384
376
  case "closeButton":
@@ -392,14 +384,14 @@ function configure(options) {
392
384
 
393
385
  case "closeLabel":
394
386
  settings.closeLabel = options.closeLabel;
395
- closeButton.innerHTML = settings.closeLabel;
387
+ closeButton.textContent = settings.closeLabel;
396
388
  break;
397
389
 
398
390
  case "a11y":
399
391
  {
400
392
  const labels = options.a11y;
401
393
  let update = false;
402
- if (typeof labels === "object") {
394
+ if (labels && typeof labels === "object") {
403
395
  for (const label of Object.keys(labels)) {
404
396
  if (labels[label] && settings.a11y[label]) {
405
397
  settings.a11y[label] = labels[label];
@@ -410,14 +402,19 @@ function configure(options) {
410
402
  if (update) {
411
403
  const openLabel = getEl("clr-open-label");
412
404
  const swatchLabel = getEl("clr-swatch-label");
413
- openLabel.innerHTML = settings.a11y.open;
414
- 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
+ }
415
411
  closeButton.setAttribute("aria-label", settings.a11y.close);
416
412
  clearButton.setAttribute("aria-label", settings.a11y.clear);
417
413
  hueSlider.setAttribute("aria-label", settings.a11y.hueSlider);
418
414
  alphaSlider.setAttribute("aria-label", settings.a11y.alphaSlider);
419
415
  colorValue.setAttribute("aria-label", settings.a11y.input);
420
416
  colorArea.setAttribute("aria-label", settings.a11y.instruction);
417
+ updateMarkerA11yLabel(currentColor.s, currentColor.v);
421
418
  }
422
419
  break;
423
420
  }
@@ -426,46 +423,52 @@ function configure(options) {
426
423
  settings[key] = options[key];
427
424
  }
428
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
+ }
429
435
  }
430
436
 
431
437
  function setVirtualInstance(selector, options) {
432
- if (typeof selector === "string" && typeof options === "object") {
438
+ if (typeof selector === "string" && options && typeof options === "object") {
433
439
  instances[selector] = options;
434
- hasInstance = true;
435
440
  }
436
441
  }
437
442
 
438
443
  function removeVirtualInstance(selector) {
439
444
  delete instances[selector];
440
- if (Object.keys(instances).length === 0) {
441
- hasInstance = false;
442
- if (selector === currentInstanceId) {
443
- resetVirtualInstance();
444
- }
445
+ if (selector === currentInstanceId) {
446
+ resetVirtualInstance();
445
447
  }
446
448
  }
447
449
 
448
450
  function attachVirtualInstance(element) {
449
- if (hasInstance) {
450
- const unsupportedOptions = [ "el", "wrap", "rtl", "inline", "defaultColor", "a11y" ];
451
- for (const selector of Object.keys(instances)) {
452
- const options = instances[selector];
453
- if (element.matches(selector)) {
454
- currentInstanceId = selector;
455
- defaultInstance = {};
456
- const clonedOptions = {
457
- ...options
458
- };
459
- unsupportedOptions.forEach(option => delete clonedOptions[option]);
460
- for (const option of Object.keys(clonedOptions)) {
461
- defaultInstance[option] = Array.isArray(settings[option]) ? [ ...settings[option] ] : settings[option];
462
- if (option === "clearButton") {
463
- defaultInstance["clearLabel"] = settings.clearLabel;
464
- }
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;
465
468
  }
466
- configure(clonedOptions);
467
- break;
468
469
  }
470
+ configure(clonedOptions);
471
+ break;
469
472
  }
470
473
  }
471
474
  }
@@ -479,13 +482,17 @@ function resetVirtualInstance() {
479
482
  }
480
483
 
481
484
  function bindFields(selector) {
482
- const fields = selector instanceof HTMLElement ? [ selector ] : selector;
485
+ const fields = selector instanceof HTMLElement ? [ selector ] : selector instanceof NodeList ? [ ...selector ] : selector;
483
486
  if (Array.isArray(fields)) {
484
487
  fields.forEach(field => {
485
- addListener(field, "click", openPicker);
486
- 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
+ }
487
493
  });
488
- } else {
494
+ } else if (typeof fields === "string" && !boundSelectors.has(fields)) {
495
+ boundSelectors.add(fields);
489
496
  addListener(document, "click", fields, openPicker);
490
497
  addListener(document, "input", fields, updateColorPreview);
491
498
  }
@@ -495,6 +502,9 @@ function openPicker(event) {
495
502
  if (settings.inline) {
496
503
  return;
497
504
  }
505
+ if (currentEl && currentEl !== event.target) {
506
+ closePicker();
507
+ }
498
508
  attachVirtualInstance(event.target);
499
509
  currentEl = event.target;
500
510
  oldColor = currentEl.value;
@@ -502,6 +512,7 @@ function openPicker(event) {
502
512
  picker.classList.add("clr-open");
503
513
  updatePickerPosition();
504
514
  setColorFromStr(oldColor);
515
+ initialFormatColor = colorValue.value;
505
516
  if (settings.focusInput || settings.selectInput) {
506
517
  colorValue.focus({
507
518
  preventScroll: true
@@ -517,7 +528,7 @@ function openPicker(event) {
517
528
  if (settings.selectInput) {
518
529
  colorValue.select();
519
530
  }
520
- if (keyboardNav || settings.swatchesOnly) {
531
+ if (settings.swatchesOnly) {
521
532
  const firstFocusable = getFocusableElements().shift();
522
533
  if (firstFocusable) {
523
534
  firstFocusable.focus();
@@ -528,32 +539,41 @@ function openPicker(event) {
528
539
  }));
529
540
  }
530
541
 
531
- function updatePickerPosition() {
532
- const parent = container;
533
- const scrollY = window.scrollY;
534
- const pickerWidth = picker.offsetWidth;
535
- const pickerHeight = picker.offsetHeight;
536
- const reposition = {
537
- left: false,
538
- top: false
539
- };
540
- let parentStyle, parentMarginTop, parentBorderTop;
541
- let offset = {
542
- x: 0,
543
- 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
544
549
  };
545
- if (parent) {
546
- parentStyle = window.getComputedStyle(parent);
547
- parentMarginTop = parseFloat(parentStyle.marginTop);
548
- parentBorderTop = parseFloat(parentStyle.borderTopWidth);
549
- offset = parent.getBoundingClientRect();
550
- offset.y += parentBorderTop + scrollY;
550
+ }
551
+
552
+ function updatePickerPosition() {
553
+ if (!picker || !currentEl && !settings.inline) {
554
+ return;
551
555
  }
552
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
+ };
553
566
  const coords = currentEl.getBoundingClientRect();
554
- let left = coords.x;
567
+ let left = scrollX + coords.x;
555
568
  let top = scrollY + coords.y + coords.height + settings.margin;
556
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;
557
577
  left -= offset.x;
558
578
  top -= offset.y;
559
579
  if (left + pickerWidth > parent.clientWidth) {
@@ -566,9 +586,10 @@ function updatePickerPosition() {
566
586
  reposition.top = true;
567
587
  }
568
588
  }
589
+ left += parent.scrollLeft;
569
590
  top += parent.scrollTop;
570
591
  } else {
571
- if (left + pickerWidth > document.documentElement.clientWidth) {
592
+ if (left - scrollX + pickerWidth > document.documentElement.clientWidth) {
572
593
  left += coords.width - pickerWidth;
573
594
  reposition.left = true;
574
595
  }
@@ -583,25 +604,18 @@ function updatePickerPosition() {
583
604
  picker.classList.toggle("clr-top", reposition.top);
584
605
  picker.style.left = `${left}px`;
585
606
  picker.style.top = `${top}px`;
586
- offset.x += picker.offsetLeft;
587
- offset.y += picker.offsetTop;
588
607
  }
589
- colorAreaDims = {
590
- width: colorArea.offsetWidth,
591
- height: colorArea.offsetHeight,
592
- x: colorArea.offsetLeft + offset.x,
593
- y: colorArea.offsetTop + offset.y
594
- };
608
+ updateColorAreaDims();
595
609
  }
596
610
 
597
611
  function wrapFields(selector) {
598
- 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) ] : [];
599
613
  fields.forEach(wrapColorField);
600
614
  }
601
615
 
602
616
  function wrapColorField(field) {
603
- const parentNode = field.parentNode;
604
- if (!parentNode.classList.contains("clr-field")) {
617
+ const parentNode = field?.parentNode;
618
+ if (parentNode && parentNode.classList && !parentNode.classList.contains("clr-field")) {
605
619
  const wrapper = document.createElement("div");
606
620
  let classes = "clr-field";
607
621
  if (settings.rtl || field.classList.contains("clr-rtl")) {
@@ -617,7 +631,7 @@ function wrapColorField(field) {
617
631
 
618
632
  function updateColorPreview(event) {
619
633
  const parent = event.target.parentNode;
620
- if (parent.classList.contains("clr-field")) {
634
+ if (parent && parent.classList && parent.classList.contains("clr-field")) {
621
635
  parent.style.color = event.target.value;
622
636
  }
623
637
  }
@@ -635,13 +649,14 @@ function closePicker(revert) {
635
649
  }
636
650
  } else {
637
651
  const value = colorValue.value;
638
- if (value === "") {
639
- pickColor("");
640
- } else {
641
- setColorFromStr(value);
642
- pickColor(colorValue.value);
652
+ if (value !== initialFormatColor) {
653
+ if (value !== "") {
654
+ setColorFromStr(value);
655
+ }
656
+ pickColor(value === "" ? "" : colorValue.value);
643
657
  }
644
658
  }
659
+ currentEl = undefined;
645
660
  setTimeout(() => {
646
661
  if (oldColor !== prevEl.value) {
647
662
  prevEl.dispatchEvent(new Event("change", {
@@ -650,9 +665,7 @@ function closePicker(revert) {
650
665
  }
651
666
  });
652
667
  picker.classList.remove("clr-open");
653
- if (hasInstance) {
654
- resetVirtualInstance();
655
- }
668
+ resetVirtualInstance();
656
669
  prevEl.dispatchEvent(new Event("close", {
657
670
  bubbles: false
658
671
  }));
@@ -661,7 +674,6 @@ function closePicker(revert) {
661
674
  preventScroll: true
662
675
  });
663
676
  }
664
- currentEl = undefined;
665
677
  }
666
678
  }
667
679
 
@@ -680,12 +692,13 @@ function setColorFromStr(str) {
680
692
  }
681
693
 
682
694
  function getColorFormatFromStr(str) {
683
- const fmt = str.slice(0, 3).toLowerCase();
695
+ const fmt = str.trim().slice(0, 3).toLowerCase();
684
696
  return fmt === "rgb" || fmt === "hsl" ? fmt : "hex";
685
697
  }
686
698
 
687
699
  function pickColor(color) {
688
700
  color = color ?? colorValue.value;
701
+ initialFormatColor = colorValue.value;
689
702
  if (currentEl) {
690
703
  currentEl.value = color;
691
704
  currentEl.dispatchEvent(new Event("input", {
@@ -721,13 +734,7 @@ function updateMarkerA11yLabel(saturation, value) {
721
734
  }
722
735
 
723
736
  function moveMarker(event) {
724
- const touch = event.changedTouches?.[0];
725
- let x = (touch?.pageX ?? event.pageX) - colorAreaDims.x;
726
- let y = (touch?.pageY ?? event.pageY) - colorAreaDims.y;
727
- if (container) {
728
- y += container.scrollTop;
729
- }
730
- setMarkerPosition(x, y);
737
+ setMarkerPosition(event.pageX - colorAreaDims.x, event.pageY - colorAreaDims.y);
731
738
  event.preventDefault();
732
739
  event.stopPropagation();
733
740
  }
@@ -744,7 +751,9 @@ function setMarkerPosition(x, y) {
744
751
  colorMarker.style.left = `${x}px`;
745
752
  colorMarker.style.top = `${y}px`;
746
753
  setColorAtPosition(x, y);
747
- colorMarker.focus();
754
+ colorMarker.focus({
755
+ preventScroll: true
756
+ });
748
757
  }
749
758
 
750
759
  function updateColor(rgba = {}, hsva = {}) {
@@ -756,12 +765,6 @@ function updateColor(rgba = {}, hsva = {}) {
756
765
  alphaMarker.parentNode.style.color = opaqueHex;
757
766
  alphaMarker.style.color = hex;
758
767
  colorPreview.style.color = hex;
759
- colorArea.style.display = "none";
760
- colorArea.offsetHeight;
761
- colorArea.style.display = "";
762
- alphaMarker.nextElementSibling.style.display = "none";
763
- alphaMarker.nextElementSibling.offsetHeight;
764
- alphaMarker.nextElementSibling.style.display = "";
765
768
  if (format === "mixed") {
766
769
  format = currentColor.a === 1 ? "hex" : "rgb";
767
770
  } else if (format === "auto") {
@@ -780,7 +783,10 @@ function updateColor(rgba = {}, hsva = {}) {
780
783
  colorValue.value = HSLAToStr(HSVAtoHSLA(currentColor));
781
784
  break;
782
785
  }
783
- 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
+ }
784
790
  }
785
791
 
786
792
  function setHue() {
@@ -802,15 +808,16 @@ function setAlpha() {
802
808
  }
803
809
 
804
810
  function getFocusableElements() {
805
- 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));
806
812
  }
807
813
 
808
814
  function init() {
809
815
  container = undefined;
810
816
  picker = document.createElement("div");
811
817
  picker.setAttribute("id", "clr-picker");
818
+ picker.setAttribute("role", "dialog");
812
819
  picker.className = "clr-picker";
813
- 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 `;
814
821
  document.body.appendChild(picker);
815
822
  colorArea = getEl("clr-color-area");
816
823
  colorMarker = getEl("clr-color-marker");
@@ -822,36 +829,36 @@ function init() {
822
829
  hueMarker = getEl("clr-hue-marker");
823
830
  alphaSlider = getEl("clr-alpha-slider");
824
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;
825
843
  bindFields(settings.el);
826
844
  wrapFields(settings.el);
827
- addListener(picker, "mousedown", event => {
828
- picker.classList.remove("clr-keyboard-nav");
845
+ addListener(picker, "pointerdown", event => {
829
846
  event.stopPropagation();
830
847
  });
831
- addListener(colorArea, "mousedown", () => {
832
- addListener(document, "mousemove", moveMarker);
848
+ addListener(colorArea, "pointerdown", () => {
849
+ updateColorAreaDims();
850
+ addListener(document, "pointermove", moveMarker);
833
851
  });
834
852
  addListener(colorArea, "contextmenu", event => {
835
853
  event.preventDefault();
836
854
  });
837
- addListener(colorArea, "touchstart", () => {
838
- document.addEventListener("touchmove", moveMarker, {
839
- passive: false
840
- });
841
- });
842
- addListener(colorMarker, "mousedown", () => {
843
- addListener(document, "mousemove", moveMarker);
844
- });
845
- addListener(colorMarker, "touchstart", () => {
846
- document.addEventListener("touchmove", moveMarker, {
847
- passive: false
848
- });
849
- });
850
855
  addListener(colorValue, "change", () => {
851
856
  const value = colorValue.value;
852
857
  if (currentEl || settings.inline) {
853
- const color = value === "" ? value : setColorFromStr(value);
854
- pickColor(color);
858
+ if (value !== "") {
859
+ setColorFromStr(value);
860
+ }
861
+ pickColor(value === "" ? "" : colorValue.value);
855
862
  }
856
863
  });
857
864
  addListener(clearButton, "click", () => {
@@ -874,33 +881,29 @@ function init() {
874
881
  closePicker();
875
882
  }
876
883
  });
877
- addListener(document, "mouseup", () => {
878
- document.removeEventListener("mousemove", moveMarker);
879
- });
880
- addListener(document, "touchend", () => {
881
- document.removeEventListener("touchmove", moveMarker);
884
+ addListener(document, "pointerup", () => {
885
+ document.removeEventListener("pointermove", moveMarker);
882
886
  });
883
- addListener(document, "mousedown", () => {
884
- keyboardNav = false;
885
- picker.classList.remove("clr-keyboard-nav");
887
+ addListener(document, "pointerdown", () => {
886
888
  closePicker();
887
889
  });
888
890
  addListener(document, "keydown", event => {
889
891
  const key = event.key;
890
892
  const target = event.target;
891
893
  const shiftKey = event.shiftKey;
892
- const navKeys = [ "Tab", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight" ];
893
894
  if (key === "Escape") {
894
- closePicker(true);
895
+ if (currentEl) {
896
+ closePicker(true);
897
+ }
895
898
  return;
896
899
  } else if (key === "Enter" && target.tagName !== "BUTTON") {
897
- closePicker();
900
+ if (currentEl) {
901
+ event.preventDefault();
902
+ closePicker();
903
+ }
898
904
  return;
899
- } else if (navKeys.includes(key)) {
900
- keyboardNav = true;
901
- picker.classList.add("clr-keyboard-nav");
902
905
  }
903
- if (key === "Tab" && target.matches(".clr-picker *")) {
906
+ if (key === "Tab" && target instanceof Element && target.matches(".clr-picker *")) {
904
907
  const focusables = getFocusableElements();
905
908
  if (focusables.length > 1) {
906
909
  const firstFocusable = focusables[0];
@@ -918,10 +921,8 @@ function init() {
918
921
  }
919
922
  });
920
923
  addListener(document, "click", ".clr-field button", event => {
921
- if (hasInstance) {
922
- resetVirtualInstance();
923
- }
924
- event.target.nextElementSibling.dispatchEvent(new Event("click", {
924
+ resetVirtualInstance();
925
+ event.target.nextElementSibling?.dispatchEvent(new Event("click", {
925
926
  bubbles: true
926
927
  }));
927
928
  });