1// Shepherd is loaded globally from the CDN script in the Blade view 2const Shepherd = window.Shepherd; 3 4let activeTourInstance = null; 5 6/** 7 * Tag sidebar items with data-tour (by URL path, then by navigation label). 8 */ 9function applyTourNavAttributes() { 10 const navigationMap = window.navigationMap || {}; 11 const dynamicSteps = window.dynamicTourSteps || []; 12 13 // 1) Match by resource/page URL (most reliable with Livewire SPA) 14 dynamicSteps.forEach((step) => { 15 if (!step.id || !step.url) { 16 return; 17 } 18 19 let stepPath; 20 21 try { 22 stepPath = new URL(step.url, window.location.origin).pathname; 23 } catch (e) { 24 return; 25 } 26 27 document.querySelectorAll('a.fi-sidebar-item-btn[href], .fi-sidebar-item a[href]').forEach((link) => { 28 let linkPath; 29 30 try { 31 linkPath = new URL(link.href, window.location.origin).pathname; 32 } catch (e) { 33 return; 34 } 35 36 if (linkPath !== stepPath) { 37 return; 38 } 39 40 const item = link.closest('.fi-sidebar-item') || link; 41 42 item.setAttribute('data-tour', step.id); 43 link.setAttribute('data-tour', step.id); 44 }); 45 }); 46 47 // 2) Fallback: match sidebar label text 48 document.querySelectorAll('.fi-sidebar-item, .fi-sidebar-group').forEach((item) => { 49 const text = item.textContent.trim(); 50 const link = item.querySelector('a.fi-sidebar-item-btn, a[href]') || item; 51 52 Object.entries(navigationMap).forEach(([stepId, navLabel]) => { 53 if (text.includes(navLabel) || text === navLabel) { 54 const sidebarItem = link.closest('.fi-sidebar-item') || item; 55 56 sidebarItem.setAttribute('data-tour', stepId); 57 link.setAttribute('data-tour', stepId); 58 } 59 }); 60 }); 61} 62 63/** @deprecated alias */ 64function autoDetectNavigationElements() { 65 applyTourNavAttributes(); 66} 67 68/** 69 * Resolve the sidebar DOM node to highlight for a step. 70 */ 71function resolveTourTargetElement(stepId, attachToSelector = null) { 72 applyTourNavAttributes(); 73 74 const selector = attachToSelector || `[data-tour="${stepId}"]`; 75 const el = document.querySelector(selector); 76 77 if (!el) { 78 return null; 79 } 80 81 return ( 82 el.closest('.fi-sidebar-item') || 83 el.closest('.fi-sidebar-group-item') || 84 el.closest('.fi-sidebar-item-button') || 85 el.closest('li') || 86 el 87 ); 88} 89 90function waitForTourTarget(stepId, attachToSelector = null, maxAttempts = 50, intervalMs = 100) { 91 return new Promise((resolve) => { 92 let attempts = 0; 93 94 const tick = () => { 95 const target = resolveTourTargetElement(stepId, attachToSelector); 96 97 if (target) { 98 resolve(target); 99 return; 100 } 101 102 attempts += 1; 103 104 if (attempts >= maxAttempts) { 105 console.warn(`Tour target not found for step "${stepId}" after ${maxAttempts} attempts`); 106 resolve(null); 107 return; 108 } 109 110 setTimeout(tick, intervalMs); 111 }; 112 113 tick(); 114 }); 115} 116 117/** 118 * Re-apply Shepherd modal cutout and target classes after DOM updates. 119 */ 120function refreshShepherdStepTarget(step) { 121 if (!step?.options?.attachTo || !step.tour?.modal) { 122 return false; 123 } 124 125 const attachOn = step.options.attachTo.on || 'right'; 126 const selector = `[data-tour="${step.id}"]`; 127 const target = resolveTourTargetElement(step.id, selector); 128 129 if (!target) { 130 return false; 131 } 132 133 if (step.target && step.target !== target) { 134 step._updateStepTargetOnHide(); 135 } 136 137 step._resolvedAttachTo = null; 138 step.options.attachTo = { 139 element: target, 140 on: attachOn, 141 }; 142 step._resolveAttachToOptions(); 143 step.target = target; 144 145 if (step.tour.options.useModalOverlay) { 146 step.tour.modal.setupForStep(step); 147 if (typeof step.tour.modal.show === 'function') { 148 step.tour.modal.show(); 149 } 150 } 151 152 step._styleTargetElementForStep(step); 153 154 return true; 155} 156 157function scheduleStepTargetRefresh(step) { 158 if (!step?.options?.attachTo) { 159 return; 160 } 161 162 const run = () => refreshShepherdStepTarget(step); 163 164 run(); 165 requestAnimationFrame(() => { 166 run(); 167 requestAnimationFrame(run); 168 }); 169
170 [100, 350, 700, 1200].forEach((delay) => setTimeout(run, delay)); 171} 172 173function highlightNavForStep(stepId) { 174 document.querySelectorAll('.shepherd-tour-active-nav').forEach((item) => { 175 item.classList.remove('shepherd-tour-active-nav'); 176 }); 177 178 const navItem = document.querySelector(`[data-tour="${stepId}"]`); 179 180 if (!navItem) { 181 return; 182 } 183 184 const navContainer = 185 navItem.closest('.fi-sidebar-item') || 186 navItem.closest('.fi-sidebar-group-item') || 187 navItem.closest('.fi-sidebar-item-button') || 188 navItem.closest('li') || 189 navItem; 190 191 navContainer.classList.add('shepherd-tour-active-nav'); 192 193 if (navItem !== navContainer) { 194 navItem.classList.add('shepherd-tour-active-nav'); 195 } 196 197 navContainer.scrollIntoView({ 198 behavior: 'smooth', 199 block: 'nearest', 200 inline: 'nearest', 201 }); 202} 203 204function waitAfterNavigation() { 205 return new Promise((resolve) => { 206 requestAnimationFrame(() => { 207 requestAnimationFrame(() => { 208 applyTourNavAttributes(); 209 setTimeout(resolve, 200); 210 }); 211 }); 212 }); 213} 214 215/** 216 * Full cleanup when the tour ends (complete / cancel). 217 */ 218function cleanupTourState() { 219 document.querySelectorAll('.shepherd-tour-active-nav').forEach((item) => { 220 item.classList.remove('shepherd-tour-active-nav'); 221 }); 222 223 document.querySelectorAll('.tour-original-active').forEach((item) => { 224 item.classList.remove('tour-original-active'); 225 }); 226 227 document.querySelectorAll('.shepherd-target, .shepherd-enabled').forEach((el) => { 228 el.classList.remove( 229 'shepherd-target', 230 'shepherd-enabled', 231 'shepherd-target-click-disabled', 232 ); 233 }); 234 235 document.querySelectorAll('.shepherd-modal-overlay-container').forEach((el) => { 236 el.remove(); 237 }); 238 239 activeTourInstance = null; 240 syncSidebarActiveState(); 241} 242 243/** 244 * Mark only the sidebar item matching the current URL as active. 245 */ 246function syncSidebarActiveState() { 247 const currentPath = window.location.pathname; 248 249 document.querySelectorAll('.fi-sidebar-item.fi-active, .fi-sidebar-group-item.fi-active').forEach((item) => { 250 item.classList.remove('fi-active'); 251 }); 252 253 document.querySelectorAll('a.fi-sidebar-item-btn[href]').forEach((link) => { 254 let linkPath; 255 256 try { 257 linkPath = new URL(link.href, window.location.origin).pathname; 258 } catch (e) { 259 return; 260 } 261 262 if (linkPath !== currentPath) { 263 return; 264 } 265 266 const item = link.closest('.fi-sidebar-item') || link.closest('.fi-sidebar-group-item'); 267 268 if (item) { 269 item.classList.add('fi-active'); 270 } 271 }); 272} 273 274function hideModalOverlay(step) { 275 if (step?.tour?.modal?.hide) { 276 step.tour.modal.hide(); 277 } 278} 279 280function navigateToStepUrl(stepData) { 281 return new Promise((resolve) => { 282 if (!stepData.url) { 283 resolve(); 284 return; 285 } 286 287 const currentUrl = window.location.pathname; 288 const targetUrl = new URL(stepData.url, window.location.origin).pathname; 289 290 if (currentUrl === targetUrl) { 291 waitAfterNavigation().then(resolve); 292 return; 293 } 294 295 const finish = () => waitAfterNavigation().then(resolve); 296 297 if (typeof Livewire !== 'undefined' && Livewire.navigate) { 298 document.addEventListener('livewire:navigated', function handler() { 299 document.removeEventListener('livewire:navigated', handler); 300 finish(); 301 }, { once: true }); 302 303 Livewire.navigate(stepData.url); 304 return; 305 } 306 307 window.location.href = stepData.url; 308 }); 309} 310 311export function initializeShepherdTour() { 312 const tour = new Shepherd.Tour({ 313 useModalOverlay: true, 314 defaultStepOptions: { 315 classes: 'shepherd-theme-custom', 316 scrollTo: { behavior: 'smooth', block: 'center' }, 317 cancelIcon: { 318 enabled: true, 319 label: window.tourTranslations?.buttons?.cancel || 'Close', 320 }, 321 modalOverlayOpeningRadius: 12, 322 modalOverlayOpeningPadding: 10, 323 }, 324 tourName: 'app-tour', 325 }); 326 327 activeTourInstance = tour; 328 329 tour.on('start', () => { 330 applyTourNavAttributes(); 331 }); 332 333 tour.on('show', (event) => { 334 if (!event.step) { 335 return; 336 } 337 338 localStorage.setItem('shepherd-tour-current-step', event.step.id); 339 localStorage.setItem('shepherd-tour-in-progress', 'true'); 340 }); 341 342 tour.on('complete', () => { 343 localStorage.removeItem('shepherd-tour-current-step'); 344 localStorage.removeItem('shepherd-tour-in-progress'); 345 localStorage.setItem('shepherd-tour-completed', 'true'); 346 localStorage.setItem('shepherd-tour-completed-at', new Date().toISOString()); 347 cleanupTourState(); 348 }); 349 350 tour.on('cancel', () => { 351 localStorage.removeItem('shepherd-tour-current-step'); 352 localStorage.removeItem('shepherd-tour-in-progress'); 353 cleanupTourState(); 354 }); 355 356 const dynamicSteps = window.dynamicTourSteps || []; 357 const welcomeStep = window.customWelcomeStep; 358 const finishStep = window.customFinishStep; 359 360 const allSteps = []; 361 if (welcomeStep) { 362 allSteps.push(welcomeStep); 363 } 364 allSteps.push(...dynamicSteps); 365 if (finishStep) { 366 allSteps.push(finishStep); 367 } 368 369 allSteps.forEach((stepData) => { 370 const stepConfig = { 371 id: stepData.id, 372 title: stepData.title, 373 text: stepData.text, 374 }; 375 376 const attachToSelector = stepData.attachTo || null; 377 378 if (stepData.url || attachToSelector) { 379 stepConfig.beforeShowPromise = function () { 380 return navigateToStepUrl(stepData).then(() => { 381 if (!attachToSelector) { 382 return null; 383 } 384 385 return waitForTourTarget(stepData.id, attachToSelector); 386 }); 387 }; 388 } 389 390 if (attachToSelector) { 391 stepConfig.attachTo = {
392 element() { 393 return resolveTourTargetElement(this.id, attachToSelector); 394 }, 395 on: stepData.position || 'right', 396 }; 397 398 stepConfig.when = { 399 show() { 400 applyTourNavAttributes(); 401 highlightNavForStep(this.id); 402 scheduleStepTargetRefresh(this); 403 }, 404 }; 405 } else { 406 // Welcome / finish: centered tooltip without full-screen dark overlay 407 const existingWhen = stepConfig.when || {}; 408 409 stepConfig.when = { 410 ...existingWhen, 411 show() { 412 if (typeof existingWhen.show === 'function') { 413 existingWhen.show.call(this); 414 } 415 416 hideModalOverlay(this); 417 }, 418 }; 419 } 420 421 const buttons = []; 422 const stepButtons = stepData.buttons || [ 423 { text: window.tourTranslations?.buttons?.previous || 'Previous', action: 'back', secondary: true }, 424 { text: window.tourTranslations?.buttons?.next || 'Next', action: 'next', secondary: false }, 425 ]; 426 427 stepButtons.forEach((btnData) => { 428 const button = { 429 text: btnData.text, 430 secondary: btnData.secondary || false, 431 }; 432 433 if (btnData.action === 'back') { 434 button.action = tour.back; 435 } else if (btnData.action === 'next') { 436 button.action = tour.next; 437 } else if (btnData.action === 'cancel') { 438 button.action = tour.cancel; 439 } else if (btnData.action === 'complete') { 440 button.action = tour.complete; 441 } 442 443 buttons.push(button); 444 }); 445 446 stepConfig.buttons = buttons; 447 tour.addStep(stepConfig); 448 }); 449 450 return tour; 451} 452 453document.addEventListener('DOMContentLoaded', function () { 454 applyTourNavAttributes(); 455 setTimeout(applyTourNavAttributes, 1000); 456 457 document.addEventListener('livewire:navigated', () => { 458 setTimeout(() => { 459 applyTourNavAttributes(); 460 461 const currentStep = activeTourInstance?.getCurrentStep?.(); 462 463 if (currentStep) { 464 highlightNavForStep(currentStep.id); 465 scheduleStepTargetRefresh(currentStep); 466 } 467 }, 100); 468 }); 469 470 const observer = new MutationObserver(function (mutations) { 471 mutations.forEach(function (mutation) { 472 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 473 clearTimeout(window.tourAttributeTimeout); 474 window.tourAttributeTimeout = setTimeout(applyTourNavAttributes, 150); 475 } 476 }); 477 }); 478 479 const sidebar = document.querySelector('.fi-sidebar'); 480 481 if (sidebar) { 482 observer.observe(sidebar, { 483 childList: true, 484 subtree: true, 485 }); 486 } 487 488 document.querySelectorAll('[data-shepherd-tour-trigger]').forEach((button) => { 489 button.addEventListener('click', function (e) { 490 e.preventDefault(); 491 492 try { 493 const tour = initializeShepherdTour(); 494 const inProgress = localStorage.getItem('shepherd-tour-in-progress'); 495 const currentStepId = localStorage.getItem('shepherd-tour-current-step'); 496 497 if (inProgress === 'true' && currentStepId) { 498 tour.show(currentStepId); 499 } else { 500 tour.start(); 501 } 502 } catch (error) { 503 console.error('Error starting tour:', error); 504 alert('An error occurred while starting the tour. Please try again.'); 505 } 506 }); 507 }); 508 509 const inProgress = localStorage.getItem('shepherd-tour-in-progress'); 510 const currentStepId = localStorage.getItem('shepherd-tour-current-step'); 511 512 if (inProgress === 'true' && currentStepId) { 513 setTimeout(() => { 514 try { 515 const tour = initializeShepherdTour(); 516 tour.show(currentStepId); 517 } catch (error) { 518 console.error('Error auto-resuming tour:', error); 519 localStorage.removeItem('shepherd-tour-in-progress'); 520 localStorage.removeItem('shepherd-tour-current-step'); 521 } 522 }, 1500); 523 } 524}); 525 526export default initializeShepherdTour;
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.