PageSourceSearch

https://m0ve.app/assets/controllers/dashboard_drawer_controller-3b…6071d603d2ac492069b104570fb2f64ee10383ac.js

js m0ve.app collected 2026-10-07 11:37:42 UTC 3,686 bytes, 119 lines download raw bytes

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.