PageSourceSearch

https://www.georgetowncoc.com/assets/controllers/tabs_controller-0ec3a638.js

js georgetowncoc.com collected 2026-09-25 04:06:13 UTC 5,114 bytes, 145 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static targets = ["tab", "panel"]
5  static classes = ["active", "inactive"]
6  static values = { current: String }
7
8  connect() {
9    this.boundSubmit = this.onSubmit.bind(this)
10    this.boundPopstate = this.onPopstate.bind(this)
11    // Capture submit events so we can preserve tab context on POST/PATCH/DELETE forms.
12    this.element.addEventListener("submit", this.boundSubmit, true)
13    window.addEventListener("popstate", this.boundPopstate)
14
15    const urlTab = new URL(window.location.href).searchParams.get("tab")
16    if (this.isKnownTab(urlTab)) this.currentValue = urlTab
17
18    if (!this.isKnownTab(this.currentValue) && this.tabTargets.length > 0) {
19      this.currentValue = this.tabTargets[0].dataset.tab
20    }
21
22    this.showCurrent()
23    this.syncTabState()
24  }
25
26  disconnect() {
27    if (this.boundSubmit) this.element.removeEventListener("submit", this.boundSubmit, true)
28    if (this.boundPopstate) window.removeEventListener("popstate", this.boundPopstate)
29  }
30
31  select(event) {
32    this.currentValue = event.currentTarget.dataset.tab
33    this.showCurrent()
34    this.syncTabState({ push: true })
35  }
36
37  navigate(event) {
38    const keys = ["ArrowLeft", "ArrowRight", "Home", "End"]
39    if (!keys.includes(event.key)) return
40
41    const tabs = this.tabTargets.filter((tab) => tab.getAttribute("role") === "tab" && !tab.disabled)
42    const currentIndex = tabs.indexOf(event.currentTarget)
43    if (currentIndex < 0 || tabs.length < 2) return
44
45    event.preventDefault()
46    let nextIndex
47    if (event.key === "Home") nextIndex = 0
48    if (event.key === "End") nextIndex = tabs.length - 1
49    if (event.key === "ArrowLeft") nextIndex = (currentIndex - 1 + tabs.length) % tabs.length
50    if (event.key === "ArrowRight") nextIndex = (currentIndex + 1) % tabs.length
51
52    const nextTab = tabs[nextIndex]
53    this.currentValue = nextTab.dataset.tab
54    this.showCurrent()
55    this.syncTabState({ push: true })
56    nextTab.focus()
57  }
58
59  showCurrent() {
60    this.tabTargets.forEach((tab) => {
61      const isActive = tab.dataset.tab === this.currentValue
62      if (this.hasActiveClass) this.toggleClasses(tab, this.activeClasses, isActive)
63      if (this.hasInactiveClass) this.toggleClasses(tab, this.inactiveClasses, !isActive)
64      tab.setAttribute("aria-selected", isActive ? "true" : "false")
65      if (tab.getAttribute("role") === "tab") tab.tabIndex = isActive ? 0 : -1
66    })
67
68    this.panelTargets.forEach((panel) => {
69      const isActive = panel.dataset.tab === this.currentValue
70      panel.classList.toggle("hidden", !isActive)
71      if (panel.getAttribute("role") === "tabpanel") panel.hidden = !isActive
72    })
73  }
74
75  syncTabState({ push = false } = {}) {
76    this.syncUrl({ push })
77    this.syncReturnTabInputs()
78  }
79
80  syncUrl({ push = false } = {}) {
81    const url = new URL(window.location.href)
82    url.searchParams.set("tab", this.currentValue)
83    const state = this.buildHistoryState()
84    if (push) {
85      window.history.pushState(state, "", url)
86    } else {
87      window.history.replaceState(state, "", url)
88    }
89  }
90
91  buildHistoryState() {
92    // Turbo Drive stores navigation state in history.state.turbo.
93    // If we overwrite history.state, Turbo back/forward breaks.
94    const base = window.history.state || {}
95    const chamberhive = base.chamberhive || {}
96    return { ...base, chamberhive: { ...chamberhive, tab: this.currentValue } }
97  }
98
99  syncReturnTabInputs() {
100    // Some forms (e.g. edit dialogs) are outside the tabs element, but still
101    // need to round-trip the current tab on submit. We sync all `return_tab`
102    // hidden inputs on the page.
103    document.querySelectorAll("input[name='return_tab']").forEach((input) => {
104      input.value = this.currentValue
105    })
106  }
107
108  onSubmit(event) {
109    const form = event.target
110    if (!(form instanceof HTMLFormElement)) return
111    if (!this.currentValue) return
112
113    // Ensure tab param round-trips to the server so redirects/render stay on the same tab.
114    const url = new URL(form.action, window.location.origin)
115    url.searchParams.set("tab", this.currentValue)
116    form.action = `${url.pathname}${url.search}`
117  }
118
119  onPopstate() {
120    // When the user navigates back/forward, update the visible tab to match
121    // the URL. Do not write back to history from here.
122    const urlTab = new URL(window.location.href).searchParams.get("tab")
123    const fallback = this.tabTargets[0] ? this.tabTargets[0].dataset.tab : null
124    const desired = this.isKnownTab(urlTab) ? urlTab : fallback
125    if (!desired) return
126    if (desired === this.currentValue) return
127
128    this.currentValue = desired
129    this.showCurrent()
130    this.syncReturnTabInputs()
131  }
132
133  toggleClasses(element, classNames, enabled) {
134    if (!classNames) return
135    classNames.forEach((className) => {
136      if (!className || className.trim().length === 0) return
137      element.classList.toggle(className, enabled)
138    })
139  }
140
141  isKnownTab(tabKey) {
142    if (!tabKey) return false
143    return this.tabTargets.some((tab) => tab.dataset.tab === tabKey)
144  }
145}

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.