/** * Tooltip Definitions * Defines all tooltip content, positioning, and behavior for the onboarding system */ (function(window) { 'use strict'; /** * Validate a tooltip definition * @param {Object} tooltip - The tooltip definition to validate * @returns {Object} { valid: boolean, errors: string[] } */ function validateTooltipDefinition(tooltip) { const errors = []; // Required fields if (!tooltip.id || typeof tooltip.id !== 'string') { errors.push('Tooltip must have a valid string id'); } if (!tooltip.element) { errors.push('Tooltip must have an element selector or HTMLElement'); } if (!tooltip.popover || typeof tooltip.popover !== 'object') { errors.push('Tooltip must have a popover object'); } else { // Validate popover fields if (!tooltip.popover.title || typeof tooltip.popover.title !== 'string') { errors.push('Tooltip popover must have a valid string title'); } if (!tooltip.popover.description || typeof tooltip.popover.description !== 'string') { errors.push('Tooltip popover must have a valid string description'); } // Validate position if provided if (tooltip.popover.position) { const validPositions = ['top', 'bottom', 'left', 'right', 'center']; if (!validPositions.includes(tooltip.popover.position)) { errors.push(`Invalid position: ${tooltip.popover.position}. Must be one of: ${validPositions.join(', ')}`); } } // Validate align if provided if (tooltip.popover.align) { const validAligns = ['start', 'center', 'end']; if (!validAligns.includes(tooltip.popover.align)) { errors.push(`Invalid align: ${tooltip.popover.align}. Must be one of: ${validAligns.join(', ')}`); } } // Validate showButtons if provided if (tooltip.popover.showButtons && !Array.isArray(tooltip.popover.showButtons)) { errors.push('showButtons must be an array'); } // Validate callbacks if provided const callbacks = ['onNext', 'onPrevious', 'onClose', 'onSetupNow', 'onLater']; callbacks.forEach(callback => { if (tooltip.popover[callback] && typeof tooltip.popover[callback] !== 'function') { errors.push(`${callback} must be a function`); } }); } // Validate condition if provided if (tooltip.condition && typeof tooltip.condition !== 'function') { errors.push('condition must be a function'); } // Validate priority if provided if (tooltip.priority !== undefined && typeof tooltip.priority !== 'number') { errors.push('priority must be a number'); } return { valid: errors.length === 0, errors }; } /** * Validate an array of tooltip definitions * @param {Array} tooltips - Array of tooltip definitions * @returns {Object} { valid: boolean, errors: Object[] } */ function validateTooltipDefinitions(tooltips) { if (!Array.isArray(tooltips)) { return { valid: false, errors: [{ tooltip: null, errors: ['tooltips must be an array'] }] }; } const allErrors = []; const ids = new Set(); tooltips.forEach((tooltip, index) => { const validation = validateTooltipDefinition(tooltip); if (!validation.valid) { allErrors.push({ tooltip: tooltip.id || `index ${index}`, errors: validation.errors }); } // Check for duplicate IDs if (tooltip.id) { if (ids.has(tooltip.id)) { allErrors.push({ tooltip: tooltip.id, errors: [`Duplicate tooltip ID: ${tooltip.id}`] }); } ids.add(tooltip.id); } }); return { valid: allErrors.length === 0, errors: allErrors }; } /** * Error handler for tooltip system */ class TooltipError extends Error { constructor(message, tooltipId = null) { super(message); this.name = 'TooltipError'; this.tooltipId = tooltipId; } } /** * Handle tooltip definition errors * @param {Object} validation - Validation result * @throws {TooltipError} If validation fails */ function handleValidationErrors(validation) { if (!validation.valid) { const errorMessages = validation.errors.map(e => `${e.tooltip}: ${e.errors.join(', ')}` ).join('\n'); console.error('[TooltipDefinitions] Validation errors:', errorMessages); throw new TooltipError(`Tooltip validation failed:\n${errorMessages}`); } } // Export to global scope window.TooltipValidation = { validateTooltipDefinition, validateTooltipDefinitions, handleValidationErrors, TooltipError }; console.log('[TooltipDefinitions] Validation module loaded'); })(window); /** * Tooltip Definitions Array * Defines all tooltips for the onboarding tour */ const TOOLTIP_DEFINITIONS = [ // 1. Welcome tooltip pointing to logo { id: 'welcome', element: '#brand', popover: { title: 'Welcome to DashCaddy!', description: `
Your personal dashboard for managing services with Caddy reverse proxy.
Let's take a quick tour to help you get started.
Tip: You can customize this logo in Settings.
`, position: 'bottom', align: 'start', showButtons: ['next'], showProgress: true }, priority: 1, isNewFeature: false }, // 2. Add Service button { id: 'add-service', element: '#add-service-btn', popover: { title: 'Adding New Services', description: `Click + Add Service to deploy new apps or add existing services to your dashboard.
Choose from 50+ templates including:
This is your service grid where all your deployed applications appear.
Each card shows:
DashCaddy comes with 7 themes. Click here to switch between them.
Your preference is saved automatically.
`, position: 'bottom', showButtons: ['previous', 'close'], showProgress: true }, priority: 4, isNewFeature: false, condition: () => { return document.getElementById('theme') !== null; } } ]; /** * Get tooltip definitions * @returns {Array} Array of tooltip definitions */ function getTooltipDefinitions() { return TOOLTIP_DEFINITIONS; } /** * Get a specific tooltip by ID * @param {string} id - Tooltip ID * @returns {Object|null} Tooltip definition or null if not found */ function getTooltipById(id) { return TOOLTIP_DEFINITIONS.find(t => t.id === id) || null; } /** * Get tooltips filtered by condition * @returns {Array} Array of tooltips that pass their condition check */ function getActiveTooltips() { return TOOLTIP_DEFINITIONS.filter(tooltip => { if (tooltip.condition && typeof tooltip.condition === 'function') { try { return tooltip.condition(); } catch (error) { console.error(`[TooltipDefinitions] Error evaluating condition for ${tooltip.id}:`, error); return false; } } return true; }); } /** * Get tooltips sorted by priority * @returns {Array} Array of tooltips sorted by priority (ascending) */ function getSortedTooltips() { const tooltips = getActiveTooltips(); return tooltips.sort((a, b) => { const priorityA = a.priority || 999; const priorityB = b.priority || 999; return priorityA - priorityB; }); } /** * Get tooltips marked as new features * @returns {Array} Array of tooltips marked with isNewFeature flag */ function getNewFeatureTooltips() { const tooltips = getActiveTooltips(); return tooltips.filter(tooltip => tooltip.isNewFeature === true) .sort((a, b) => { const priorityA = a.priority || 999; const priorityB = b.priority || 999; return priorityA - priorityB; }); } // Export to global scope window.TooltipDefinitions = { TOOLTIP_DEFINITIONS, getTooltipDefinitions, getTooltipById, getActiveTooltips, getSortedTooltips, getNewFeatureTooltips }; console.log('[TooltipDefinitions] Definitions loaded:', TOOLTIP_DEFINITIONS.length, 'tooltips');