1// app/javascript/controllers/dashboard_drawer_controller.js 2// 3// Turns the dashboard sidebar into a slide-in drawer on mobile while 4// leaving the desktop layout untouched. The controller scope wraps the 5// hamburger trigger, the overlay, and the sidebar panel. On widths below 6// the breakpoint the panel is positioned `fixed`/off-screen and slides in 7// when toggled; at md+ widths everything reverts to its native flex 8// positioning and the controller becomes a no-op. 9import { Controller } from "@hotwired/stimulus" 10 11export default class extends Controller { 12 static targets = ["panel", "overlay", "toggle"] 13 static values = { breakpoint: { type: Number, default: 768 } } 14 15 connect() { 16 this.isOpen = false 17 this.boundOnKey = this.onKey.bind(this) 18 this.boundOnResize = this.onResize.bind(this) 19 this.boundOnPanelClick = this.onPanelClick.bind(this) 20 21 document.addEventListener("keydown", this.boundOnKey) 22 window.addEventListener("resize", this.boundOnResize) 23 if (this.hasPanelTarget) { 24 this.panelTarget.addEventListener("click", this.boundOnPanelClick) 25 } 26 } 27 28 disconnect() { 29 document.removeEventListener("keydown", this.boundOnKey) 30 window.removeEventListener("resize", this.boundOnResize) 31 if (this.hasPanelTarget) { 32 this.panelTarget.removeEventListener("click", this.boundOnPanelClick) 33 } 34 this.unlockScroll() 35 } 36 37 open(event) { 38 if (event) event.preventDefault() 39 if (!this.isMobile()) return 40 this.isOpen = true 41 42 if (this.hasPanelTarget) { 43 this.panelTarget.classList.remove("-translate-x-full") 44 this.panelTarget.classList.add("translate-x-0") 45 } 46 if (this.hasOverlayTarget) { 47 this.overlayTarget.classList.remove("opacity-0", "pointer-events-none") 48 this.overlayTarget.classList.add("opacity-100") 49 } 50 if (this.hasToggleTarget) { 51 this.toggleTarget.setAttribute("aria-expanded", "true") 52 } 53 54 this.lockScroll() 55 } 56 57 close(event) { 58 if (event) event.preventDefault() 59 this.isOpen = false 60 61 if (this.hasPanelTarget) { 62 this.panelTarget.classList.add("-translate-x-full") 63 this.panelTarget.classList.remove("translate-x-0") 64 } 65 if (this.hasOverlayTarget) { 66 this.overlayTarget.classList.add("opacity-0", "pointer-events-none") 67 this.overlayTarget.classList.remove("opacity-100") 68 } 69 if (this.hasToggleTarget) { 70 this.toggleTarget.setAttribute("aria-expanded", "false") 71 } 72 73 this.unlockScroll() 74 } 75 76 toggle(event) { 77 this.isOpen ? this.close(event) : this.open(event) 78 } 79 80 // Auto-close after the user taps a nav link inside the drawer so the 81 // destination page isn't rendered behind a half-open menu on slow nets. 82 onPanelClick(event) { 83 if (!this.isOpen) return 84 if (!this.isMobile()) return 85 const link = event.target.closest("a[href]") 86 if (!link) return 87 if (link.getAttribute("href") === "#") return 88 this.close() 89 } 90 91 onKey(event) { 92 if (event.key === "Escape" && this.isOpen) this.close() 93 } 94 95 // If the viewport widens past the breakpoint while the drawer is open, 96 // unlock body scroll and reset state so the desktop layout takes over. 97 onResize() { 98 if (!this.isOpen) return 99 if (!this.isMobile()) this.close() 100 } 101 102 isMobile() { 103 return window.innerWidth < this.breakpointValue 104 } 105 106 lockScroll() { 107 if (this.priorBodyOverflow === undefined) { 108 this.priorBodyOverflow = document.body.style.overflow 109 } 110 document.body.style.overflow = "hidden" 111 } 112 113 unlockScroll() { 114 if (this.priorBodyOverflow !== undefined) { 115 document.body.style.overflow = this.priorBodyOverflow 116 this.priorBodyOverflow = undefined 117 } 118 } 119};
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.