PageSourceSearch

https://my.pressable.com/assets/controllers/navigation/menu_slide_controller-1d17dd8c.js

js pressable.com collected 2026-09-24 15:24:03 UTC 14,242 bytes, 394 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2
3export default class extends Controller {
4  static targets = ["siteNav", "collaboratorNav", "tagNav", "toolsNav", "settingsNav", "billingNav", "designNav", "newEntityNav", "mainNav", "container"]
5  static values = {
6    siteId: String,
7    collaboratorId: String,
8    tagId: String,
9    tools: String,
10    settings: String,
11    billing: String,
12    design: String,
13    newEntityId: String
14  }
15
16  connect() {
17    // Initialize menu state storage
18    this.menuStates = {
19      siteNav: { scrollTop: 0, expandedItems: [] },
20      collaboratorNav: { scrollTop: 0, expandedItems: [] },
21      tagNav: { scrollTop: 0, expandedItems: [] },
22      toolsNav: { scrollTop: 0, expandedItems: [] },
23      settingsNav: { scrollTop: 0, expandedItems: [] },
24      billingNav: { scrollTop: 0, expandedItems: [] },
25      designNav: { scrollTop: 0, expandedItems: [] },
26      newEntityNav: { scrollTop: 0, expandedItems: [] },
27      mainNav: { scrollTop: 0, expandedItems: [] }
28    }
29    this.currentNav = this.hasSiteNavTarget ? 'site' : (this.hasCollaboratorNavTarget ? 'collaborator' : (this.hasTagNavTarget ? 'tag' : (this.hasSettingsNavTarget ? 'settings' : (this.hasBillingNavTarget ? 'billing' : (this.hasToolsNavTarget ? 'tools' : (this.hasNewEntityNavTarget ? 'newEntity' : (this.hasDesignNavTarget ? 'design' : 'main')))))))
30  }
31
32  // Slide to main menu
33  async slideToMainMenu(event) {
34    event.preventDefault()
35
36    // Save current nav state (site, collaborator, tag, tools, settings, billing, newEntity, or design)
37    const currentNavType = this.hasSiteNavTarget ? 'siteNav' : (this.hasCollaboratorNavTarget ? 'collaboratorNav' : (this.hasTagNavTarget ? 'tagNav' : (this.hasSettingsNavTarget ? 'settingsNav' : (this.hasBillingNavTarget ? 'billingNav' : (this.hasNewEntityNavTarget ? 'newEntityNav' : (this.hasDesignNavTarget ? 'designNav' : 'toolsNav'))))))
38    this.saveMenuState(currentNavType)
39
40    // Check if main nav already exists
41    if (this.hasMainNavTarget) {
42      this.performSlideTransition('left')
43    } else {
44      // Fetch and inject main nav
45      await this.fetchMainNav()
46    }
47  }
48
49  // Slide back to site menu
50  slideToSiteMenu(event) {
51    event.preventDefault()
52
53    // Save current main nav state
54    this.saveMenuState('mainNav')
55
56    // Perform slide transition
57    this.performSlideTransition('right', 'site')
58  }
59
60  // Slide back to collaborator menu
61  slideToCollaboratorMenu(event) {
62    event.preventDefault()
63
64    // Save current main nav state
65    this.saveMenuState('mainNav')
66
67    // Perform slide transition
68    this.performSlideTransition('right', 'collaborator')
69  }
70
71  // Slide back to tag menu
72  slideToTagMenu(event) {
73    event.preventDefault()
74
75    // Save current main nav state
76    this.saveMenuState('mainNav')
77
78    // Perform slide transition
79    this.performSlideTransition('right', 'tag')
80  }
81
82  // Slide back to tools menu
83  slideToToolsMenu(event) {
84    event.preventDefault()
85
86    // Save current main nav state
87    this.saveMenuState('mainNav')
88
89    // Perform slide transition
90    this.performSlideTransition('right', 'tools')
91  }
92
93  // Slide back to settings menu
94  slideToSettingsMenu(event) {
95    event.preventDefault()
96
97    // Save current main nav state
98    this.saveMenuState('mainNav')
99
100    // Perform slide transition
101    this.performSlideTransition('right', 'settings')
102  }
103
104  // Slide back to billing menu
105  slideToBillingMenu(event) {
106    event.preventDefault()
107
108    // Save current main nav state
109    this.saveMenuState('mainNav')
110
111    // Perform slide transition
112    this.performSlideTransition('right', 'billing')
113  }
114
115  // Slide back to design menu
116  slideToDesignMenu(event) {
117    event.preventDefault()
118
119    // Save current main nav state
120    this.saveMenuState('mainNav')
121
122    // Perform slide transition
123    this.performSlideTransition('right', 'design')
124  }
125
126  // Slide back to new entity menu
127  slideToNewEntityMenu(event) {
128    event.preventDefault()
129
130    // Save current main nav state
131    this.saveMenuState('mainNav')
132
133    // Perform slide transition
134    this.performSlideTransition('right', 'newEntity')
135  }
136
137  // Fetch main navigation partial
138  async fetchMainNav() {
139    try {
140      // Build query params
141      const params = new URLSearchParams()
142      if (this.hasSiteIdValue) {
143        params.append('site_id', this.siteIdValue)
144      }
145      if (this.hasCollaboratorIdValue) {
146        params.append('collaborator_id', this.collaboratorIdValue)
147      }
148      if (this.hasTagIdValue) {
149        params.append('tag_id', this.tagIdValue)
150      }
151      if (this.hasToolsValue) {
152        params.append('tools', this.toolsValue)
153      }
154      if (this.hasSettingsValue) {
155        params.append('settings', this.settingsValue)
156      }
157      if (this.hasBillingValue) {
158        params.append('billing', this.billingValue)
159      }
160      if (this.hasDesignValue) {
161        params.append('design', this.designValue)
162      }
163      if (this.hasNewEntityIdValue) {
164        params.append('new_entity_id', this.newEntityIdValue)
165      }
166
167      const response = await fetch(`/navigation/main_app_nav?${params.toString()}`, {
168        headers: {
169          'Accept': 'text/html',
170          'X-Requested-With': 'XMLHttpRequest'
171        }
172      })
173
174      if (!response.ok) {
175        throw new Error('Failed to fetch main navigation')
176      }
177
178      const html = await response.text()
179
180      // Create a container for the main nav
181      const mainNavContainer = document.createElement('div')
182      mainNavContainer.setAttribute('data-navigation--menu-slide-target', 'mainNav')
183      // Start positioned off-screen to the left (no nav-slide-hidden, so it's visible but off-screen)
184      mainNavContainer.classList.add('nav-slide-container', 'nav-slide-left')
185      mainNavContainer.innerHTML = html
186
187      // Insert after current nav (site, collaborator, tag, tools, settings, billing, newEntity, or design)
188      const currentNavTarget = this.hasSiteNavTarget ? this.siteNavTarget : (this.hasCollaboratorNavTarget ? this.collaboratorNavTarget : (this.hasTagNavTarget ? this.tagNavTarget : (this.hasSettingsNavTarget ? this.settingsNavTarget : (this.hasBillingNavTarget ? this.billingNavTarget : (this.hasNewEntityNavTarget ? this.newEntityNavTarget : (this.hasDesignNavTarget ? this.designNavTarget : this.toolsNavTarget))))))
189      currentNavTarget.parentNode.appendChild(mainNavContainer)
190
191      // Force a reflow to ensure the initial position is rendered before transition
192      mainNavContainer.offsetHeight
193
194      // Wait for next frame to ensure CSS is applied
195      await new Promise(resolve => requestAnimationFrame(resolve))
196
197      // Perform slide transition
198      this.performSlideTransition('left')
199
200    } catch (error) {
201      console.error('Error fetching main navigation:', error)
202      // Fallback to regular navigation
203      window.location.href = '/sites'
204    }
205  }
206
207  // Perform the slide animation
208  performSlideTransition(direction, targetNav = null) {
209    if (direction === 'left') {
210      // Sliding to main menu (current nav goes right, main nav 
210comes from left)
211      const currentNavTarget = this.hasSiteNavTarget ? this.siteNavTarget : (this.hasCollaboratorNavTarget ? this.collaboratorNavTarget : (this.hasTagNavTarget ? this.tagNavTarget : (this.hasSettingsNavTarget ? this.settingsNavTarget : (this.hasBillingNavTarget ? this.billingNavTarget : (this.hasNewEntityNavTarget ? this.newEntityNavTarget : (this.hasDesignNavTarget ? this.designNavTarget : this.toolsNavTarget))))))
212      currentNavTarget.classList.remove('nav-slide-center')
213      currentNavTarget.classList.add('nav-slide-right')
214
215      this.mainNavTarget.classList.remove('nav-slide-hidden', 'nav-slide-left')
216      this.mainNavTarget.classList.add('nav-slide-center')
217
218      this.currentNav = 'main'
219
220      // Restore main nav state after transition
221      setTimeout(() => {
222        this.restoreMenuState('mainNav')
223      }, 200)
224
225    } else {
226      // Sliding back to site, collaborator, or tag menu (main nav goes left, target nav comes from right)
227      this.mainNavTarget.classList.remove('nav-slide-center')
228      this.mainNavTarget.classList.add('nav-slide-left')
229
230      if (targetNav === 'site' && this.hasSiteNavTarget) {
231        this.siteNavTarget.classList.remove('nav-slide-right')
232        this.siteNavTarget.classList.add('nav-slide-center')
233        this.currentNav = 'site'
234
235        // Restore site nav state after transition
236        setTimeout(() => {
237          this.restoreMenuState('siteNav')
238        }, 200)
239      } else if (targetNav === 'collaborator' && this.hasCollaboratorNavTarget) {
240        this.collaboratorNavTarget.classList.remove('nav-slide-right')
241        this.collaboratorNavTarget.classList.add('nav-slide-center')
242        this.currentNav = 'collaborator'
243
244        // Restore collaborator nav state after transition
245        setTimeout(() => {
246          this.restoreMenuState('collaboratorNav')
247        }, 200)
248      } else if (targetNav === 'tag' && this.hasTagNavTarget) {
249        this.tagNavTarget.classList.remove('nav-slide-right')
250        this.tagNavTarget.classList.add('nav-slide-center')
251        this.currentNav = 'tag'
252
253        // Restore tag nav state after transition
254        setTimeout(() => {
255          this.restoreMenuState('tagNav')
256        }, 200)
257      } else if (targetNav === 'tools' && this.hasToolsNavTarget) {
258        this.toolsNavTarget.classList.remove('nav-slide-right')
259        this.toolsNavTarget.classList.add('nav-slide-center')
260        this.currentNav = 'tools'
261
262        // Restore tools nav state after transition
263        setTimeout(() => {
264          this.restoreMenuState('toolsNav')
265        }, 200)
266      } else if (targetNav === 'settings' && this.hasSettingsNavTarget) {
267        this.settingsNavTarget.classList.remove('nav-slide-right')
268        this.settingsNavTarget.classList.add('nav-slide-center')
269        this.currentNav = 'settings'
270
271        // Restore settings nav state after transition
272        setTimeout(() => {
273          this.restoreMenuState('settingsNav')
274        }, 200)
275      } else if (targetNav === 'billing' && this.hasBillingNavTarget) {
276        this.billingNavTarget.classList.remove('nav-slide-right')
277        this.billingNavTarget.classList.add('nav-slide-center')
278        this.currentNav = 'billing'
279
280        // Restore billing nav state after transition
281        setTimeout(() => {
282          this.restoreMenuState('billingNav')
283        }, 200)
284      } else if (targetNav === 'newEntity' && this.hasNewEntityNavTarget) {
285        this.newEntityNavTarget.classList.remove('nav-slide-right')
286        this.newEntityNavTarget.classList.add('nav-slide-center')
287        this.currentNav = 'newEntity'
288
289        // Restore newEntity nav state after transition
290        setTimeout(() => {
291          this.restoreMenuState('newEntityNav')
292        }, 200)
293      } else if (targetNav === 'design' && this.hasDesignNavTarget) {
294        this.designNavTarget.classList.remove('nav-slide-right')
295        this.designNavTarget.classList.add('nav-slide-center')
296        this.currentNav = 'design'
297
298        // Restore design nav state after transition
299        setTimeout(() => {
300          this.restoreMenuState('designNav')
301        }, 200)
302      } else {
303        // Defensive fallback: target nav doesn't exist, revert main nav to center
304        this.mainNavTarget.classList.remove('nav-slide-left')
305        this.mainNavTarget.classList.add('nav-slide-center')
306        this.currentNav = 'main'
307
308        // Restore main nav state after transition
309        setTimeout(() => {
310          this.restoreMenuState('mainNav')
311        }, 200)
312      }
313    }
314  }
315
316  // Save menu state (scroll position and expanded items)
317  saveMenuState(navType) {
318    let target
319    if (navType === 'siteNav') {
320      target = this.siteNavTarget
321    } else if (navType === 'collaboratorNav') {
322      target = this.collaboratorNavTarget
323    } else if (navType === 'tagNav') {
324      target = this.tagNavTarget
325    } else if (navType === 'toolsNav') {
326      target = this.toolsNavTarget
327    } else if (navType === 'settingsNav') {
328      target = this.settingsNavTarget
329    } else if (navType === 'billingNav') {
330      target = this.billingNavTarget
331    } else if (navType === 'newEntityNav') {
332      target = this.newEntityNavTarget
333    } else if (navType === 'designNav') {
334      target = this.designNavTarget
335    } else {
336      target = this.mainNavTarget
337    }
338
339    const navElement = target.querySelector('.primary-navigation-wrapper') || target
340
341    // Save scroll position
342    this.menuStates[navType].scrollTop = navElement.scrollTop || 0
343
344    // Save expanded collapse items
345    const expandedItems = []
346    const collapseElements = navElement.querySelectorAll('.collapse.show')
347    collapseElements.forEach(el => {
348      if (el.id) {
349        expandedItems.push(el.id)
350      }
351    })
352    this.menuStates[navType].expandedItems = expandedItems
353  }
354
355  // Restore menu state (scroll position and expanded items)
356  restoreMenuState(navType) {
357    let target
358    if (navType === 'siteNav') {
359      target = this.siteNavTarget
360    } else if (navType === 'collaboratorNav') {
361      target = this.collaboratorNavTarget
362    } else if (navType === 'tagNav') {
363      target = this.tagNavTarget
364    } else if (navType === 'toolsNav') {
365      target = this.toolsNavTarget
366    } else if (navType === 'settingsNav') {
367      target = this.settingsNavTarget
368    } else if (navType === 'billingNav') {
369      target = this.billingNavTarget
370    } else if (navType === 'newEntityNav') {
371      target = this.newEntityNavTarget
372    } else if (navType === 'designNav') {
373      target = this.designNavTarget
374    } else {
375      target = this.mainNavTarget
376    }
377
378    const navElement = target.querySelector('.primary-navigation-wrapper') || target
379
380    // Restore scroll position
381    if (this.menuStates[navType].scrollTop) {
382      navElement.scrollTop = this.menuStates[navType].scrollTop
383    }
384
385    // Restore expanded collapse items
386    this.menuStates[navType].expandedItems.forEach(id => {
387      const collapseElement = navElement.querySelector(`#${id}`)
388      if (collapseElement) {
389        const bsCollapse = new bootstrap.Collapse(collapseElement, { toggle: false })
390        bsCollapse.show()
391      }
392    })
393  }
394}

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.