Persist onboardingCompleted flag server-side via /api/v1/config so the tour only auto-starts once per DashCaddy installation, not on every new browser that connects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
367 lines
11 KiB
JavaScript
367 lines
11 KiB
JavaScript
/**
|
|
* 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.isInstallOnboardingCompleted() &&
|
|
!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);
|