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.