271 lines
8.0 KiB
JavaScript
271 lines
8.0 KiB
JavaScript
/**
|
|
* UI Utilities Module
|
|
* Toast notifications, context menu, dark mode, file upload dialog
|
|
*/
|
|
|
|
/**
|
|
* Show toast notification
|
|
*/
|
|
function showNotification(message, type = 'info') {
|
|
const container = document.getElementById('toastContainer') || createToastContainer();
|
|
|
|
const toast = document.createElement('div');
|
|
const bgClass = type === 'error' ? 'danger' : type === 'success' ? 'success' : type === 'warning' ? 'warning' : 'primary';
|
|
toast.className = `toast align-items-center text-white bg-${bgClass} border-0`;
|
|
toast.setAttribute('role', 'alert');
|
|
|
|
toast.innerHTML = `
|
|
<div class="d-flex">
|
|
<div class="toast-body">${message}</div>
|
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
|
</div>
|
|
`;
|
|
|
|
container.appendChild(toast);
|
|
|
|
const bsToast = new bootstrap.Toast(toast, { delay: 3000 });
|
|
bsToast.show();
|
|
|
|
toast.addEventListener('hidden.bs.toast', () => {
|
|
toast.remove();
|
|
});
|
|
}
|
|
|
|
function createToastContainer() {
|
|
const container = document.createElement('div');
|
|
container.id = 'toastContainer';
|
|
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
|
container.style.zIndex = '9999';
|
|
document.body.appendChild(container);
|
|
return container;
|
|
}
|
|
|
|
/**
|
|
* Enhanced Context Menu
|
|
*/
|
|
function showContextMenu(x, y, target) {
|
|
const menu = document.getElementById('contextMenu');
|
|
if (!menu) return;
|
|
|
|
// Store target data
|
|
menu.dataset.targetPath = target.path;
|
|
menu.dataset.targetIsDir = target.isDir;
|
|
|
|
// Show/hide menu items based on target type
|
|
const items = {
|
|
'new-file': target.isDir,
|
|
'new-folder': target.isDir,
|
|
'upload': target.isDir,
|
|
'download': true,
|
|
'paste': target.isDir && window.fileTreeActions?.clipboard,
|
|
'open': !target.isDir
|
|
};
|
|
|
|
Object.entries(items).forEach(([action, show]) => {
|
|
const item = menu.querySelector(`[data-action="${action}"]`);
|
|
if (item) {
|
|
item.style.display = show ? 'flex' : 'none';
|
|
}
|
|
});
|
|
|
|
// Position menu
|
|
menu.style.display = 'block';
|
|
menu.style.left = x + 'px';
|
|
menu.style.top = y + 'px';
|
|
|
|
// Adjust if off-screen
|
|
setTimeout(() => {
|
|
const rect = menu.getBoundingClientRect();
|
|
if (rect.right > window.innerWidth) {
|
|
menu.style.left = (window.innerWidth - rect.width - 10) + 'px';
|
|
}
|
|
if (rect.bottom > window.innerHeight) {
|
|
menu.style.top = (window.innerHeight - rect.height - 10) + 'px';
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
function hideContextMenu() {
|
|
const menu = document.getElementById('contextMenu');
|
|
if (menu) {
|
|
menu.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Combined click handler for context menu and outside clicks
|
|
document.addEventListener('click', async (e) => {
|
|
const menuItem = e.target.closest('.context-menu-item');
|
|
|
|
if (menuItem) {
|
|
// Handle context menu item click
|
|
const action = menuItem.dataset.action;
|
|
const menu = document.getElementById('contextMenu');
|
|
const targetPath = menu.dataset.targetPath;
|
|
const isDir = menu.dataset.targetIsDir === 'true';
|
|
|
|
hideContextMenu();
|
|
|
|
if (window.fileTreeActions) {
|
|
await window.fileTreeActions.execute(action, targetPath, isDir);
|
|
}
|
|
} else if (!e.target.closest('#contextMenu') && !e.target.closest('.tree-node')) {
|
|
// Hide on outside click
|
|
hideContextMenu();
|
|
}
|
|
});
|
|
|
|
/**
|
|
* File Upload Dialog
|
|
*/
|
|
function showFileUploadDialog(targetPath, onUpload) {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.multiple = true;
|
|
|
|
input.addEventListener('change', async (e) => {
|
|
const files = Array.from(e.target.files);
|
|
if (files.length === 0) return;
|
|
|
|
for (const file of files) {
|
|
try {
|
|
await onUpload(targetPath, file);
|
|
} catch (error) {
|
|
console.error('Upload failed:', error);
|
|
}
|
|
}
|
|
});
|
|
|
|
input.click();
|
|
}
|
|
|
|
/**
|
|
* Dark Mode Manager
|
|
*/
|
|
class DarkMode {
|
|
constructor() {
|
|
this.isDark = localStorage.getItem('darkMode') === 'true';
|
|
this.apply();
|
|
}
|
|
|
|
toggle() {
|
|
this.isDark = !this.isDark;
|
|
localStorage.setItem('darkMode', this.isDark);
|
|
this.apply();
|
|
}
|
|
|
|
apply() {
|
|
if (this.isDark) {
|
|
document.body.classList.add('dark-mode');
|
|
const btn = document.getElementById('darkModeBtn');
|
|
if (btn) btn.textContent = '☀️';
|
|
|
|
// Update mermaid theme
|
|
if (window.mermaid) {
|
|
mermaid.initialize({ theme: 'dark' });
|
|
}
|
|
} else {
|
|
document.body.classList.remove('dark-mode');
|
|
const btn = document.getElementById('darkModeBtn');
|
|
if (btn) btn.textContent = '🌙';
|
|
|
|
// Update mermaid theme
|
|
if (window.mermaid) {
|
|
mermaid.initialize({ theme: 'default' });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collection Selector
|
|
*/
|
|
class CollectionSelector {
|
|
constructor(selectId, webdavClient) {
|
|
this.select = document.getElementById(selectId);
|
|
this.webdavClient = webdavClient;
|
|
this.onChange = null;
|
|
}
|
|
|
|
async load() {
|
|
try {
|
|
const collections = await this.webdavClient.getCollections();
|
|
this.select.innerHTML = '';
|
|
|
|
collections.forEach(collection => {
|
|
const option = document.createElement('option');
|
|
option.value = collection;
|
|
option.textContent = collection;
|
|
this.select.appendChild(option);
|
|
});
|
|
|
|
// Select first collection
|
|
if (collections.length > 0) {
|
|
this.select.value = collections[0];
|
|
this.webdavClient.setCollection(collections[0]);
|
|
if (this.onChange) {
|
|
this.onChange(collections[0]);
|
|
}
|
|
}
|
|
|
|
// Add change listener
|
|
this.select.addEventListener('change', () => {
|
|
const collection = this.select.value;
|
|
this.webdavClient.setCollection(collection);
|
|
if (this.onChange) {
|
|
this.onChange(collection);
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load collections:', error);
|
|
showNotification('Failed to load collections', 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Editor Drop Handler
|
|
* Handles file drops into the editor
|
|
*/
|
|
class EditorDropHandler {
|
|
constructor(editorElement, onFileDrop) {
|
|
this.editorElement = editorElement;
|
|
this.onFileDrop = onFileDrop;
|
|
this.setupHandlers();
|
|
}
|
|
|
|
setupHandlers() {
|
|
this.editorElement.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.editorElement.classList.add('drag-over');
|
|
});
|
|
|
|
this.editorElement.addEventListener('dragleave', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.editorElement.classList.remove('drag-over');
|
|
});
|
|
|
|
this.editorElement.addEventListener('drop', async (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.editorElement.classList.remove('drag-over');
|
|
|
|
const files = Array.from(e.dataTransfer.files);
|
|
if (files.length === 0) return;
|
|
|
|
for (const file of files) {
|
|
try {
|
|
if (this.onFileDrop) {
|
|
await this.onFileDrop(file);
|
|
}
|
|
} catch (error) {
|
|
console.error('Drop failed:', error);
|
|
showNotification(`Failed to upload ${file.name}`, 'error');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|