From 49138835ceb90b018fd2cc6daf3a556363331da1 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 25 Mar 2025 03:29:32 -0400 Subject: [PATCH] Authentication & Initialization Changes plus File & Fold Manager Enhancements --- auth.js | 228 +++++++++++++++++++++------------------------ domUtils.js | 31 ++++--- extractZip.php | 146 +++++++++++++++++++++++++++++ fileManager.js | 236 ++++++++++++++++++++++++++++++++++++++++++++++- folderManager.js | 158 +++++++++++++++++++++++++++++-- index.html | 2 +- styles.css | 19 ++++ 7 files changed, 673 insertions(+), 147 deletions(-) create mode 100644 extractZip.php diff --git a/auth.js b/auth.js index 018cc2f..cf015b6 100644 --- a/auth.js +++ b/auth.js @@ -3,78 +3,110 @@ import { toggleVisibility, showToast, attachEnterKeyListener, showCustomConfirmM import { loadFileList, renderFileTable, displayFilePreview, initFileActions } from './fileManager.js'; import { loadFolderTree } from './folderManager.js'; -function initAuth() { - // First, check if the user is already authenticated. - checkAuthentication(false).then(data => { - if (data.setup) { - window.setupMode = true; - showToast("Setup mode: No users found. Please add an admin user."); - toggleVisibility("loginForm", false); - toggleVisibility("mainOperations", false); - document.querySelector(".header-buttons").style.visibility = "hidden"; - toggleVisibility("addUserModal", true); - document.getElementById('newUsername').focus(); - return; - } - window.setupMode = false; - if (data.authenticated) { - // User is logged in—show the main UI. - toggleVisibility("loginForm", false); - toggleVisibility("mainOperations", true); - toggleVisibility("uploadFileForm", true); - toggleVisibility("fileListContainer", true); - attachEnterKeyListener("addUserModal", "saveUserBtn"); - attachEnterKeyListener("removeUserModal", "deleteUserBtn"); - attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn"); - document.querySelector(".header-buttons").style.visibility = "visible"; - // If admin, show admin-only buttons. - if (data.isAdmin) { - const addUserBtn = document.getElementById("addUserBtn"); - const removeUserBtn = document.getElementById("removeUserBtn"); - if (addUserBtn) addUserBtn.style.display = "block"; - if (removeUserBtn) removeUserBtn.style.display = "block"; - // Create and show the restore button. - let restoreBtn = document.getElementById("restoreFilesBtn"); - if (!restoreBtn) { - restoreBtn = document.createElement("button"); - restoreBtn.id = "restoreFilesBtn"; - restoreBtn.classList.add("btn", "btn-warning"); - // Use a material icon. - restoreBtn.innerHTML = 'restore_from_trash'; - const headerButtons = document.querySelector(".header-buttons"); - if (headerButtons) { - if (headerButtons.children.length >= 5) { - headerButtons.insertBefore(restoreBtn, headerButtons.children[5]); - } else { - headerButtons.appendChild(restoreBtn); - } - } +/** + * Updates the select element to reflect the stored items-per-page value. + */ +function updateItemsPerPageSelect() { + const selectElem = document.querySelector(".form-control.bottom-select"); + if (selectElem) { + const stored = localStorage.getItem("itemsPerPage") || "10"; + selectElem.value = stored; + } +} + +/** + * Updates the UI for an authenticated user. + * This includes showing the main UI panels, attaching key listeners, updating header buttons, + * and displaying admin-only buttons if applicable. + */ +function updateAuthenticatedUI(data) { + toggleVisibility("loginForm", false); + toggleVisibility("mainOperations", true); + toggleVisibility("uploadFileForm", true); + toggleVisibility("fileListContainer", true); + attachEnterKeyListener("addUserModal", "saveUserBtn"); + attachEnterKeyListener("removeUserModal", "deleteUserBtn"); + attachEnterKeyListener("changePasswordModal", "saveNewPasswordBtn"); + document.querySelector(".header-buttons").style.visibility = "visible"; + + // If admin, show admin-only buttons; otherwise hide them. + if (data.isAdmin) { + const addUserBtn = document.getElementById("addUserBtn"); + const removeUserBtn = document.getElementById("removeUserBtn"); + if (addUserBtn) addUserBtn.style.display = "block"; + if (removeUserBtn) removeUserBtn.style.display = "block"; + let restoreBtn = document.getElementById("restoreFilesBtn"); + if (!restoreBtn) { + restoreBtn = document.createElement("button"); + restoreBtn.id = "restoreFilesBtn"; + restoreBtn.classList.add("btn", "btn-warning"); + // Using a material icon for restore. + restoreBtn.innerHTML = 'restore_from_trash'; + const headerButtons = document.querySelector(".header-buttons"); + if (headerButtons) { + if (headerButtons.children.length >= 5) { + headerButtons.insertBefore(restoreBtn, headerButtons.children[5]); + } else { + headerButtons.appendChild(restoreBtn); } - restoreBtn.style.display = "block"; + } + } + restoreBtn.style.display = "block"; + } else { + const addUserBtn = document.getElementById("addUserBtn"); + const removeUserBtn = document.getElementById("removeUserBtn"); + if (addUserBtn) addUserBtn.style.display = "none"; + if (removeUserBtn) removeUserBtn.style.display = "none"; + const restoreBtn = document.getElementById("restoreFilesBtn"); + if (restoreBtn) restoreBtn.style.display = "none"; + } + updateItemsPerPageSelect(); +} + +/** + * Checks the user's authentication state and updates the UI accordingly. + * If in setup mode or not authenticated, it shows the proper UI elements. + * When authenticated, it calls updateAuthenticatedUI to handle the UI updates. + */ +function checkAuthentication(showLoginToast = true) { + return sendRequest("checkAuth.php") + .then(data => { + if (data.setup) { + window.setupMode = true; + if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user."); + toggleVisibility("loginForm", false); + toggleVisibility("mainOperations", false); + document.querySelector(".header-buttons").style.visibility = "hidden"; + toggleVisibility("addUserModal", true); + document.getElementById('newUsername').focus(); + return false; + } + window.setupMode = false; + if (data.authenticated) { + updateAuthenticatedUI(data); + return data; } else { - const addUserBtn = document.getElementById("addUserBtn"); - const removeUserBtn = document.getElementById("removeUserBtn"); - if (addUserBtn) addUserBtn.style.display = "none"; - if (removeUserBtn) removeUserBtn.style.display = "none"; - const restoreBtn = document.getElementById("restoreFilesBtn"); - if (restoreBtn) { - restoreBtn.style.display = "none"; - } + if (showLoginToast) showToast("Please log in to continue."); + toggleVisibility("loginForm", true); + toggleVisibility("mainOperations", false); + toggleVisibility("uploadFileForm", false); + toggleVisibility("fileListContainer", false); + document.querySelector(".header-buttons").style.visibility = "hidden"; + return false; } - const selectElem = document.querySelector(".form-control.bottom-select"); - if (selectElem) { - const stored = localStorage.getItem("itemsPerPage") || "10"; - selectElem.value = stored; - } - } else { - toggleVisibility("loginForm", true); - attachEnterKeyListener("loginModal", "loginBtn"); - toggleVisibility("mainOperations", false); - toggleVisibility("uploadFileForm", false); - toggleVisibility("fileListContainer", false); - document.querySelector(".header-buttons").style.visibility = "hidden"; - } - }).catch(error => { + }) + .catch(error => { + console.error("Error checking authentication:", error); + return false; + }); +} + +/** + * Initializes authentication by checking the user's state and setting up event listeners. + * The UI will update automatically based on the auth state. + */ +function initAuth() { + checkAuthentication(false).catch(error => { console.error("Error checking authentication:", error); }); @@ -83,7 +115,6 @@ function initAuth() { if (authForm) { authForm.addEventListener("submit", function (event) { event.preventDefault(); - // Get the "Remember me" checkbox value. const rememberMe = document.getElementById("rememberMeCheckbox") ? document.getElementById("rememberMeCheckbox").checked : false; @@ -137,10 +168,8 @@ function initAuth() { }); document.getElementById("saveUserBtn").addEventListener("click", function () { const newUsername = document.getElementById("newUsername").value.trim(); - // Use the new ID for the add user modal's password field. const newPassword = document.getElementById("addUserPassword").value.trim(); const isAdmin = document.getElementById("isAdmin").checked; - console.log("newUsername:", newUsername, "newPassword:", newPassword); if (!newUsername || !newPassword) { showToast("Username and password are required!"); return; @@ -163,6 +192,7 @@ function initAuth() { if (data.success) { showToast("User added successfully!"); closeAddUserModal(); + // Re-check auth state to update the UI after adding a user. checkAuthentication(false); } else { showToast("Error: " + (data.error || "Could not add user")); @@ -187,14 +217,10 @@ function initAuth() { showToast("Please select a user to remove."); return; } - - // Await the confirmation result from your custom modal helper. const confirmed = await showCustomConfirmModal("Are you sure you want to delete user " + usernameToRemove + "?"); if (!confirmed) { return; } - - // Proceed with deletion... fetch("removeUser.php", { method: "POST", credentials: "include", @@ -222,35 +248,27 @@ function initAuth() { }); document.getElementById("changePasswordBtn").addEventListener("click", function () { - // Show the Change Password modal. document.getElementById("changePasswordModal").style.display = "block"; document.getElementById("oldPassword").focus(); }); document.getElementById("closeChangePasswordModal").addEventListener("click", function () { - // Hide the Change Password modal. document.getElementById("changePasswordModal").style.display = "none"; }); document.getElementById("saveNewPasswordBtn").addEventListener("click", function () { const oldPassword = document.getElementById("oldPassword").value.trim(); - const newPassword = document.getElementById("newPassword").value.trim(); // Change Password modal field + const newPassword = document.getElementById("newPassword").value.trim(); const confirmPassword = document.getElementById("confirmPassword").value.trim(); - if (!oldPassword || !newPassword || !confirmPassword) { showToast("Please fill in all fields."); return; } - if (newPassword !== confirmPassword) { showToast("New passwords do not match."); return; } - - // Prepare the data to send. const data = { oldPassword, newPassword, confirmPassword }; - - // Send request to changePassword.php. fetch("changePassword.php", { method: "POST", credentials: "include", @@ -264,7 +282,6 @@ function initAuth() { .then(result => { if (result.success) { showToast(result.success); - // Clear form fields and close modal. document.getElementById("oldPassword").value = ""; document.getElementById("newPassword").value = ""; document.getElementById("confirmPassword").value = ""; @@ -280,39 +297,6 @@ function initAuth() { }); } -function checkAuthentication(showLoginToast = true) { - return sendRequest("checkAuth.php") - .then(data => { - if (data.setup) { - window.setupMode = true; - if (showLoginToast) showToast("Setup mode: No users found. Please add an admin user."); - toggleVisibility("loginForm", false); - toggleVisibility("mainOperations", false); - document.querySelector(".header-buttons").style.visibility = "hidden"; - toggleVisibility("addUserModal", true); - document.getElementById('newUsername').focus(); - return false; - } - window.setupMode = false; - if (data.authenticated) { - return data; - } else { - if (showLoginToast) showToast("Please log in to continue."); - toggleVisibility("loginForm", true); - toggleVisibility("mainOperations", false); - toggleVisibility("uploadFileForm", false); - toggleVisibility("fileListContainer", false); - document.querySelector(".header-buttons").style.visibility = "hidden"; - return false; - } - }) - .catch(error => { - console.error("Error checking authentication:", error); - return false; - }); -} -window.checkAuthentication = checkAuthentication; - window.changeItemsPerPage = function (value) { localStorage.setItem("itemsPerPage", value); const folder = window.currentFolder || "root"; @@ -322,11 +306,7 @@ window.changeItemsPerPage = function (value) { }; document.addEventListener("DOMContentLoaded", function () { - const selectElem = document.querySelector(".form-control.bottom-select"); - if (selectElem) { - const stored = localStorage.getItem("itemsPerPage") || "10"; - selectElem.value = stored; - } + updateItemsPerPageSelect(); }); function resetUserForm() { diff --git a/domUtils.js b/domUtils.js index 12ae71c..8a3d5e8 100644 --- a/domUtils.js +++ b/domUtils.js @@ -28,35 +28,39 @@ export function toggleAllCheckboxes(masterCheckbox) { } export function updateFileActionButtons() { - const fileListContainer = document.getElementById("fileList"); const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox"); const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked"); const copyBtn = document.getElementById("copySelectedBtn"); const moveBtn = document.getElementById("moveSelectedBtn"); const deleteBtn = document.getElementById("deleteSelectedBtn"); const zipBtn = document.getElementById("downloadZipBtn"); + const extractZipBtn = document.getElementById("extractZipBtn"); if (fileCheckboxes.length === 0) { if (copyBtn) copyBtn.style.display = "none"; if (moveBtn) moveBtn.style.display = "none"; if (deleteBtn) deleteBtn.style.display = "none"; if (zipBtn) zipBtn.style.display = "none"; + if (extractZipBtn) extractZipBtn.style.display = "none"; } else { if (copyBtn) copyBtn.style.display = "inline-block"; if (moveBtn) moveBtn.style.display = "inline-block"; if (deleteBtn) deleteBtn.style.display = "inline-block"; if (zipBtn) zipBtn.style.display = "inline-block"; + if (extractZipBtn) extractZipBtn.style.display = "inline-block"; - if (selectedCheckboxes.length > 0) { - if (copyBtn) copyBtn.disabled = false; - if (moveBtn) moveBtn.disabled = false; - if (deleteBtn) deleteBtn.disabled = false; - if (zipBtn) zipBtn.disabled = false; - } else { - if (copyBtn) copyBtn.disabled = true; - if (moveBtn) moveBtn.disabled = true; - if (deleteBtn) deleteBtn.disabled = true; - if (zipBtn) zipBtn.disabled = true; + const anySelected = selectedCheckboxes.length > 0; + if (copyBtn) copyBtn.disabled = !anySelected; + if (moveBtn) moveBtn.disabled = !anySelected; + if (deleteBtn) deleteBtn.disabled = !anySelected; + if (zipBtn) zipBtn.disabled = !anySelected; + + if (extractZipBtn) { + // Enable only if at least one selected file ends with .zip (case-insensitive). + const anyZipSelected = Array.from(selectedCheckboxes).some(chk => + chk.value.toLowerCase().endsWith(".zip") + ); + extractZipBtn.disabled = !anyZipSelected; } } } @@ -310,7 +314,10 @@ export function previewFile(fileUrl, fileName) { export function attachEnterKeyListener(modalId, buttonId) { const modal = document.getElementById(modalId); if (modal) { - modal.addEventListener("keypress", function(e) { + // Make the modal focusable + modal.setAttribute("tabindex", "-1"); + modal.focus(); + modal.addEventListener("keydown", function(e) { if (e.key === "Enter") { e.preventDefault(); const btn = document.getElementById(buttonId); diff --git a/extractZip.php b/extractZip.php new file mode 100644 index 0000000..582e2a2 --- /dev/null +++ b/extractZip.php @@ -0,0 +1,146 @@ + "Invalid CSRF token"]); + exit; +} + +// Ensure user is authenticated. +if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) { + http_response_code(401); + echo json_encode(["error" => "Unauthorized"]); + exit; +} + +// Read and decode the JSON input. +$rawData = file_get_contents("php://input"); +$data = json_decode($rawData, true); +if (!is_array($data) || !isset($data['folder']) || !isset($data['files']) || !is_array($data['files'])) { + http_response_code(400); + echo json_encode(["error" => "Invalid input."]); + exit; +} + +$folder = $data['folder']; +$files = $data['files']; + +if (empty($files)) { + http_response_code(400); + echo json_encode(["error" => "No files specified."]); + exit; +} + +// Validate folder name (allow "root" or valid subfolder names). +if ($folder !== "root") { + $parts = explode('/', $folder); + foreach ($parts as $part) { + if (empty($part) || $part === '.' || $part === '..' || !preg_match('/^[A-Za-z0-9_\-\.\(\) ]+$/', $part)) { + http_response_code(400); + echo json_encode(["error" => "Invalid folder name."]); + exit; + } + } + $relativePath = implode(DIRECTORY_SEPARATOR, $parts) . DIRECTORY_SEPARATOR; +} else { + $relativePath = ""; +} + +$baseDir = realpath(UPLOAD_DIR); +if ($baseDir === false) { + http_response_code(500); + echo json_encode(["error" => "Uploads directory not configured correctly."]); + exit; +} + +$folderPath = $baseDir . DIRECTORY_SEPARATOR . $relativePath; +$folderPathReal = realpath($folderPath); +if ($folderPathReal === false || strpos($folderPathReal, $baseDir) !== 0) { + http_response_code(404); + echo json_encode(["error" => "Folder not found."]); + exit; +} + +// ---------- Metadata Setup ---------- +function getMetadataFilePath($folder) { + if (strtolower($folder) === 'root' || $folder === '') { + return META_DIR . "root_metadata.json"; + } + return META_DIR . str_replace(['/', '\\', ' '], '-', $folder) . '_metadata.json'; +} + +$srcMetaFile = getMetadataFilePath($folder); +$destMetaFile = getMetadataFilePath($folder); +$srcMetadata = file_exists($srcMetaFile) ? json_decode(file_get_contents($srcMetaFile), true) : []; +$destMetadata = file_exists($destMetaFile) ? json_decode(file_get_contents($destMetaFile), true) : []; + +$errors = []; +$safeFileNamePattern = '/^[A-Za-z0-9_\-\.\(\) ]+$/'; +$allSuccess = true; + +// ---------- Process Each File ---------- +foreach ($files as $zipFileName) { + $originalName = basename(trim($zipFileName)); + // Process only .zip files. + if (strtolower(substr($originalName, -4)) !== '.zip') { + continue; + } + if (!preg_match($safeFileNamePattern, $originalName)) { + $errors[] = "$originalName has an invalid name."; + $allSuccess = false; + continue; + } + + $zipFilePath = $folderPathReal . DIRECTORY_SEPARATOR . $originalName; + if (!file_exists($zipFilePath)) { + $errors[] = "$originalName does not exist in folder."; + $allSuccess = false; + continue; + } + + $zip = new ZipArchive(); + if ($zip->open($zipFilePath) !== TRUE) { + $errors[] = "Could not open $originalName as a zip file."; + $allSuccess = false; + continue; + } + + // Attempt extraction. + if (!$zip->extractTo($folderPathReal)) { + $errors[] = "Failed to extract $originalName."; + $allSuccess = false; + } else { + // Update metadata for each extracted file if the zip file has metadata. + if (isset($srcMetadata[$originalName])) { + $zipMeta = $srcMetadata[$originalName]; + // Iterate through all entries in the zip. + for ($i = 0; $i < $zip->numFiles; $i++) { + $entryName = $zip->getNameIndex($i); + $extractedFileName = basename($entryName); + if ($extractedFileName) { + $destMetadata[$extractedFileName] = $zipMeta; + } + } + } + } + $zip->close(); +} + +// Write updated metadata back to the destination metadata file. +if (file_put_contents($destMetaFile, json_encode($destMetadata, JSON_PRETTY_PRINT)) === false) { + $errors[] = "Failed to update metadata."; + $allSuccess = false; +} + +if ($allSuccess) { + echo json_encode(["success" => true]); +} else { + echo json_encode(["success" => false, "error" => implode(" ", $errors)]); +} +exit; +?> \ No newline at end of file diff --git a/fileManager.js b/fileManager.js index df8d219..6121219 100644 --- a/fileManager.js +++ b/fileManager.js @@ -9,6 +9,7 @@ import { showToast, updateRowHighlight, toggleRowSelection, + attachEnterKeyListener, previewFile as originalPreviewFile } from './domUtils.js'; @@ -661,6 +662,7 @@ export function handleDeleteSelected(e) { document.getElementById("deleteFilesMessage").textContent = "Are you sure you want to delete " + window.filesToDelete.length + " selected file(s)?"; document.getElementById("deleteFilesModal").style.display = "block"; + attachEnterKeyListener("deleteFilesModal", "confirmDeleteFiles"); } document.addEventListener("DOMContentLoaded", function () { @@ -671,6 +673,7 @@ document.addEventListener("DOMContentLoaded", function () { window.filesToDelete = []; }); } + const confirmDelete = document.getElementById("confirmDeleteFiles"); if (confirmDelete) { confirmDelete.addEventListener("click", function () { @@ -700,7 +703,7 @@ document.addEventListener("DOMContentLoaded", function () { }); } }); - +attachEnterKeyListener("downloadZipModal", "confirmDownloadZip"); export function handleDownloadZipSelected(e) { e.preventDefault(); e.stopImmediatePropagation(); @@ -711,6 +714,64 @@ export function handleDownloadZipSelected(e) { } window.filesToDownload = Array.from(checkboxes).map(chk => chk.value); document.getElementById("downloadZipModal").style.display = "block"; + setTimeout(() => { + const input = document.getElementById("zipFileNameInput"); + input.focus(); + }, 100); + +} + +export function handleExtractZipSelected(e) { + if (e) { + e.preventDefault(); + e.stopImmediatePropagation(); + } + // Get selected file names + const checkboxes = document.querySelectorAll(".file-checkbox:checked"); + if (!checkboxes.length) { + showToast("No files selected."); + return; + } + // Filter for zip files only + const zipFiles = Array.from(checkboxes) + .map(chk => chk.value) + .filter(name => name.toLowerCase().endsWith(".zip")); + if (!zipFiles.length) { + showToast("No zip files selected."); + return; + } + // Call the extract endpoint with the selected zip files + fetch("extractZip.php", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": window.csrfToken + }, + body: JSON.stringify({ + folder: window.currentFolder || "root", + files: zipFiles + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + showToast("Zip file(s) extracted successfully!"); + loadFileList(window.currentFolder); + } else { + showToast("Error extracting zip: " + (data.error || "Unknown error")); + } + }) + .catch(error => { + console.error("Error extracting zip files:", error); + showToast("Error extracting zip files."); + }); +} + +const extractZipBtn = document.getElementById("extractZipBtn"); +if (extractZipBtn) { + extractZipBtn.replaceWith(extractZipBtn.cloneNode(true)); + document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected); } document.addEventListener("DOMContentLoaded", function () { @@ -720,6 +781,7 @@ document.addEventListener("DOMContentLoaded", function () { document.getElementById("downloadZipModal").style.display = "none"; }); } + const confirmDownloadZip = document.getElementById("confirmDownloadZip"); if (confirmDownloadZip) { confirmDownloadZip.addEventListener("click", function () { @@ -1035,7 +1097,7 @@ export function editFile(fileName, folder) { fetch(fileUrl, { method: "HEAD" }) .then(response => { const contentLength = response.headers.get("Content-Length"); - if (!contentLength || parseInt(contentLength) > 10485760) { + if (contentLength !== null && parseInt(contentLength) > 10485760) { showToast("This file is larger than 10 MB and cannot be edited in the browser."); throw new Error("File too large."); } @@ -1196,8 +1258,13 @@ export function initFileActions() { downloadZipBtn.replaceWith(downloadZipBtn.cloneNode(true)); document.getElementById("downloadZipBtn").addEventListener("click", handleDownloadZipSelected); } + const extractZipBtn = document.getElementById("extractZipBtn"); + if (extractZipBtn) { + extractZipBtn.replaceWith(extractZipBtn.cloneNode(true)); + document.getElementById("extractZipBtn").addEventListener("click", handleExtractZipSelected); + } } - +attachEnterKeyListener("renameFileModal", "submitRenameFile"); export function renameFile(oldName, folder) { window.fileToRename = oldName; window.fileFolder = folder || window.currentFolder || "root"; @@ -1223,6 +1290,7 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById("newFileName").value = ""; }); } + const submitBtn = document.getElementById("submitRenameFile"); if (submitBtn) { submitBtn.addEventListener("click", function () { @@ -1283,4 +1351,164 @@ document.addEventListener("DOMContentLoaded", function () { el.addEventListener("dragleave", folderDragLeaveHandler); el.addEventListener("drop", folderDropHandler); }); -}); \ No newline at end of file +}); + +document.addEventListener("keydown", function(e) { + // Skip if focus is on an input, textarea, or any contentEditable element. + const tag = e.target.tagName.toLowerCase(); + if (tag === "input" || tag === "textarea" || e.target.isContentEditable) { + return; + } + // On Mac, the delete key is often reported as "Backspace" (keyCode 8) + if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) { + const selectedCheckboxes = document.querySelectorAll("#fileList .file-checkbox:checked"); + if (selectedCheckboxes.length > 0) { + e.preventDefault(); // Prevent default back navigation in some browsers. + handleDeleteSelected(new Event("click")); + } + } +}); + +// ---------- CONTEXT MENU SUPPORT FOR FILE LIST ---------- + +// Function to display the context menu with provided items at (x, y) +function showFileContextMenu(x, y, menuItems) { + let menu = document.getElementById("fileContextMenu"); + if (!menu) { + menu = document.createElement("div"); + menu.id = "fileContextMenu"; + menu.style.position = "absolute"; + menu.style.backgroundColor = "#fff"; + menu.style.border = "1px solid #ccc"; + menu.style.boxShadow = "2px 2px 6px rgba(0,0,0,0.2)"; + menu.style.zIndex = "9999"; + menu.style.padding = "5px 0"; + menu.style.minWidth = "150px"; + document.body.appendChild(menu); + } + // Clear previous items + menu.innerHTML = ""; + menuItems.forEach(item => { + let menuItem = document.createElement("div"); + menuItem.textContent = item.label; + menuItem.style.padding = "5px 15px"; + menuItem.style.cursor = "pointer"; + menuItem.addEventListener("mouseover", () => { + if (document.body.classList.contains("dark-mode")) { + menuItem.style.backgroundColor = "#444"; // darker gray for dark mode + } else { + menuItem.style.backgroundColor = "#f0f0f0"; // light gray for light mode + } + }); + menuItem.addEventListener("mouseout", () => { + menuItem.style.backgroundColor = ""; + }); + menuItem.addEventListener("click", () => { + item.action(); + hideFileContextMenu(); + }); + menu.appendChild(menuItem); + }); + menu.style.left = x + "px"; + menu.style.top = y + "px"; + menu.style.display = "block"; +} + +function hideFileContextMenu() { + const menu = document.getElementById("fileContextMenu"); + if (menu) { + menu.style.display = "none"; + } +} + +// Context menu handler for the file list. +function fileListContextMenuHandler(e) { + e.preventDefault(); + // If no file is selected, try to select the row that was right-clicked. + let row = e.target.closest("tr"); + if (row) { + const checkbox = row.querySelector(".file-checkbox"); + if (checkbox && !checkbox.checked) { + checkbox.checked = true; + updateRowHighlight(checkbox); + updateFileActionButtons(); + } + } + + // Get selected file names. + const selected = Array.from(document.querySelectorAll("#fileList .file-checkbox:checked")).map(chk => chk.value); + + // Build the context menu items. + let menuItems = [ + { label: "Delete Selected", action: () => { handleDeleteSelected(new Event("click")); } }, + { label: "Copy Selected", action: () => { handleCopySelected(new Event("click")); } }, + { label: "Move Selected", action: () => { handleMoveSelected(new Event("click")); } }, + { label: "Download Zip", action: () => { handleDownloadZipSelected(new Event("click")); } } + ]; + + if (selected.some(name => name.toLowerCase().endsWith(".zip"))) { + menuItems.push({ + label: "Extract Zip", + action: () => { handleExtractZipSelected(new Event("click")); } + }); + } + + if (selected.length === 1) { + // Look up the file object. + const file = fileData.find(f => f.name === selected[0]); + + // Add Preview option. + menuItems.push({ + label: "Preview", + action: () => { + const folder = window.currentFolder || "root"; + const folderPath = folder === "root" + ? "uploads/" + : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; + previewFile(folderPath + encodeURIComponent(file.name) + "?t=" + new Date().getTime(), file.name); + } + }); + + // Only show Edit option if file is editable. + if (canEditFile(file.name)) { + menuItems.push({ + label: "Edit", + action: () => { editFile(selected[0], window.currentFolder); } + }); + } + + // Add Rename option. + menuItems.push({ + label: "Rename", + action: () => { renameFile(selected[0], window.currentFolder); } + }); + } + + showFileContextMenu(e.pageX, e.pageY, menuItems); +} + +// Bind the context menu to the file list container. +// (This is set every time the file list is rendered.) +function bindFileListContextMenu() { + const fileListContainer = document.getElementById("fileList"); + if (fileListContainer) { + fileListContainer.oncontextmenu = fileListContextMenuHandler; + } +} + +// Hide the context menu if clicking anywhere else. +document.addEventListener("click", function(e) { + const menu = document.getElementById("fileContextMenu"); + if (menu && menu.style.display === "block") { + hideFileContextMenu(); + } +}); + +// After rendering the file table, bind the context menu handler. +(function() { + const originalRenderFileTable = renderFileTable; + renderFileTable = function(folder) { + originalRenderFileTable(folder); + bindFileListContextMenu(); + }; +})(); \ No newline at end of file diff --git a/folderManager.js b/folderManager.js index e48007e..45ad395 100644 --- a/folderManager.js +++ b/folderManager.js @@ -1,7 +1,7 @@ // folderManager.js import { loadFileList } from './fileManager.js'; -import { showToast, escapeHTML } from './domUtils.js'; +import { showToast, escapeHTML, attachEnterKeyListener } from './domUtils.js'; // ---------------------- // Helper Functions (Data/State) @@ -90,7 +90,6 @@ function bindBreadcrumbEvents() { link.addEventListener("click", function (e) { e.stopPropagation(); let folder = this.getAttribute("data-folder"); - console.log("Breadcrumb clicked, folder:", folder); window.currentFolder = folder; localStorage.setItem("lastOpenedFolder", folder); const titleEl = document.getElementById("fileListTitle"); @@ -447,6 +446,7 @@ export function loadFolderList(selectedFolder) { // ---------------------- document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal); + document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal); function openRenameFolderModal() { @@ -470,7 +470,7 @@ document.getElementById("cancelRenameFolder").addEventListener("click", function document.getElementById("renameFolderModal").style.display = "none"; document.getElementById("newRenameFolderName").value = ""; }); - +attachEnterKeyListener("renameFolderModal", "submitRenameFolder"); document.getElementById("submitRenameFolder").addEventListener("click", function (event) { event.preventDefault(); const selectedFolder = window.currentFolder || "root"; @@ -527,7 +527,7 @@ function openDeleteFolderModal() { document.getElementById("cancelDeleteFolder").addEventListener("click", function () { document.getElementById("deleteFolderModal").style.display = "none"; }); - +attachEnterKeyListener("deleteFolderModal", "confirmDeleteFolder"); document.getElementById("confirmDeleteFolder").addEventListener("click", function () { const selectedFolder = window.currentFolder || "root"; const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); @@ -565,7 +565,7 @@ document.getElementById("cancelCreateFolder").addEventListener("click", function document.getElementById("createFolderModal").style.display = "none"; document.getElementById("newFolderName").value = ""; }); - +attachEnterKeyListener("createFolderModal", "submitCreateFolder"); document.getElementById("submitCreateFolder").addEventListener("click", function () { const folderInput = document.getElementById("newFolderName").value.trim(); if (!folderInput) { @@ -606,4 +606,150 @@ document.getElementById("submitCreateFolder").addEventListener("click", function console.error("Error creating folder:", error); document.getElementById("createFolderModal").style.display = "none"; }); -}); \ No newline at end of file +}); + +// ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ---------- + +// Function to display the custom context menu at (x, y) with given menu items. +function showFolderManagerContextMenu(x, y, menuItems) { + let menu = document.getElementById("folderManagerContextMenu"); + if (!menu) { + menu = document.createElement("div"); + menu.id = "folderManagerContextMenu"; + menu.style.position = "absolute"; + menu.style.padding = "5px 0"; + menu.style.minWidth = "150px"; + menu.style.zIndex = "9999"; + document.body.appendChild(menu); + } + + // Set styles based on dark mode. + if (document.body.classList.contains("dark-mode")) { + menu.style.backgroundColor = "#2c2c2c"; + menu.style.border = "1px solid #555"; + menu.style.color = "#e0e0e0"; + } else { + menu.style.backgroundColor = "#fff"; + menu.style.border = "1px solid #ccc"; + menu.style.color = "#000"; + } + + // Clear previous items. + menu.innerHTML = ""; + menuItems.forEach(item => { + const menuItem = document.createElement("div"); + menuItem.textContent = item.label; + menuItem.style.padding = "5px 15px"; + menuItem.style.cursor = "pointer"; + menuItem.addEventListener("mouseover", () => { + if (document.body.classList.contains("dark-mode")) { + menuItem.style.backgroundColor = "#444"; + } else { + menuItem.style.backgroundColor = "#f0f0f0"; + } + }); + menuItem.addEventListener("mouseout", () => { + menuItem.style.backgroundColor = ""; + }); + menuItem.addEventListener("click", () => { + item.action(); + hideFolderManagerContextMenu(); + }); + menu.appendChild(menuItem); + }); + menu.style.left = x + "px"; + menu.style.top = y + "px"; + menu.style.display = "block"; +} + +function hideFolderManagerContextMenu() { + const menu = document.getElementById("folderManagerContextMenu"); + if (menu) { + menu.style.display = "none"; + } +} + +// Context menu handler for folder tree and breadcrumb items. +function folderManagerContextMenuHandler(e) { + e.preventDefault(); + e.stopPropagation(); + + // Get the closest folder element (either from the tree or breadcrumb). + const target = e.target.closest(".folder-option, .breadcrumb-link"); + if (!target) return; + + const folder = target.getAttribute("data-folder"); + if (!folder) return; + + // Update current folder and highlight the selected element. + window.currentFolder = folder; + document.querySelectorAll(".folder-option, .breadcrumb-link").forEach(el => el.classList.remove("selected")); + target.classList.add("selected"); + + // Build context menu items. + const menuItems = [ + { + label: "Create Folder", + action: () => { + document.getElementById("createFolderModal").style.display = "block"; + document.getElementById("newFolderName").focus(); + } + }, + { + label: "Rename Folder", + action: () => { openRenameFolderModal(); } + }, + { + label: "Delete Folder", + action: () => { openDeleteFolderModal(); } + } + ]; + + showFolderManagerContextMenu(e.pageX, e.pageY, menuItems); +} + +// Bind contextmenu events to folder tree and breadcrumb elements. +function bindFolderManagerContextMenu() { + // Bind context menu to folder tree container. + const container = document.getElementById("folderTreeContainer"); + if (container) { + container.removeEventListener("contextmenu", folderManagerContextMenuHandler); + container.addEventListener("contextmenu", folderManagerContextMenuHandler, false); + } + + // Bind context menu to breadcrumb links. + const breadcrumbNodes = document.querySelectorAll(".breadcrumb-link"); + breadcrumbNodes.forEach(node => { + node.removeEventListener("contextmenu", folderManagerContextMenuHandler); + node.addEventListener("contextmenu", folderManagerContextMenuHandler, false); + }); +} + +// Hide context menu when clicking elsewhere. +document.addEventListener("click", function () { + hideFolderManagerContextMenu(); +}); + +document.addEventListener("DOMContentLoaded", function() { + document.addEventListener("keydown", function(e) { + // Skip if the user is typing in an input, textarea, or contentEditable element. + const tag = e.target.tagName.toLowerCase(); + if (tag === "input" || tag === "textarea" || e.target.isContentEditable) { + return; + } + + // On macOS, "Delete" is typically reported as "Backspace" (keyCode 8) + if (e.key === "Delete" || e.key === "Backspace" || e.keyCode === 46 || e.keyCode === 8) { + // Ensure a folder is selected and it isn't the root folder. + if (window.currentFolder && window.currentFolder !== "root") { + // Prevent default (avoid navigating back on macOS). + e.preventDefault(); + // Call your existing folder delete function. + openDeleteFolderModal(); + } + } + }); +}); + +// Call this binding function after rendering the folder tree and breadcrumbs. +bindFolderManagerContextMenu(); \ No newline at end of file diff --git a/index.html b/index.html index f567e30..2a5064f 100644 --- a/index.html +++ b/index.html @@ -302,8 +302,8 @@ - +