1/** 2 * Spotlight Card - Vanilla JavaScript 3 * A mouse-following glow effect for cards 4 * by Mahedi amin 5 * @version 2.0.0 6 * @license MIT 7 */ 8 9(function (global) { 10 "use strict"; 11 12 // Autoplay animation paths - smooth mathematical functions 13 const autoplayPaths = { 14 circle: (t) => ({ 15 x: 50 + 35 * Math.cos(t * Math.PI * 2), 16 y: 50 + 35 * Math.sin(t * Math.PI * 2), 17 }), 18 horizontal: (t) => ({ 19 x: 50 + 45 * Math.sin(t * Math.PI * 2), 20 y: 50, 21 }), 22 vertical: (t) => ({ 23 x: 50, 24 y: 50 + 45 * Math.sin(t * Math.PI * 2), 25 }), 26 diagonal: (t) => ({ 27 x: 50 + 45 * Math.sin(t * Math.PI * 2), 28 y: 50 + 45 * Math.sin(t * Math.PI * 2), 29 }), 30 figure8: (t) => ({ 31 x: 50 + 35 * Math.sin(t * Math.PI * 2), 32 y: 50 + 25 * Math.sin(t * Math.PI * 4), 33 }), 34 }; 35 36 // Speed in milliseconds for one cycle 37 const autoplaySpeeds = { 38 slow: 6000, 39 normal: 3000, 40 fast: 1500, 41 }; 42 43 /** 44 * Color conversion utilities 45 */ 46 const ColorUtils = { 47 /** 48 * Parse any color format and return RGB values 49 * @param {string} color - hex, rgb, rgba, hsl, hsla, or named color 50 * @returns {object|null} - {r, g, b} or null if invalid 51 */ 52 parseColor(color) { 53 if (!color || typeof color !== "string") return null; 54 55 color = color.trim().toLowerCase(); 56 57 // Handle hex colors 58 if (color.startsWith("#")) { 59 return this.hexToRgb(color); 60 } 61 62 // Handle rgb/rgba 63 if (color.startsWith("rgb")) { 64 const match = color.match( 65 /rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i, 66 ); 67 if (match) { 68 return { 69 r: parseInt(match[1], 10), 70 g: parseInt(match[2], 10), 71 b: parseInt(match[3], 10), 72 }; 73 } 74 } 75 76 // Handle hsl/hsla - convert to RGB first 77 if (color.startsWith("hsl")) { 78 const match = color.match( 79 /hsla?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/i, 80 ); 81 if (match) { 82 return this.hslToRgb( 83 parseFloat(match[1]), 84 parseFloat(match[2]), 85 parseFloat(match[3]), 86 ); 87 } 88 } 89 90 // Handle named colors using a temporary element 91 if (typeof document !== "undefined") { 92 const temp = document.createElement("div"); 93 temp.style.color = color; 94 document.body.appendChild(temp); 95 const computed = getComputedStyle(temp).color; 96 document.body.removeChild(temp); 97 98 const match = computed.match( 99 /rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i, 100 ); 101 if (match) { 102 return { 103 r: parseInt(match[1], 10), 104 g: parseInt(match[2], 10), 105 b: parseInt(match[3], 10), 106 }; 107 } 108 } 109 110 return null; 111 }, 112 113 /** 114 * Convert hex color to RGB 115 * @param {string} hex - hex color (#fff, #ffffff, #ffffffff) 116 * @returns {object|null} - {r, g, b} or null 117 */ 118 hexToRgb(hex) { 119 // Remove # if present 120 hex = hex.replace(/^#/, ""); 121 122 // Handle shorthand (#fff -> #ffffff) 123 if (hex.length === 3 || hex.length === 4) { 124 hex = hex 125 .split("") 126 .map((c) => c + c) 127 .join(""); 128 } 129 130 // Handle 8-digit hex (with alpha) - just use first 6 131 if (hex.length === 8) { 132 hex = hex.substring(0, 6); 133 } 134 135 if (hex.length !== 6) return null; 136 137 const num = parseInt(hex, 16); 138 if (isNaN(num)) return null; 139 140 return { 141 r: (num >> 16) & 255, 142 g: (num >> 8) & 255, 143 b: num & 255, 144 }; 145 }, 146 147 /** 148 * Convert HSL to RGB 149 * @param {number} h - hue (0-360) 150 * @param {number} s - saturation (0-100) 151 * @param {number} l - lightness (0-100) 152 * @returns {object} - {r, g, b} 153 */ 154 hslToRgb(h, s, l) { 155 s /= 100; 156 l /= 100; 157 158 const c = (1 - Math.abs(2 * l - 1)) * s; 159 const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); 160 const m = l - c / 2; 161 162 let r = 0, 163 g = 0, 164 b = 0; 165 166 if (h >= 0 && h < 60) { 167 r = c; 168 g = x; 169 b = 0; 170 } else if (h >= 60 && h < 120) { 171 r = x; 172 g = c; 173 b = 0; 174 } else if (h >= 120 && h < 180) { 175 r = 0; 176 g = c; 177 b = x; 178 } else if (h >= 180 && h < 240) { 179 r = 0; 180 g = x; 181 b = c; 182 } else if (h >= 240 && h < 300) { 183 r = x; 184 g = 0; 185 b = c; 186 } else if (h >= 300 && h < 360) { 187 r = c; 188 g = 0; 189 b = x; 190 } 191 192 return { 193 r: Math.round((r + m) * 255), 194 g: Math.round((g + m) * 255), 195 b: Math.round((b + m) * 255), 196 }; 197 }, 198 199 /** 200 * Convert RGB to HSL 201 * @param {number} r - red (0-255) 202 * @param {number} g - green (0-255) 203 * @param {number} b - blue (0-255) 204 * @returns {object} - {h, s, l} 205 */ 206 rgbToHsl(r, g, b) { 207 r /= 255; 208 g /= 255; 209 b /= 255; 210 211 const max = Math.max(r, g, b); 212 const min = Math.min(r, g, b); 213 let h = 0; 214 let s = 0; 215 const l = (max + min) / 2; 216 217 if (max !== min) { 218 const d = max - min; 219 s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 220 221 switch (max) { 222 case r: 223 h = ((g - b) / d + (g < b ? 6 : 0)) / 6; 224 break; 225 case g: 226 h = ((b - r) / d + 2) / 6; 227 break; 228 case b: 229 h = ((r - g) / d + 4) / 6; 230 break; 231 } 232 } 233 234 return { 235 h: Math.round(h * 360), 236 s: Math.round(s * 100), 237 l: Math.round(l * 100), 238 }; 239 }, 240 241 /** 242 * Convert any color to HSL CSS variable format 243 * @param {string} color - any valid CSS color 244 * @returns {string|null} - "h, s%, l%" or null 245 */ 246 toHslString(color) { 247 const rgb = this.parseColor(color); 248 if (!rgb) return null; 249 250 const hsl = this.rgbToHsl(rgb.r, rgb.g, rgb.b); 251 return `${hsl.h}, ${hsl.s}%, ${hsl.l}%`; 252 }, 253 }; 254 255 /** 256 * SpotlightCard Class 257 */ 258 class SpotlightCard { 259 constructor(element, options = {}) { 260 this.element = 261 typeof element === "string" ? document.querySelector(element) : element; 262 263 if (!this.element) { 264 console.warn("SpotlightCard: Element not found"); 265 return; 266 } 267 268 this.options = { 269 color: null, // Single color for non-gradient modes 270 gradientColors: [], // Array of colors for gradient modes 271 size: "md", 272 intensity: "medium", 273 variant: "default", 274 mode: "hover", 275 autoplay: false, 276 autoplayType: "circle",
277 autoplaySpeed: "normal", 278 disabled: false, 279 cardBg: null, // Card background color 280 cardBorder: null, // Card border color 281 cardRadius: null, // Card border radius 282 onMouseEnter: null, 283 onMouseLeave: null, 284 onMouseMove: null, 285 ...options, 286 }; 287 288 this._rafId = null; 289 this._autoplayRafId = null; 290 this._autoplayStartTime = null; 291 this._isHovering = false; 292 293 this._handleMouseMove = this._handleMouseMove.bind(this); 294 this._handleMouseEnter = this._handleMouseEnter.bind(this); 295 this._handleMouseLeave = this._handleMouseLeave.bind(this); 296 this._animateAutoplay = this._animateAutoplay.bind(this); 297 298 this._init(); 299 } 300 301 _init() { 302 this.element.classList.add("spotlight-card"); 303 304 // Sync border-radius: if not explicitly provided, read from computed style 305 // so the CSS variable matches any externally applied border-radius (e.g. Divi module settings) 306 if (!this.options.cardRadius) { 307 const computedRadius = getComputedStyle(this.element).borderRadius; 308 if (computedRadius && computedRadius !== "0px") { 309 this.options.cardRadius = computedRadius; 310 } 311 } 312 313 this._applyOptions(); 314 this._addEventListeners(); 315 316 if (this.options.autoplay) { 317 this._startAutoplay(); 318 } 319 } 320 321 /** 322 * Apply color as CSS variable in HSL format 323 * @param {string} color - The color value 324 */ 325 _applyColor(color) { 326 if (color) { 327 const hslString = ColorUtils.toHslString(color); 328 if (hslString) { 329 this.element.style.setProperty("--spotlight-color", hslString); 330 } 331 } else { 332 this.element.style.removeProperty("--spotlight-color"); 333 } 334 } 335 336 /** 337 * Apply gradient colors as CSS variables 338 * @param {Array<string>} colors - Array of color values 339 */ 340 _applyGradientColors(colors) { 341 if (!colors || colors.length === 0) { 342 // Clear all gradient color variables 343 for (let i = 1; i <= 10; i++) { 344 this.element.style.removeProperty(`--spotlight-gradient-${i}`); 345 } 346 this.element.style.removeProperty("--spotlight-gradient-count"); 347 return; 348 } 349 350 // Set the count of gradient colors 351 this.element.style.setProperty( 352 "--spotlight-gradient-count", 353 colors.length, 354 ); 355 356 // Apply each color 357 colors.forEach((color, index) => { 358 const hslString = ColorUtils.toHslString(color); 359 if (hslString) { 360 this.element.style.setProperty( 361 `--spotlight-gradient-${index + 1}`, 362 hslString, 363 ); 364 } 365 }); 366 367 // Clear any extra variables 368 for (let i = colors.length + 1; i <= 10; i++) { 369 this.element.style.removeProperty(`--spotlight-gradient-${i}`); 370 } 371 372 // Build and apply the gradient 373 this._buildGradient(colors); 374 } 375 376 /** 377 * Build gradient CSS from colors array 378 * @param {Array<string>} colors - Array of color values 379 */ 380 _buildGradient(colors) { 381 if (!colors || colors.length === 0) return; 382 383 const hslColors = colors 384 .map((c) => ColorUtils.toHslString(c)) 385 .filter(Boolean); 386 if (hslColors.length === 0) return; 387 388 // Store for use in CSS 389 this.element.setAttribute("data-gradient-colors", hslColors.join("|")); 390 } 391 392 /** 393 * Apply card styling options 394 */ 395 _applyCardStyles() { 396 const { cardBg, cardBorder, cardRadius } = this.options; 397 398 if (cardBg) { 399 this.element.style.setProperty("--spotlight-card-bg", cardBg); 400 } else { 401 this.element.style.removeProperty("--spotlight-card-bg"); 402 } 403 404 if (cardBorder) { 405 this.element.style.setProperty("--spotlight-card-border", cardBorder); 406 } else { 407 this.element.style.removeProperty("--spotlight-card-border"); 408 } 409 410 if (cardRadius) { 411 this.element.style.setProperty("--spotlight-card-radius", cardRadius); 412 } else { 413 this.element.style.removeProperty("--spotlight-card-radius"); 414 } 415 } 416 417 _applyOptions() { 418 const { 419 color, 420 gradientColors, 421 size, 422 intensity, 423 variant, 424 mode, 425 autoplay, 426 autoplayType, 427 autoplaySpeed, 428 disabled, 429 } = this.options; 430 431 // Set data attributes (using setter methods for size and intensity to handle custom values) 432 this.setSize(size); 433 this.setIntensity(intensity); 434 this.element.setAttribute("data-variant", variant); 435 this.element.setAttribute("data-mode", mode); 436 this.element.setAttribute("data-autoplay", autoplay); 437 this.element.setAttribute("data-autoplay-type", autoplayType); 438 this.element.setAttribute("data-autoplay-speed", autoplaySpeed); 439 this.element.setAttribute("data-disabled", disabled); 440 441 // Apply colors 442 this._applyColor(color); 443 this._applyGradientColors(gradientColors); 444 this._applyCardStyles(); 445 446 // Toggle classes based on variant and mode 447 this.element.classList.toggle( 448 "spotlight-card--proximity", 449 mode === "proximity", 450 ); 451 this.element.classList.toggle("spotlight-card--autoplay", autoplay); 452 this.element.classList.toggle( 453 "spotlight-card--border", 454 variant === "border", 455 ); 456 this.element.classList.toggle( 457 "spotlight-card--gradient", 458 variant === "gradient", 459 ); 460 this.element.classList.toggle( 461 "spotlight-card--gradient-border", 462 variant === "gradient-border", 463 ); 464 } 465 466 _addEventListeners() { 467 this.element.addEventListener("mouseenter", this._handleMouseEnter); 468 this.element.addEventListener("mouseleave", this._handleMouseLeave); 469 this.element.addEventListener("mousemove", this._handleMouseMove); 470 471 if (this.options.mode === "proximity") { 472 document.addEventListener("mousemove", this._handleMouseMove); 473 } 474 } 475 476 _removeEventListeners() { 477 document.removeEventListener("mousemove", this._handleMouseMove); 478 this.element.removeEventListener("mousemove", this._handleMouseMove); 479 this.element.removeEventListener("mouseenter", this._handleMouseEnter); 480 this.element.removeEventListener("mouseleave", this._handleMouseLeave); 481 482 if (this._rafId) { 483 cancelAnimationFrame(this._rafId); 484 this._rafId = null; 485 } 486 } 487 488 _handleMouseMove(e) { 489 if (this.options.disabled) return; 490 491 if (this._rafId) cancelAnimationFrame(this._rafId); 492 493 this._rafId = requestAnimationFrame(() => { 494 const rect = this.element.getBoundingClientRect(); 495 const x = e.clientX - rect.left; 496 const y = e.clientY - rect.top; 497 498 this.element.style.setProperty("--mouse-x", `${x}px`); 499 this.element.style.setProperty("--mouse-y", `${y}px`); 500 501 if (typeof this.options.onMouseMove === "function") { 502 this.options.onMouseMove({ x, y, event: e }); 503 } 504 }); 505 } 506 507 _handleMouseEnter(e) { 508 if (this.options.disabled) return; 509 510 this._isHovering = true; 511 512 // Pause autoplay on hover 513 if (this.options.autoplay) { 514 this._stopAutoplay(); 515 } 516 517 if (typeof this.options.onMouseEnter === "function") { 518 this.options.onMouseEnter(e); 519 } 520 } 521 522 _handleMouseLeave(e) { 523 if (this.options.disabled) return; 524 525 this._isHovering = false;
526 527 // Resume autoplay on leave 528 if (this.options.autoplay) { 529 this._startAutoplay(); 530 } else { 531 this.element.style.setProperty("--mouse-x", "50%"); 532 this.element.style.setProperty("--mouse-y", "50%"); 533 } 534 535 if (typeof this.options.onMouseLeave === "function") { 536 this.options.onMouseLeave(e); 537 } 538 } 539 540 // Smooth autoplay animation using requestAnimationFrame 541 _animateAutoplay(timestamp) { 542 if (!this.options.autoplay || this._isHovering || this.options.disabled) { 543 return; 544 } 545 546 if (!this._autoplayStartTime) { 547 this._autoplayStartTime = timestamp; 548 } 549 550 // Support both preset names and numeric millisecond values 551 let duration; 552 if (typeof this.options.autoplaySpeed === "number") { 553 duration = this.options.autoplaySpeed; 554 } else { 555 duration = 556 autoplaySpeeds[this.options.autoplaySpeed] || autoplaySpeeds.normal; 557 } 558 559 const elapsed = timestamp - this._autoplayStartTime; 560 const progress = (elapsed % duration) / duration; // 0 to 1 561 562 const pathFn = 563 autoplayPaths[this.options.autoplayType] || autoplayPaths.circle; 564 const { x, y } = pathFn(progress); 565 566 const rect = this.element.getBoundingClientRect(); 567 const pixelX = (x / 100) * rect.width; 568 const pixelY = (y / 100) * rect.height; 569 570 this.element.style.setProperty("--mouse-x", `${pixelX}px`); 571 this.element.style.setProperty("--mouse-y", `${pixelY}px`); 572 573 this._autoplayRafId = requestAnimationFrame(this._animateAutoplay); 574 } 575 576 _startAutoplay() { 577 if (this._autoplayRafId) return; 578 this._autoplayStartTime = null; 579 this._autoplayRafId = requestAnimationFrame(this._animateAutoplay); 580 } 581 582 _stopAutoplay() { 583 if (this._autoplayRafId) { 584 cancelAnimationFrame(this._autoplayRafId); 585 this._autoplayRafId = null; 586 } 587 } 588 589 // Public Methods 590 setOptions(newOptions) { 591 const modeChanged = 592 newOptions.mode && newOptions.mode !== this.options.mode; 593 const autoplayChanged = 594 newOptions.autoplay !== undefined && 595 newOptions.autoplay !== this.options.autoplay; 596 597 this.options = { ...this.options, ...newOptions }; 598 this._applyOptions(); 599 600 if (modeChanged) { 601 this._removeEventListeners(); 602 this._addEventListeners(); 603 } 604 605 if (autoplayChanged) { 606 if (this.options.autoplay) { 607 this._startAutoplay(); 608 } else { 609 this._stopAutoplay(); 610 } 611 } 612 } 613 614 /** 615 * Set spotlight color (for non-gradient modes) 616 * @param {string} color - Any CSS color format (hex, rgb, hsl, named) 617 */ 618 setColor(color) { 619 this.options.color = color; 620 this._applyColor(color); 621 } 622 623 /** 624 * Set gradient colors (for gradient variants) 625 * @param {Array<string>} colors - Array of colors in any CSS format 626 */ 627 setGradientColors(colors) { 628 this.options.gradientColors = colors; 629 this._applyGradientColors(colors); 630 } 631 632 /** 633 * Set card background color 634 * @param {string} color - Any CSS color format 635 */ 636 setCardBg(color) { 637 this.options.cardBg = color; 638 if (color) { 639 this.element.style.setProperty("--spotlight-card-bg", color); 640 } else { 641 this.element.style.removeProperty("--spotlight-card-bg"); 642 } 643 } 644 645 /** 646 * Set card border color 647 * @param {string} color - Any CSS color format 648 */ 649 setCardBorder(color) { 650 this.options.cardBorder = color; 651 if (color) { 652 this.element.style.setProperty("--spotlight-card-border", color); 653 } else { 654 this.element.style.removeProperty("--spotlight-card-border"); 655 } 656 } 657 658 /** 659 * Set card border radius 660 * @param {string} radius - CSS border-radius value 661 */ 662 setCardRadius(radius) { 663 this.options.cardRadius = radius; 664 if (radius) { 665 this.element.style.setProperty("--spotlight-card-radius", radius); 666 } else { 667 this.element.style.removeProperty("--spotlight-card-radius"); 668 } 669 } 670 671 setSize(size) { 672 this.options.size = size; 673 // Check if it's a custom pixel value (number or string with px) 674 if ( 675 typeof size === "number" || 676 (typeof size === "string" && /^\d+/.test(size)) 677 ) { 678 const pxValue = 679 typeof size === "number" 680 ? `${size}px` 681 : size.endsWith("px") 682 ? size 683 : `${size}px`; 684 this.element.style.setProperty("--spotlight-size", pxValue); 685 this.element.setAttribute("data-size", "custom"); 686 this.element.setAttribute("data-size-value", pxValue); 687 } else { 688 // Preset size (sm, md, lg, xl) 689 this.element.style.removeProperty("--spotlight-size"); 690 this.element.setAttribute("data-size", size); 691 this.element.removeAttribute("data-size-value"); 692 } 693 } 694 695 setIntensity(intensity) { 696 this.options.intensity = intensity; 697 // Check if it's a custom value (number between 0-1 or 0-100) 698 if (
699 typeof intensity === "number" || 700 (typeof intensity === "string" && /^[\d.]+$/.test(intensity)) 701 ) { 702 let value = parseFloat(intensity); 703 // Normalize to 0-1 range if given as percentage (0-100) 704 if (value > 1) value = value / 100; 705 // Clamp between 0 and 1 706 value = Math.max(0, Math.min(1, value)); 707 708 // Calculate opacity values based on intensity 709 const spotlightOpacity = (value * 0.35).toFixed(3); // 0 to 0.35 710 const borderOpacity = (value * 0.9).toFixed(3); // 0 to 0.9 711 712 this.element.style.setProperty("--spotlight-opacity", spotlightOpacity); 713 this.element.style.setProperty( 714 "--spotlight-border-opacity", 715 borderOpacity, 716 ); 717 this.element.setAttribute("data-intensity", "custom"); 718 this.element.setAttribute("data-intensity-value", value); 719 } else { 720 // Preset intensity (low, medium, high) 721 this.element.style.removeProperty("--spotlight-opacity"); 722 this.element.style.removeProperty("--spotlight-border-opacity"); 723 this.element.setAttribute("data-intensity", intensity); 724 this.element.removeAttribute("data-intensity-value"); 725 } 726 } 727 728 setVariant(variant) { 729 this.options.variant = variant; 730 this.element.setAttribute("data-variant", variant); 731 this.element.classList.toggle( 732 "spotlight-card--border", 733 variant === "border", 734 ); 735 this.element.classList.toggle( 736 "spotlight-card--gradient", 737 variant === "gradient", 738 ); 739 this.element.classList.toggle( 740 "spotlight-card--gradient-border", 741 variant === "gradient-border", 742 ); 743 } 744 745 setAutoplay(enabled, type = "circle", speed = 3000) { 746 this.options.autoplay = enabled; 747 this.options.autoplayType = type; 748 this.options.autoplaySpeed = speed; 749 750 this.element.setAttribute("data-autoplay", enabled); 751 this.element.setAttribute("data-autoplay-type", type); 752 // Store speed value (works with both preset names and numbers) 753 this.element.setAttribute("data-autoplay-speed", speed); 754 this.element.classList.toggle("spotlight-card--autoplay", enabled); 755 756 if (enabled && !this._isHovering) { 757 this._startAutoplay(); 758 } else { 759 this._stopAutoplay(); 760 } 761 } 762 763 enable() { 764 this.options.disabled = false; 765 this.element.setAttribute("data-disabled", "false"); 766 if (this.options.autoplay) { 767 this._startAutoplay(); 768 } 769 } 770 771 disable() { 772 this.options.disabled = true; 773 this.element.setAttribute("data-disabled", "true"); 774 this._stopAutoplay(); 775 } 776 777 toggle() { 778 if (this.options.disabled) { 779 this.enable(); 780 } else { 781 this.disable(); 782 } 783 return this.options.disabled; 784 } 785 786 destroy() { 787 this._removeEventListeners(); 788 this._stopAutoplay(); 789 790 this.element.classList.remove( 791 "spotlight-card", 792 "spotlight-card--proximity", 793 "spotlight-card--autoplay", 794 "spotlight-card--border", 795 "spotlight-card--gradient", 796 "spotlight-card--gradient-border", 797 ); 798 799 const attrs = [ 800 "size", 801 "intensity", 802 "variant", 803 "mode", 804 "autoplay", 805 "autoplay-type", 806 "autoplay-speed", 807 "disabled", 808 "gradient-colors", 809 ]; 810 attrs.forEach((attr) => this.element.removeAttribute(`data-${attr}`)); 811 812 // Remove all CSS custom properties 813 const propsToRemove = [ 814 "--mouse-x", 815 "--mouse-y", 816 "--spotlight-color", 817 "--spotlight-card-bg", 818 "--spotlight-card-border", 819 "--spotlight-card-radius", 820 "--spotlight-gradient-count", 821 ]; 822 propsToRemove.forEach((prop) => this.element.style.removeProperty(prop)); 823 824 // Remove gradient color variables 825 for (let i = 1; i <= 10; i++) { 826 this.element.style.removeProperty(`--spotlight-gradient-${i}`); 827 } 828 } 829 } 830 831 /** 832 * SpotlightCardManager Class 833 */ 834 class SpotlightCardManager { 835 constructor() { 836 this.cards = new Map(); 837 } 838 839 init(selector = "[data-spotlight-card]", options = {}) { 840 const elements = document.querySelectorAll(selector); 841 const instances = []; 842
843 elements.forEach((element) => { 844 if (this.cards.has(element)) { 845 instances.push(this.cards.get(element)); 846 return; 847 } 848 849 // Parse data attributes 850 const speedAttr = element.getAttribute("data-spotlight-autoplay-speed"); 851 const parsedSpeed = speedAttr ? parseInt(speedAttr, 10) : NaN; 852 const dataOptions = { 853 color: element.getAttribute("data-spotlight-color"), 854 size: element.getAttribute("data-spotlight-size"), 855 intensity: element.getAttribute("data-spotlight-intensity"), 856 variant: element.getAttribute("data-spotlight-variant"), 857 mode: element.getAttribute("data-spotlight-mode"), 858 autoplay: element.getAttribute("data-spotlight-autoplay") === "true", 859 autoplayType: element.getAttribute("data-spotlight-autoplay-type"), 860 autoplaySpeed: !isNaN(parsedSpeed) ? parsedSpeed : speedAttr, 861 disabled: element.getAttribute("data-spotlight-disabled") === "true", 862 cardBg: element.getAttribute("data-spotlight-card-bg"), 863 cardBorder: element.getAttribute("data-spotlight-card-border"), 864 cardRadius: element.getAttribute("data-spotlight-card-radius"), 865 }; 866 867 // Parse gradient colors (comma-separated) 868 const gradientColorsAttr = element.getAttribute( 869 "data-spotlight-gradient-colors", 870 ); 871 if (gradientColorsAttr) { 872 dataOptions.gradientColors = gradientColorsAttr 873 .split(",") 874 .map((c) => c.trim()) 875 .filter(Boolean); 876 } 877 878 // Remove null/undefined values 879 Object.keys(dataOptions).forEach((key) => { 880 if (dataOptions[key] === null || dataOptions[key] === undefined) { 881 delete dataOptions[key]; 882 } 883 }); 884 885 const mergedOptions = { ...options, ...dataOptions }; 886 const instance = new SpotlightCard(element, mergedOptions); 887 888 this.cards.set(element, instance); 889 instances.push(instance); 890 }); 891 892 return instances; 893 } 894 895 get(element) { 896 return this.cards.get(element); 897 } 898 899 has(element) { 900 return this.cards.has(element); 901 } 902 903 remove(element) { 904 const instance = this.cards.get(element); 905 if (instance) { 906 instance.destroy(); 907 this.cards.delete(element); 908 } 909 } 910 911 getAll() { 912 return Array.from(this.cards.values()); 913 } 914 915 destroyAll() { 916 this.cards.forEach((instance) => instance.destroy()); 917 this.cards.clear(); 918 } 919 920 setAllOptions(options) { 921 this.cards.forEach((instance) => instance.setOptions(options)); 922 } 923 } 924 925 function autoInit() { 926 const manager = new SpotlightCardManager(); 927 manager.init(); 928 929 // Support [data-spotlight-children] â init spotlight on each child matching selector 930 document.querySelectorAll('[data-spotlight-children]').forEach((parent) => { 931 const childSelector = parent.getAttribute('data-spotlight-children'); 932 let configStr = parent.getAttribute('data-spotlight-config'); 933 let config = {}; 934 try { config = JSON.parse(configStr || '{}'); } catch(e) {} 935 936 // Parse config into SpotlightCard options 937 const opts = {}; 938 if (config['data-spotlight-color']) opts.color = config['data-spotlight-color']; 939 if (config['data-spotlight-gradient-colors']) { 940 opts.gradientColors = config['data-spotlight-gradient-colors'].split(',').map(s => s.trim()); 941 } 942 if (config['data-spotlight-size']) opts.size = config['data-spotlight-size']; 943 if (config['data-spotlight-intensity']) opts.intensity = config['data-spotlight-intensity']; 944 if (config['data-spotlight-variant']) opts.variant = config['data-spotlight-variant']; 945 if (config['data-spotlight-mode']) opts.mode = config['data-spotlight-mode']; 946 if (config['data-spotlight-autoplay'] === 'true') { 947 opts.autoplay = true; 948 if (config['data-spotlight-autoplay-type']) opts.autoplayType = config['data-spotlight-autoplay-type']; 949 if (config['data-spotlight-autoplay-speed']) opts.autoplaySpeed = parseInt(config['data-spotlight-autoplay-speed']); 950 } 951 952 const children = parent.querySelectorAll(childSelector);
953 children.forEach((child) => { 954 if (child.style.position === '' || child.style.position === 'static') { 955 child.style.position = 'relative'; 956 } 957 child.style.overflow = 'hidden'; 958 const instance = new SpotlightCard(child, opts); 959 manager.cards.set(child, instance); 960 }); 961 }); 962 963 global.spotlightCardManager = manager; 964 } 965 966 // Expose for external use 967 global.SpotlightCard = SpotlightCard; 968 global.SpotlightCardManager = SpotlightCardManager; 969 global.SpotlightColorUtils = ColorUtils; 970 971 if (document.readyState === "loading") { 972 document.addEventListener("DOMContentLoaded", autoInit); 973 } else { 974 autoInit(); 975 } 976})(typeof window !== "undefined" ? window : this);
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.