PageSourceSearch

https://ivaprime.com/static/js/libraries/coloris.js

js ivaprime.com collected 2026-10-03 21:37:47 UTC 36,987 bytes, 1,210 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.