perf: optimize UI rendering and event lifecycle

This commit is contained in:
Diocrafts
2026-02-18 10:53:43 +01:00
parent f661282962
commit 0d7c8bc019
28 changed files with 1832 additions and 766 deletions
+192 -215
View File
@@ -46,6 +46,172 @@ const elements = {
// Will be populated on initialization
};
// Upload dropdown listener state (prevents accumulated listeners)
let uploadDropdownDocumentClickHandler = null;
let uploadDropdownBindingsController = null;
let actionsBarDelegationBound = false;
const ACTIONS_BAR_TEMPLATES = {
files: `
<div class="action-buttons">
<div class="upload-dropdown" id="upload-dropdown">
<button class="btn btn-primary" id="upload-btn">
<i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i>
<span data-i18n="actions.upload">Upload</span>
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
</button>
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
<button class="upload-dropdown-item" id="upload-files-btn">
<i class="fas fa-file"></i>
<span data-i18n="actions.upload_files">Upload files</span>
</button>
<button class="upload-dropdown-item" id="upload-folder-btn">
<i class="fas fa-folder-open"></i>
<span data-i18n="actions.upload_folder">Upload folder</span>
</button>
</div>
</div>
<button class="btn btn-secondary" id="new-folder-btn">
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i>
<span data-i18n="actions.new_folder">New folder</span>
</button>
</div>
<div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i>
</button>
<button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i>
</button>
</div>
`,
trash: `
<div class="action-buttons">
<button class="btn btn-danger" id="empty-trash-btn">
<i class="fas fa-trash-alt"></i>
<span data-i18n="trash.empty_trash">Empty trash</span>
</button>
</div>
`,
favorites: `
<div class="action-buttons"></div>
<div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i>
</button>
<button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i>
</button>
</div>
`,
recent: `
<div class="action-buttons">
<button class="btn btn-secondary" id="clear-recent-btn">
<i class="fas fa-broom" style="margin-right: 5px;"></i>
<span data-i18n="actions.clear_recent">Clear recent</span>
</button>
</div>
<div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i>
</button>
<button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i>
</button>
</div>
`
};
function setActionsBarMode(mode, force = false) {
if (!elements.actionsBar) return;
if (mode === 'hidden') {
elements.actionsBar.style.display = 'none';
elements.actionsBar.dataset.mode = 'hidden';
return;
}
if (!force && elements.actionsBar.dataset.mode === mode) {
return;
}
const html = ACTIONS_BAR_TEMPLATES[mode];
if (!html) return;
elements.actionsBar.innerHTML = html;
elements.actionsBar.style.display = 'flex';
elements.actionsBar.dataset.mode = mode;
// Refresh cached action elements after rebuild
elements.uploadBtn = document.getElementById('upload-btn');
elements.newFolderBtn = document.getElementById('new-folder-btn');
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
if (window.i18n && window.i18n.translateElement) {
window.i18n.translateElement(elements.actionsBar);
}
if (mode === 'files') {
setupUploadDropdown();
}
}
function setupActionsBarDelegation() {
if (actionsBarDelegationBound || !elements.actionsBar) return;
actionsBarDelegationBound = true;
elements.actionsBar.addEventListener('click', async (e) => {
const btn = e.target.closest('button');
if (!btn) return;
switch (btn.id) {
case 'upload-files-btn': {
e.stopPropagation();
const menu = document.getElementById('upload-dropdown-menu');
if (menu) menu.classList.remove('show');
if (elements.fileInput) elements.fileInput.click();
break;
}
case 'upload-folder-btn': {
e.stopPropagation();
const menu = document.getElementById('upload-dropdown-menu');
if (menu) menu.classList.remove('show');
const folderInput = document.getElementById('folder-input');
if (folderInput) folderInput.click();
break;
}
case 'new-folder-btn': {
const folderName = await window.Modal.promptNewFolder();
if (folderName) {
fileOps.createFolder(folderName);
}
break;
}
case 'grid-view-btn':
ui.switchToGridView();
break;
case 'list-view-btn':
ui.switchToListView();
break;
case 'empty-trash-btn':
if (await fileOps.emptyTrash()) {
loadTrashItems();
}
break;
case 'clear-recent-btn':
if (window.recent) {
window.recent.clearRecentFiles();
window.recent.displayRecentFiles();
window.ui.showNotification('Cleanup completed', 'Recent files history has been cleared');
}
break;
default:
break;
}
});
}
/**
* Initialize the application
*/
@@ -328,13 +494,17 @@ async function fetchAppVersion() {
* Handles opening/closing the dropdown and triggering file/folder inputs
*/
function setupUploadDropdown() {
const dropdown = document.getElementById('upload-dropdown');
const uploadBtn = document.getElementById('upload-btn');
const menu = document.getElementById('upload-dropdown-menu');
const uploadFilesBtn = document.getElementById('upload-files-btn');
const uploadFolderBtn = document.getElementById('upload-folder-btn');
if (!uploadBtn || !menu) return;
// Abort any previous local bindings (safe across repeated/rebuilt UI)
if (uploadDropdownBindingsController) {
uploadDropdownBindingsController.abort();
}
uploadDropdownBindingsController = new AbortController();
const signal = uploadDropdownBindingsController.signal;
// Toggle dropdown on button click
uploadBtn.addEventListener('click', (e) => {
@@ -345,33 +515,18 @@ function setupUploadDropdown() {
if (!isOpen) {
menu.classList.add('show');
}
});
// Upload files option
if (uploadFilesBtn) {
uploadFilesBtn.addEventListener('click', (e) => {
e.stopPropagation();
menu.classList.remove('show');
elements.fileInput.click();
});
}
// Upload folder option
if (uploadFolderBtn) {
uploadFolderBtn.addEventListener('click', (e) => {
e.stopPropagation();
menu.classList.remove('show');
const folderInput = document.getElementById('folder-input');
if (folderInput) {
folderInput.click();
}
});
}
}, { signal });
// Close dropdown when clicking outside
document.addEventListener('click', () => {
// remove+add stable handler: guarantees exactly one global listener
if (uploadDropdownDocumentClickHandler) {
document.removeEventListener('click', uploadDropdownDocumentClickHandler);
}
uploadDropdownDocumentClickHandler = (e) => {
if (e.target.closest('#upload-dropdown')) return;
document.querySelectorAll('.upload-dropdown-menu.show').forEach(m => m.classList.remove('show'));
});
};
document.addEventListener('click', uploadDropdownDocumentClickHandler);
}
/**
@@ -435,6 +590,10 @@ function setupEventListeners() {
// Upload dropdown
setupUploadDropdown();
setupActionsBarDelegation();
if (elements.actionsBar) {
elements.actionsBar.dataset.mode = 'files';
}
// File input
elements.fileInput.addEventListener('change', (e) => {
@@ -455,18 +614,6 @@ function setupEventListeners() {
});
}
// New folder button
elements.newFolderBtn.addEventListener('click', async () => {
const folderName = await window.Modal.promptNewFolder();
if (folderName) {
fileOps.createFolder(folderName);
}
});
// View toggle
elements.gridViewBtn.addEventListener('click', ui.switchToGridView);
elements.listViewBtn.addEventListener('click', ui.switchToListView);
// Sidebar navigation
elements.navItems.forEach(item => {
item.addEventListener('click', () => {
@@ -529,22 +676,7 @@ function setupEventListeners() {
// Update UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.trash') : 'Trash';
elements.pageTitle.setAttribute('data-i18n', 'nav.trash');
elements.actionsBar.innerHTML = `
<div class="action-buttons">
<button class="btn btn-danger" id="empty-trash-btn">
<i class="fas fa-trash-alt"></i>
<span>${window.i18n ? window.i18n.t('trash.empty_trash') : 'Empty trash'}</span>
</button>
</div>
`;
elements.actionsBar.style.display = 'flex';
// Add event listener to empty trash button
document.getElementById('empty-trash-btn').addEventListener('click', async () => {
if (await fileOps.emptyTrash()) {
loadTrashItems();
}
});
setActionsBarMode('trash');
// Load trash items
loadTrashItems();
@@ -572,39 +704,7 @@ function setupEventListeners() {
// Reset UI
elements.pageTitle.textContent = window.i18n ? window.i18n.t('nav.files') : 'Files';
elements.actionsBar.innerHTML = `
<div class="action-buttons">
<div class="upload-dropdown" id="upload-dropdown">
<button class="btn btn-primary" id="upload-btn">
<i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i>
<span data-i18n="actions.upload">Upload</span>
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
</button>
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
<button class="upload-dropdown-item" id="upload-files-btn">
<i class="fas fa-file"></i>
<span data-i18n="actions.upload_files">Upload files</span>
</button>
<button class="upload-dropdown-item" id="upload-folder-btn">
<i class="fas fa-folder-open"></i>
<span data-i18n="actions.upload_folder">Upload folder</span>
</button>
</div>
</div>
<button class="btn btn-secondary" id="new-folder-btn">
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">New folder</span>
</button>
</div>
<div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i>
</button>
<button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i>
</button>
</div>
`;
elements.actionsBar.style.display = 'flex';
setActionsBarMode('files');
// Show files containers
const filesGrid = document.getElementById('files-grid');
@@ -612,25 +712,6 @@ function setupEventListeners() {
if (filesGrid) filesGrid.style.display = app.currentView === 'grid' ? 'grid' : 'none';
if (filesListView) filesListView.style.display = app.currentView === 'list' ? 'block' : 'none';
// Restore event listeners
setupUploadDropdown();
document.getElementById('new-folder-btn').addEventListener('click', async () => {
const folderName = await window.Modal.promptNewFolder();
if (folderName) {
fileOps.createFolder(folderName);
}
});
document.getElementById('grid-view-btn').addEventListener('click', ui.switchToGridView);
document.getElementById('list-view-btn').addEventListener('click', ui.switchToListView);
// Restore cached elements
elements.uploadBtn = document.getElementById('upload-btn');
elements.newFolderBtn = document.getElementById('new-folder-btn');
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
// Load regular files
app.currentPath = '';
ui.updateBreadcrumb('');
@@ -1188,58 +1269,7 @@ function switchToFilesView() {
}
// Reset UI
elements.actionsBar.innerHTML = `
<div class="action-buttons">
<div class="upload-dropdown" id="upload-dropdown">
<button class="btn btn-primary" id="upload-btn">
<i class="fas fa-cloud-upload-alt" style="margin-right: 5px;"></i>
<span data-i18n="actions.upload">Upload</span>
<i class="fas fa-caret-down" style="margin-left: 4px; font-size: 12px;"></i>
</button>
<div class="upload-dropdown-menu" id="upload-dropdown-menu">
<button class="upload-dropdown-item" id="upload-files-btn">
<i class="fas fa-file"></i>
<span data-i18n="actions.upload_files">Upload files</span>
</button>
<button class="upload-dropdown-item" id="upload-folder-btn">
<i class="fas fa-folder-open"></i>
<span data-i18n="actions.upload_folder">Upload folder</span>
</button>
</div>
</div>
<button class="btn btn-secondary" id="new-folder-btn">
<i class="fas fa-folder-plus" style="margin-right: 5px;"></i> <span data-i18n="actions.new_folder">New folder</span>
</button>
</div>
<div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i>
</button>
<button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i>
</button>
</div>
`;
elements.actionsBar.style.display = 'flex';
// Restore event listeners
setupUploadDropdown();
document.getElementById('new-folder-btn').addEventListener('click', async () => {
const folderName = await window.Modal.promptNewFolder();
if (folderName) {
fileOps.createFolder(folderName);
}
});
document.getElementById('grid-view-btn').addEventListener('click', ui.switchToGridView);
document.getElementById('list-view-btn').addEventListener('click', ui.switchToListView);
// Restore cached elements
elements.uploadBtn = document.getElementById('upload-btn');
elements.newFolderBtn = document.getElementById('new-folder-btn');
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
setActionsBarMode('files');
// Hide shared view if it exists
if (window.sharedView) {
@@ -1302,28 +1332,7 @@ function switchToFavoritesView() {
}
// Configure actions bar for favorites view
elements.actionsBar.innerHTML = `
<div class="action-buttons">
<!-- No actions needed for favorites view -->
</div>
<div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i>
</button>
<button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i>
</button>
</div>
`;
elements.actionsBar.style.display = 'flex';
// Restore view toggle event listeners
document.getElementById('grid-view-btn').addEventListener('click', ui.switchToGridView);
document.getElementById('list-view-btn').addEventListener('click', ui.switchToListView);
// Update cached elements
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
setActionsBarMode('favorites');
// Show standard files containers
const filesGrid = document.getElementById('files-grid');
@@ -1392,39 +1401,7 @@ function switchToRecentFilesView() {
}
// Configure actions bar for recent view
elements.actionsBar.innerHTML = `
<div class="action-buttons">
<button class="btn btn-secondary" id="clear-recent-btn">
<i class="fas fa-broom" style="margin-right: 5px;"></i> <span data-i18n="actions.clear_recent">Clear recent</span>
</button>
</div>
<div class="view-toggle">
<button class="toggle-btn active" id="grid-view-btn" title="Grid view">
<i class="fas fa-th"></i>
</button>
<button class="toggle-btn" id="list-view-btn" title="List view">
<i class="fas fa-list"></i>
</button>
</div>
`;
elements.actionsBar.style.display = 'flex';
// Add event listener for clear button
document.getElementById('clear-recent-btn').addEventListener('click', () => {
if (window.recent) {
window.recent.clearRecentFiles();
window.recent.displayRecentFiles();
window.ui.showNotification('Cleanup completed', 'Recent files history has been cleared');
}
});
// Restore view toggle event listeners
document.getElementById('grid-view-btn').addEventListener('click', ui.switchToGridView);
document.getElementById('list-view-btn').addEventListener('click', ui.switchToListView);
// Update cached elements
elements.gridViewBtn = document.getElementById('grid-view-btn');
elements.listViewBtn = document.getElementById('list-view-btn');
setActionsBarMode('recent');
// Show standard files containers
const filesGrid = document.getElementById('files-grid');