1import { Controller } from "@hotwired/stimulus"; 2 3// Connects to data-controller="sidebar" 4export default class extends Controller { 5 static targets = [ 6 "desktopSidebar", 7 "mobileSidebar", 8 "contentTemplate", 9 "sharedContent", 10 "desktopContent", 11 "mobileBackdrop", 12 "mobilePanel", 13 ]; 14 static values = { 15 storageKey: { type: String, default: "sidebarOpen" }, 16 }; 17 18 connect() { 19 // Bind the handler once so we can properly remove it later 20 this.boundHandleToggle = this.handleToggle.bind(this); 21 22 // Clone the sidebar content for both mobile and desktop 23 if (this.hasContentTemplateTarget) { 24 // Clone content for desktop (only if not already cloned) 25 // Check if nav element exists (the actual cloned content) 26 if (this.hasDesktopContentTarget && !this.desktopContentTarget.querySelector("nav")) { 27 const desktopClone = this.contentTemplateTarget.content.cloneNode(true); 28 this.desktopContentTarget.appendChild(desktopClone); 29 } 30 31 // Clone content for mobile (only if not already cloned) 32 if (this.hasSharedContentTarget && !this.sharedContentTarget.querySelector("nav")) { 33 const mobileClone = this.contentTemplateTarget.content.cloneNode(true); 34 this.sharedContentTarget.appendChild(mobileClone); 35 36 // Update the close button in mobile view to show X icon and have mobile close action 37 const mobileNav = this.sharedContentTarget.querySelector("nav"); 38 if (mobileNav) { 39 const closeButton = mobileNav.querySelector('[data-action*="sidebar#close"]'); 40 if (closeButton) { 41 closeButton.setAttribute("data-action", "click->sidebar#closeMobile"); 42 closeButton.classList.remove("cursor-w-resize"); 43 closeButton.setAttribute("aria-label", "Close sidebar"); 44 45 // Replace the collapse icon with an X icon 46 const svg = closeButton.querySelector("svg"); 47 if (svg) { 48 svg.innerHTML = 49 '<g fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke="currentColor"><line x1="13.25" y1="4.75" x2="4.75" y2="13.25"></line><line x1="13.25" y1="13.25" x2="4.75" y2="4.75"></line></g>'; 50 } 51 } 52 } 53 } 54 } 55 56 if (this.hasDesktopSidebarTarget) { 57 // Temporarily disable transitions to prevent animation on page load 58 this.desktopSidebarTarget.style.transition = "none"; 59 60 // Restore the saved state from localStorage, default to open if no saved state 61 const savedState = localStorage.getItem(this.storageKeyValue); 62 63 if (savedState !== null) { 64 this.desktopSidebarTarget.open = savedState === "true"; 65 } else { 66 // Default to open for first-time visitors 67 this.desktopSidebarTarget.open = true; 68 } 69 70 // Re-enable transitions after a brief delay 71 requestAnimationFrame(() => { 72 requestAnimationFrame(() => { 73 this.desktopSidebarTarget.style.transition = ""; 74 }); 75 }); 76 77 // Listen for toggle events to save the state 78 this.desktopSidebarTarget.addEventListener("toggle", this.boundHandleToggle); 79 } 80 } 81 82 disconnect() { 83 if (this.hasDesktopSidebarTarget && this.boundHandleToggle) { 84 this.desktopSidebarTarget.removeEventListener("toggle", this.boundHandleToggle); 85 } 86 } 87 88 handleToggle(event) { 89 // Save the current state to localStorage 90 localStorage.setItem(this.storageKeyValue, this.desktopSidebarTarget.open.toString()); 91 } 92 93 open() { 94 if (this.hasDesktopSidebarTarget) { 95 this.desktopSidebarTarget.open = true; 96 } 97 } 98 99 close() { 100 if (this.hasDesktopSidebarTarget) { 101 this.desktopSidebarTarget.open = false; 102 } 103 } 104 105 toggle() { 106 if (this.hasDesktopSidebarTarget) { 107 this.desktopSidebarTarget.open = !this.desktopSidebarTarget.open; 108 } 109 } 110 111 openMobile() { 112 if (this.hasMobileSidebarTarget) { 113 // Set initial hidden states 114 if (this.hasMobileBackdropTarget) { 115 this.mobileBackdropTarget.style.opacity = "0"; 116 } 117 if (this.hasMobilePanelTarget) { 118 this.mobilePanelTarget.style.transform = "translateX(-100%)"; 119 } 120 121 // Remove hidden class 122 this.mobileSidebarTarget.classList.remove("hidden"); 123 124 // Trigger transition on next frame 125 requestAnimationFrame(() => { 126 if (this.hasMobileBackdropTarget) { 127 this.mobileBackdropTarget.style.opacity = "1"; 128 } 129 if (this.hasMobilePanelTarget) { 130 this.mobilePanelTarget.style.transform = "translateX(0)"; 131 } 132 }); 133 } 134 } 135 136 closeMobile() { 137 if (this.hasMobileSidebarTarget) { 138 // Trigger closing transition 139 if (this.hasMobileBackdropTarget) { 140 this.mobileBackdropTarget.style.opacity = "0"; 141 } 142 if (this.hasMobilePanelTarget) { 143 this.mobilePanelTarget.style.transform = "translateX(-100%)"; 144 } 145 146 // Wait for transition to complete before hiding 147 setTimeout(() => { 148 if (this.hasMobileSidebarTarget) { 149 this.mobileSidebarTarget.classList.add("hidden"); 150 } 151 }, 300); // Match the transition duration 152 } 153 } 154};
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.