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.