/** * DITTO Application JavaScript * Modern ES6+ syntax with proper error handling */ document.addEventListener('DOMContentLoaded', () => { console.log('App script loaded.'); // Initialize Tiptap editor if element exists const editorElement = document.getElementById('editor'); if (editorElement) { initTiptapEditor(editorElement); } // Add Bootstrap tooltips var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl); }); // Close flash messages after 5 seconds setTimeout(function () { var alerts = document.querySelectorAll('.alert:not(.alert-persistent)'); alerts.forEach(function (alert) { var bsAlert = bootstrap.Alert.getInstance(alert); if (bsAlert) { bsAlert.close(); } else { alert.classList.add('fade'); setTimeout(function () { alert.remove(); }, 150); } }); }, 5000); // Add event listener for subnet scan buttons with HTMX document.body.addEventListener('htmx:afterOnLoad', function (event) { if (event.detail.xhr.status === 200) { showNotification('Subnet scan started successfully', 'success'); } }); // Add markdown preview for documentation fields const docTextareas = document.querySelectorAll('textarea[name="documentation"]'); docTextareas.forEach(function (textarea) { // Only if preview container exists const previewContainer = document.getElementById('markdown-preview'); if (previewContainer && textarea) { textarea.addEventListener('input', function () { // Use the server to render the markdown (safer) fetch('/api/markdown-preview', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content: textarea.value }) }) .then(response => response.json()) .then(data => { previewContainer.innerHTML = data.html; }); }); } }); // Wait for DOM to be fully loaded document.addEventListener('DOMContentLoaded', function () { // Initialize theme toggle initThemeToggle(); // Initialize clipboard functionality initClipboard(); // Initialize port map tooltips initTooltips(); // Initialize mobile sidebar initMobileSidebar(); // Initialize notifications initNotifications(); }); // Initialize Bootstrap components initBootstrapComponents(); // Setup sidebar toggle functionality setupSidebar(); // Add form validation setupFormValidation(); }); /** * Initialize Bootstrap components */ function initBootstrapComponents() { // Initialize all tooltips const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); tooltipTriggerList.forEach(el => { try { new bootstrap.Tooltip(el); } catch (e) { console.warn('Error initializing tooltip:', e); } }); // Initialize all popovers const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]'); popoverTriggerList.forEach(el => { try { new bootstrap.Popover(el); } catch (e) { console.warn('Error initializing popover:', e); } }); } /** * Setup sidebar toggle functionality */ function setupSidebar() { const sidebarToggler = document.querySelector('.sidebar-toggler'); const sidebar = document.querySelector('.sidebar'); if (sidebarToggler && sidebar) { sidebarToggler.addEventListener('click', () => { sidebar.classList.toggle('show'); }); // Close sidebar when clicking outside on mobile document.addEventListener('click', (event) => { const isClickInside = sidebar.contains(event.target) || sidebarToggler.contains(event.target); if (!isClickInside && sidebar.classList.contains('show') && window.innerWidth < 992) { sidebar.classList.remove('show'); } }); } } /** * Setup form validation */ function setupFormValidation() { // Add custom validation for forms const forms = document.querySelectorAll('.needs-validation'); forms.forEach(form => { form.addEventListener('submit', event => { if (!form.checkValidity()) { event.preventDefault(); event.stopPropagation(); } form.classList.add('was-validated'); }, false); }); } /** * Safe query selector with error handling * @param {string} selector - CSS selector * @param {Element} parent - Parent element (optional) * @returns {Element|null} - The selected element or null */ function $(selector, parent = document) { try { return parent.querySelector(selector); } catch (e) { console.warn(`Error selecting "${selector}":`, e); return null; } } /** * Format date for display * @param {string|Date} date - Date to format * @returns {string} - Formatted date string */ function formatDate(date) { try { const d = new Date(date); return d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } catch (e) { console.warn('Error formatting date:', e); return String(date); } } function initTiptapEditor(element) { // Load required Tiptap scripts const editorContainer = document.getElementById('editor-container'); const preview = document.getElementById('markdown-preview'); // Initialize the Tiptap editor const { Editor } = window.tiptap; const { StarterKit } = window.tiptapExtensions; const editor = new Editor({ element: element, extensions: [ StarterKit ], content: element.getAttribute('data-content') || '', onUpdate: ({ editor }) => { // Update preview with current content if (preview) { const markdown = editor.getHTML(); fetch('/api/markdown-preview', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ markdown: markdown }) }) .then(response => response.json()) .then(data => { preview.innerHTML = data.html; }); } } }); // Store editor reference window.editor = editor; // Form submission handling const form = element.closest('form'); if (form) { form.addEventListener('submit', () => { const contentInput = form.querySelector('input[name="content"]'); if (contentInput) { contentInput.value = editor.getHTML(); } }); } } // Copy to clipboard function function copyToClipboard(text) { navigator.clipboard.writeText(text).then(function () { // Success notification showNotification('Copied to clipboard!', 'success'); }, function (err) { // Error notification showNotification('Could not copy text', 'danger'); }); } // Show notification function showNotification(message, type = 'info') { const notificationArea = document.getElementById('notification-area'); if (!notificationArea) return; const notification = document.createElement('div'); notification.className = `alert alert-${type} alert-dismissible fade show`; notification.innerHTML = ` ${message} `; notificationArea.appendChild(notification); // Auto-remove after 5 seconds setTimeout(() => { if (notification.parentNode) { notification.remove(); } }, 5000); } function initThemeToggle() { const themeToggle = document.getElementById('theme-toggle'); if (themeToggle) { themeToggle.addEventListener('click', function () { const currentTheme = document.documentElement.getAttribute('data-bs-theme') || 'light'; const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-bs-theme', newTheme); localStorage.setItem('theme', newTheme); console.log(`Theme switched to ${newTheme} mode`); }); } // Load saved theme or use OS preference const storedTheme = localStorage.getItem('theme'); if (storedTheme) { document.documentElement.setAttribute('data-bs-theme', storedTheme); } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { document.documentElement.setAttribute('data-bs-theme', 'dark'); localStorage.setItem('theme', 'dark'); } } function initClipboard() { // Add click handlers to any clipboard copy buttons document.querySelectorAll('.copy-btn').forEach(btn => { btn.addEventListener('click', function () { const textToCopy = this.getAttribute('data-clipboard-text'); if (textToCopy) { navigator.clipboard.writeText(textToCopy) .then(() => { showNotification('Copied to clipboard!', 'success'); }) .catch(err => { console.error('Failed to copy: ', err); }); } }); }); } function initTooltips() { const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]'); if (tooltips.length > 0) { Array.from(tooltips).map(tooltipNode => new bootstrap.Tooltip(tooltipNode)); } } function initMobileSidebar() { // Sidebar toggle for mobile const sidebarToggler = document.querySelector('.sidebar-toggler'); if (sidebarToggler) { sidebarToggler.addEventListener('click', function () { document.querySelector('.sidebar').classList.toggle('show'); document.querySelector('.main-content').classList.toggle('sidebar-open'); }); } } function initNotifications() { // Add flash messages as notifications const flashMessages = document.querySelectorAll('.alert.flash-message'); flashMessages.forEach(message => { setTimeout(() => { const bsAlert = new bootstrap.Alert(message); bsAlert.close(); }, 5000); }); } // For random port suggestion async function suggestRandomPort(serverId) { try { const response = await fetch(`/api/servers/${serverId}/suggest_port`); if (!response.ok) throw new Error('Failed to get port suggestion'); const data = await response.json(); if (data.port) { // Copy to clipboard navigator.clipboard.writeText(data.port.toString()) .then(() => { showNotification(`Port ${data.port} copied to clipboard!`, 'success'); }) .catch(err => { console.error('Failed to copy: ', err); showNotification(`Suggested free port: ${data.port}`, 'info'); }); } return data.port; } catch (error) { console.error('Error:', error); showNotification('Failed to suggest port', 'danger'); return null; } }