/** * Tour Manager * Orchestrates the onboarding tour using Driver.js */ (function(window) { 'use strict'; class TourManager { constructor(progressTracker, themeAdapter, dnsTemplateSelector) { this.progressTracker = progressTracker; this.themeAdapter = themeAdapter; this.dnsTemplateSelector = dnsTemplateSelector; this.driver = null; this.currentStepIndex = 0; this.isActive = false; this.resizeHandler = null; this.layoutChangeHandler = null; } /** * Initialize Driver.js with theme-aware configuration */ async initializeDriver() { // Driver.js v1.x IIFE: window.driver.js.driver is the factory function const driverFactory = window.driver?.js?.driver || window.driver?.driver || window.driver; if (typeof driverFactory !== 'function') { console.error('[TourManager] Driver.js not loaded or invalid. window.driver:', window.driver); return false; } const themeConfig = this.themeAdapter.getDriverTheme(); this.driver = driverFactory({ showProgress: true, showButtons: ['next', 'previous', 'close'], allowClose: true, overlayClickNext: false, overlayOpacity: 0.6, stagePadding: 12, stageRadius: 12, allowKeyboardControl: true, popoverClass: 'dashcaddy-popover', animate: true, smoothScroll: true, onDestroyed: () => this.onTourComplete(), onDestroyStarted: () => { if (!this.progressTracker.isTourCompleted()) { this.onTourSkip(); } } }); // Apply theme this.themeAdapter.applyTheme(this.driver); // Listen for theme changes this.themeAdapter.onThemeChange(() => { this.themeAdapter.applyTheme(this.driver); }); // Set up dynamic repositioning this.setupDynamicRepositioning(); return true; } /** * Check if tour should auto-start */ shouldAutoStart() { return !this.progressTracker.isTourCompleted() && this.progressTracker.getCurrentStep() === 0; } /** * Start the onboarding tour */ async startTour() { if (!this.driver) { const initialized = await this.initializeDriver(); if (!initialized) return; } // Get active tooltips (filtered by conditions) const allTooltips = window.TooltipDefinitions.getSortedTooltips(); // Filter out completed tooltips const completedIds = this.progressTracker.getCompletedTooltips(); const activeTooltips = allTooltips.filter(t => !completedIds.includes(t.id)); if (activeTooltips.length === 0) { console.log('[TourManager] No tooltips to show'); this.progressTracker.markTourCompleted(); return; } // Convert to Driver.js steps with navigation logic const steps = activeTooltips.map((tooltip, index) => { const isFirst = index === 0; const isLast = index === activeTooltips.length - 1; const step = { element: tooltip.element, popover: { title: tooltip.popover.title, description: tooltip.popover.description, side: tooltip.popover.position || 'bottom', align: tooltip.popover.align || 'start', showButtons: this._getButtonsForStep(tooltip, isFirst, isLast), showProgress: tooltip.popover.showProgress !== false, onNextClick: () => { this.progressTracker.markTooltipCompleted(tooltip.id); this.progressTracker.setCurrentStep(index + 1); this.currentStepIndex = index + 1; this.driver.moveNext(); }, onPrevClick: () => { this.progressTracker.setCurrentStep(Math.max(0, index - 1)); this.currentStepIndex = Math.max(0, index - 1); this.driver.movePrevious(); }, onCloseClick: () => { this.skipTour(); } } }; // Add custom handlers for DNS tooltip if (tooltip.id === 'dns-priority' && this.dnsTemplateSelector) { step.popover.onSetupNowClick = () => { console.log('[TourManager] Opening DNS template selector'); this.dnsTemplateSelector.showTemplateSelector(); // Mark tooltip as completed and move to next this.progressTracker.markTooltipCompleted(tooltip.id); this.progressTracker.setCurrentStep(index + 1); this.currentStepIndex = index + 1; this.driver.moveNext(); }; step.popover.onLaterClick = () => { console.log('[TourManager] DNS setup deferred'); this.progressTracker.markDnsSetupDeferred(); // Mark tooltip as completed and move to next this.progressTracker.markTooltipCompleted(tooltip.id); this.progressTracker.setCurrentStep(index + 1); this.currentStepIndex = index + 1; this.driver.moveNext(); }; } return step; }); this.isActive = true; this.driver.setSteps(steps); this.driver.drive(); } /** * Resume tour from last step */ async resumeTour() { const currentStep = this.progressTracker.getCurrentStep(); if (currentStep > 0) { await this.startTour(); // Driver.js will start from beginning, we'd need to skip to current step // This is a simplified implementation } else { await this.startTour(); } } /** * Skip the entire tour */ skipTour() { if (this.driver) { this.driver.destroy(); } this.cleanupDynamicRepositioning(); this.isActive = false; } /** * Restart tour from beginning */ async restartTour() { this.progressTracker.resetProgress(); await this.startTour(); } /** * Show a specific tooltip by ID */ async showTooltip(tooltipId) { const tooltip = window.TooltipDefinitions.getTooltipById(tooltipId); if (!tooltip) { console.error(`[TourManager] Tooltip not found: ${tooltipId}`); return; } if (!this.driver) { await this.initializeDriver(); } const step = { element: tooltip.element, popover: { title: tooltip.popover.title, description: tooltip.popover.description, side: tooltip.popover.position || 'bottom', align: tooltip.popover.align || 'start' } }; this.driver.highlight(step); } /** * Show "What's New" tour - only tooltips marked as new features */ async showWhatsNew() { if (!this.driver) { const initialized = await this.initializeDriver(); if (!initialized) return; } // Get only new feature tooltips const newFeatureTooltips = window.TooltipDefinitions.getNewFeatureTooltips(); if (newFeatureTooltips.length === 0) { console.log('[TourManager] No new features to show'); return; } console.log(`[TourManager] Showing ${newFeatureTooltips.length} new features`); // Convert to Driver.js steps const steps = newFeatureTooltips.map((tooltip, index) => { const isFirst = index === 0; const isLast = index === newFeatureTooltips.length - 1; return { element: tooltip.element, popover: { title: `✨ NEW: ${tooltip.popover.title}`, description: tooltip.popover.description, side: tooltip.popover.position || 'bottom', align: tooltip.popover.align || 'start', showButtons: this._getButtonsForStep(tooltip, isFirst, isLast), showProgress: true, onNextClick: () => { this.driver.moveNext(); }, onPrevClick: () => { this.driver.movePrevious(); }, onCloseClick: () => { this.skipTour(); } } }; }); this.isActive = true; this.driver.setSteps(steps); this.driver.drive(); } /** * Set up dynamic repositioning for window resize and layout changes */ setupDynamicRepositioning() { // Window resize handler with debouncing let resizeTimeout; this.resizeHandler = () => { clearTimeout(resizeTimeout); resizeTimeout = setTimeout(() => { if (this.isActive && this.driver) { console.log('[TourManager] Window resized, repositioning tooltip'); this.driver.refresh(); } }, 150); // Debounce for 150ms }; // Layout change handler (for theme changes, DOM mutations) this.layoutChangeHandler = () => { if (this.isActive && this.driver) { console.log('[TourManager] Layout changed, repositioning tooltip'); // Small delay to allow layout to settle setTimeout(() => { if (this.driver) { this.driver.refresh(); } }, 100); } }; // Add event listeners window.addEventListener('resize', this.resizeHandler); // Listen for theme changes (already handled by ThemeAdapter, but also trigger reposition) this.themeAdapter.onThemeChange(this.layoutChangeHandler); } /** * Clean up dynamic repositioning listeners */ cleanupDynamicRepositioning() { if (this.resizeHandler) { window.removeEventListener('resize', this.resizeHandler); } } /** * Get buttons to show for a specific step * @private */ _getButtonsForStep(tooltip, isFirst, isLast) { // Check if tooltip has custom buttons defined if (tooltip.popover.showButtons) { return tooltip.popover.showButtons; } // Default button configuration const buttons = []; if (!isFirst) { buttons.push('previous'); } if (!isLast) { buttons.push('next'); } else { buttons.push('close'); } return buttons; } /** * Handle tour completion */ onTourComplete() { this.progressTracker.markTourCompleted(); this.isActive = false; console.log('[TourManager] Tour completed'); } /** * Handle tour skip */ onTourSkip() { // Save current progress but don't mark as completed console.log('[TourManager] Tour skipped'); this.isActive = false; } } window.TourManager = TourManager; console.log('[TourManager] Module loaded'); })(window);