376 lines
12 KiB
JavaScript
376 lines
12 KiB
JavaScript
/**
|
|
* 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}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
`;
|
|
|
|
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;
|
|
}
|
|
}
|