From d7b539320017fba974d66cc85001784551014ab8 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 17 Mar 2025 01:24:31 -0400 Subject: [PATCH] refactoring fileManager domUtils --- domUtils.js | 261 +++++++++++++++++++--- fileManager.js | 586 +++++++++++++++---------------------------------- 2 files changed, 406 insertions(+), 441 deletions(-) diff --git a/domUtils.js b/domUtils.js index e754faf..e3e89c0 100644 --- a/domUtils.js +++ b/domUtils.js @@ -1,5 +1,6 @@ // domUtils.js +// Basic DOM Helpers export function toggleVisibility(elementId, shouldShow) { const element = document.getElementById(elementId); if (element) { @@ -18,16 +19,14 @@ export function escapeHTML(str) { .replace(/'/g, "'"); } -// Toggle all checkboxes (assumes checkboxes have class 'file-checkbox') export function toggleAllCheckboxes(masterCheckbox) { const checkboxes = document.querySelectorAll(".file-checkbox"); checkboxes.forEach(chk => { chk.checked = masterCheckbox.checked; }); - updateFileActionButtons(); // call the updated function + updateFileActionButtons(); // update buttons based on current selection } -// This updateFileActionButtons function checks for checkboxes inside the file list container. export function updateFileActionButtons() { const fileListContainer = document.getElementById("fileList"); const fileCheckboxes = document.querySelectorAll("#fileList .file-checkbox"); @@ -36,31 +35,28 @@ export function updateFileActionButtons() { const moveBtn = document.getElementById("moveSelectedBtn"); const deleteBtn = document.getElementById("deleteSelectedBtn"); const zipBtn = document.getElementById("downloadZipBtn"); - - // Hide the buttons and dropdown if no files exist. + if (fileCheckboxes.length === 0) { - copyBtn.style.display = "none"; - moveBtn.style.display = "none"; - deleteBtn.style.display = "none"; - zipBtn.style.display = "none"; + if (copyBtn) copyBtn.style.display = "none"; + if (moveBtn) moveBtn.style.display = "none"; + if (deleteBtn) deleteBtn.style.display = "none"; + if (zipBtn) zipBtn.style.display = "none"; } else { - // Otherwise, show the buttons and dropdown. - copyBtn.style.display = "inline-block"; - moveBtn.style.display = "inline-block"; - deleteBtn.style.display = "inline-block"; - zipBtn.style.display = "inline-block"; - - // Enable the buttons if at least one file is selected; otherwise disable. + 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 (selectedCheckboxes.length > 0) { - copyBtn.disabled = false; - moveBtn.disabled = false; - deleteBtn.disabled = false; - zipBtn.disabled = false; + if (copyBtn) copyBtn.disabled = false; + if (moveBtn) moveBtn.disabled = false; + if (deleteBtn) deleteBtn.disabled = false; + if (zipBtn) zipBtn.disabled = false; } else { - copyBtn.disabled = true; - moveBtn.disabled = true; - deleteBtn.disabled = true; - zipBtn.disabled = true; + if (copyBtn) copyBtn.disabled = true; + if (moveBtn) moveBtn.disabled = true; + if (deleteBtn) deleteBtn.disabled = true; + if (zipBtn) zipBtn.disabled = true; } } } @@ -73,13 +69,226 @@ export function showToast(message, duration = 3000) { } toast.textContent = message; toast.style.display = "block"; - // Force reflow so the transition works. + // Force reflow for transition effect. void toast.offsetWidth; toast.classList.add("show"); setTimeout(() => { toast.classList.remove("show"); setTimeout(() => { toast.style.display = "none"; - }, 500); // Wait for the opacity transition to finish. + }, 500); }, duration); +} + +// --- DOM Building Functions for File Table --- + +export function buildSearchAndPaginationControls({ currentPage, totalPages, searchTerm }) { + const safeSearchTerm = escapeHTML(searchTerm); + return ` +
+
+
+
+ + search + +
+ +
+
+
+
+ + Page ${currentPage} of ${totalPages || 1} + +
+
+
+ `; +} + +export function buildFileTableHeader(sortOrder) { + return ` + + + + + + + + + + + + + `; +} + +export function buildFileTableRow(file, folderPath) { + const safeFileName = escapeHTML(file.name); + const safeModified = escapeHTML(file.modified); + const safeUploaded = escapeHTML(file.uploaded); + const safeSize = escapeHTML(file.size); + const safeUploader = escapeHTML(file.uploader || "Unknown"); + + let previewButton = ""; + if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|ogg)$/i.test(file.name)) { + let previewIcon = ""; + if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) { + previewIcon = `image`; + } else if (/\.(mp4|webm|mov|ogg)$/i.test(file.name)) { + previewIcon = `videocam`; + } else if (/\.pdf$/i.test(file.name)) { + previewIcon = `picture_as_pdf`; + } + previewButton = ``; + } + + return ` + + + + + + + + + + `; +} + +export function buildBottomControls(itemsPerPageSetting) { + return ` +
+ + + items per page +
+ `; +} + +// --- Global Helper Functions --- + +export function debounce(func, wait) { + let timeout; + return function (...args) { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; +} + +export function updateRowHighlight(checkbox) { + const row = checkbox.closest('tr'); + if (!row) return; + if (checkbox.checked) { + row.classList.add('row-selected'); + } else { + row.classList.remove('row-selected'); + } +} + +export function toggleRowSelection(event, fileName) { + const targetTag = event.target.tagName.toLowerCase(); + if (targetTag === 'a' || targetTag === 'button' || targetTag === 'input') { + return; + } + const row = event.currentTarget; + const checkbox = row.querySelector('.file-checkbox'); + if (!checkbox) return; + checkbox.checked = !checkbox.checked; + updateRowHighlight(checkbox); + updateFileActionButtons(); +} + +export function previewFile(fileUrl, fileName) { + let modal = document.getElementById("filePreviewModal"); + if (!modal) { + modal = document.createElement("div"); + modal.id = "filePreviewModal"; + Object.assign(modal.style, { + display: "none", + position: "fixed", + top: "0", + left: "0", + width: "100vw", + height: "100vh", + backgroundColor: "rgba(0,0,0,0.7)", + display: "flex", + justifyContent: "center", + alignItems: "center", + zIndex: "1000" + }); + modal.innerHTML = ` + `; + document.body.appendChild(modal); + + document.getElementById("closeFileModal").addEventListener("click", function () { + const video = modal.querySelector("video"); + if (video) { + video.pause(); + video.currentTime = 0; + } + modal.style.display = "none"; + }); + + modal.addEventListener("click", function (e) { + if (e.target === modal) { + const video = modal.querySelector("video"); + if (video) { + video.pause(); + video.currentTime = 0; + } + modal.style.display = "none"; + } + }); + } + + modal.querySelector("h4").textContent = fileName; + const container = modal.querySelector(".file-preview-container"); + container.innerHTML = ""; + + const extension = fileName.split('.').pop().toLowerCase(); + + if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(fileName)) { + const img = document.createElement("img"); + img.src = fileUrl; + img.className = "image-modal-img"; + container.appendChild(img); + } else if (extension === "pdf") { + const embed = document.createElement("embed"); + const separator = fileUrl.indexOf('?') === -1 ? '?' : '&'; + embed.src = fileUrl + separator + 't=' + new Date().getTime(); + embed.type = "application/pdf"; + embed.style.width = "80vw"; + embed.style.height = "80vh"; + embed.style.border = "none"; + container.appendChild(embed); + } else if (/\.(mp4|webm|mov|ogg)$/i.test(fileName)) { + const video = document.createElement("video"); + video.src = fileUrl; + video.controls = true; + video.className = "image-modal-img"; + container.appendChild(video); + } else { + container.textContent = "Preview not available for this file type."; + } + + modal.style.display = "flex"; } \ No newline at end of file diff --git a/fileManager.js b/fileManager.js index d871503..04ddeae 100644 --- a/fileManager.js +++ b/fileManager.js @@ -1,19 +1,180 @@ // fileManager.js -import { escapeHTML, updateFileActionButtons, showToast } from './domUtils.js'; -import { formatFolderName } from './folderManager.js'; + +import { + escapeHTML, + debounce, + buildSearchAndPaginationControls, + buildFileTableHeader, + buildFileTableRow, + buildBottomControls, + updateFileActionButtons, + showToast, + updateRowHighlight, + toggleRowSelection, + previewFile +} from './domUtils.js'; export let fileData = []; export let sortOrder = { column: "uploaded", ascending: true }; -// Global pagination defaults window.itemsPerPage = window.itemsPerPage || 10; window.currentPage = window.currentPage || 1; -// ------------------------------- -// Helper Functions -// ------------------------------- +// --- Define formatFolderName --- +// This helper formats folder names for display. Adjust as needed. +function formatFolderName(folder) { + // Example: If folder is "root", return "(Root)" + if (folder === "root") return "(Root)"; + // Replace underscores/dashes with spaces and capitalize each word. + return folder + .replace(/[_-]+/g, " ") + .replace(/\b\w/g, char => char.toUpperCase()); +} + +// Expose DOM helper functions for inline handlers. +window.toggleRowSelection = toggleRowSelection; +window.updateRowHighlight = updateRowHighlight; +window.previewFile = previewFile; + +export function loadFileList(folderParam) { + const folder = folderParam || "root"; + const fileListContainer = document.getElementById("fileList"); + + fileListContainer.style.visibility = "hidden"; + fileListContainer.innerHTML = "
Loading files...
"; + + return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime()) + .then(response => response.json()) + .then(data => { + fileListContainer.innerHTML = ""; + if (data.files && data.files.length > 0) { + data.files = data.files.map(file => { + file.fullName = (file.path || file.name).trim().toLowerCase(); + file.editable = canEditFile(file.name); + file.folder = folder; + return file; + }); + fileData = data.files; + renderFileTable(folder); + } else { + fileListContainer.textContent = "No files found."; + updateFileActionButtons(); + } + return data.files || []; + }) + .catch(error => { + console.error("Error loading file list:", error); + fileListContainer.textContent = "Error loading files."; + return []; + }) + .finally(() => { + fileListContainer.style.visibility = "visible"; + }); +} + +export function renderFileTable(folder) { + const fileListContainer = document.getElementById("fileList"); + const searchTerm = window.currentSearchTerm || ""; + const itemsPerPageSetting = parseInt(localStorage.getItem('itemsPerPage') || '10', 10); + let currentPage = window.currentPage || 1; + + const filteredFiles = fileData.filter(file => + file.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const totalFiles = filteredFiles.length; + const totalPages = Math.ceil(totalFiles / itemsPerPageSetting); + + if (currentPage > totalPages) { + currentPage = totalPages > 0 ? totalPages : 1; + window.currentPage = currentPage; + } + + const folderPath = (folder === "root") + ? "uploads/" + : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; + + const topControlsHTML = buildSearchAndPaginationControls({ + currentPage, + totalPages, + searchTerm + }); + + const headerHTML = buildFileTableHeader(sortOrder); + + const startIndex = (currentPage - 1) * itemsPerPageSetting; + const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles); + + let rowsHTML = ""; + if (totalFiles > 0) { + filteredFiles.slice(startIndex, endIndex).forEach(file => { + rowsHTML += buildFileTableRow(file, folderPath); + }); + } else { + rowsHTML += ``; + } + rowsHTML += "
File Name ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}Date Modified ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}Upload Date ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}File Size ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}Uploader ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}Actions
+ + ${safeFileName}${safeModified}${safeUploaded}${safeSize}${safeUploader} +
+ Download + ${file.editable ? `` : ""} + ${previewButton} + +
+
No files found.
"; + + const bottomControlsHTML = buildBottomControls(itemsPerPageSetting); + + fileListContainer.innerHTML = topControlsHTML + headerHTML + rowsHTML + bottomControlsHTML; + + const newSearchInput = document.getElementById("searchInput"); + if (newSearchInput) { + newSearchInput.addEventListener("input", debounce(function () { + window.currentSearchTerm = newSearchInput.value; + window.currentPage = 1; + renderFileTable(folder); + setTimeout(() => { + newSearchInput.focus(); + newSearchInput.setSelectionRange(newSearchInput.value.length, newSearchInput.value.length); + }, 0); + }, 300)); + } + + document.querySelectorAll("table.table thead th[data-column]").forEach(cell => { + cell.addEventListener("click", function () { + const column = this.getAttribute("data-column"); + sortFiles(column, folder); + }); + }); + + document.querySelectorAll('#fileList .file-checkbox').forEach(checkbox => { + checkbox.addEventListener('change', function (e) { + updateRowHighlight(e.target); + updateFileActionButtons(); + }); + }); + + updateFileActionButtons(); +} + +export function sortFiles(column, folder) { + if (sortOrder.column === column) { + sortOrder.ascending = !sortOrder.ascending; + } else { + sortOrder.column = column; + sortOrder.ascending = true; + } + fileData.sort((a, b) => { + let valA = a[column] || ""; + let valB = b[column] || ""; + if (column === "modified" || column === "uploaded") { + const parsedA = parseCustomDate(valA); + const parsedB = parseCustomDate(valB); + valA = parsedA; + valB = parsedB; + } else if (typeof valA === "string") { + valA = valA.toLowerCase(); + valB = valB.toLowerCase(); + } + if (valA < valB) return sortOrder.ascending ? -1 : 1; + if (valA > valB) return sortOrder.ascending ? 1 : -1; + return 0; + }); + renderFileTable(folder); +} -// Parse date strings in "m/d/y h:iA" format into a timestamp. function parseCustomDate(dateStr) { dateStr = dateStr.replace(/\s+/g, " ").trim(); const parts = dateStr.split(" "); @@ -49,7 +210,6 @@ function parseCustomDate(dateStr) { return new Date(year, month - 1, day, hour, minute).getTime(); } -// Determines if a file is editable based on its extension. export function canEditFile(fileName) { const allowedExtensions = [ "txt", "html", "htm", "css", "js", "json", "xml", @@ -60,371 +220,6 @@ export function canEditFile(fileName) { return allowedExtensions.includes(ext); } -// ------------------------------- -// Global Functions (attached to window) -// ------------------------------- - -window.toggleRowSelection = function (event, fileName) { - const targetTag = event.target.tagName.toLowerCase(); - if (targetTag === 'a' || targetTag === 'button' || targetTag === 'input') { - return; - } - const row = event.currentTarget; - const checkbox = row.querySelector('.file-checkbox'); - if (!checkbox) return; - checkbox.checked = !checkbox.checked; - window.updateRowHighlight(checkbox); - updateFileActionButtons(); -}; - -window.updateRowHighlight = function (checkbox) { - const row = checkbox.closest('tr'); - if (!row) return; - if (checkbox.checked) { - row.classList.add('row-selected'); - } else { - row.classList.remove('row-selected'); - } -}; - -// ------------------------------- -// File List Rendering -// ------------------------------- - -export function loadFileList(folderParam) { - const folder = folderParam || "root"; - const fileListContainer = document.getElementById("fileList"); - - // Hide the container while loading - fileListContainer.style.visibility = "hidden"; - fileListContainer.innerHTML = "
Loading files...
"; - - return fetch("getFileList.php?folder=" + encodeURIComponent(folder) + "&recursive=1&t=" + new Date().getTime()) - .then(response => response.json()) - .then(data => { - // Clear the container once data is loaded - fileListContainer.innerHTML = ""; - if (data.files && data.files.length > 0) { - data.files = data.files.map(file => { - file.fullName = (file.path || file.name).trim().toLowerCase(); - return file; - }); - fileData = data.files; - renderFileTable(folder); - } else { - fileListContainer.textContent = "No files found."; - updateFileActionButtons(); - } - return data.files || []; - }) - .catch(error => { - console.error("Error loading file list:", error); - fileListContainer.textContent = "Error loading files."; - return []; - }) - .finally(() => { - // Make the container visible after processing - fileListContainer.style.visibility = "visible"; - }); -} - -// Debounce helper (if not defined already) -function debounce(func, wait) { - let timeout; - return function (...args) { - clearTimeout(timeout); - timeout = setTimeout(() => func.apply(this, args), wait); - }; -} - -export function renderFileTable(folder) { - const fileListContainer = document.getElementById("fileList"); - const folderPath = (folder === "root") - ? "uploads/" - : "uploads/" + folder.split("/").map(encodeURIComponent).join("/") + "/"; - - // Use the global search term if available. - const searchTerm = window.currentSearchTerm || ""; - - const filteredFiles = fileData.filter(file => - file.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); - - // Get persistent items per page from localStorage. - const itemsPerPageSetting = parseInt(localStorage.getItem('itemsPerPage') || '10', 10); - - // Use a mutable currentPage variable - let currentPage = window.currentPage || 1; - const totalFiles = filteredFiles.length; - const totalPages = Math.ceil(totalFiles / itemsPerPageSetting); - - // If the current page is greater than totalPages, reset it to a valid page (for example, page 1 or totalPages) - if (currentPage > totalPages) { - currentPage = totalPages > 0 ? totalPages : 1; - window.currentPage = currentPage; - } - - const safeSearchTerm = escapeHTML(searchTerm); - - const topControlsHTML = ` -
-
-
-
- - search - -
- -
-
-
-
- - Page ${currentPage} of ${totalPages || 1} - -
-
-
- `; - - let tableHTML = ` - - - - - - - - - - - - - `; - - const startIndex = (currentPage - 1) * itemsPerPageSetting; - const endIndex = Math.min(startIndex + itemsPerPageSetting, totalFiles); - let tableBody = ``; - - if (totalFiles > 0) { - filteredFiles.slice(startIndex, endIndex).forEach(file => { - const isEditable = canEditFile(file.name); - const safeFileName = escapeHTML(file.name); - const safeModified = escapeHTML(file.modified); - const safeUploaded = escapeHTML(file.uploaded); - const safeSize = escapeHTML(file.size); - const safeUploader = escapeHTML(file.uploader || "Unknown"); - - const isViewable = /\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic|pdf|mp4|webm|mov|ogg)$/i.test(file.name); - let previewButton = ""; - if (isViewable) { - let previewIcon = ""; - if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(file.name)) { - previewIcon = `image`; - } else if (/\.(mp4|webm|mov|ogg)$/i.test(file.name)) { - previewIcon = `videocam`; - } else if (/\.pdf$/i.test(file.name)) { - previewIcon = `picture_as_pdf`; - } - - previewButton = ``; - } - - tableBody += ` - - - - - - - - - - `; - }); - } else { - tableBody += ``; - } - tableBody += `
File Name ${sortOrder.column === "name" ? (sortOrder.ascending ? "▲" : "▼") : ""}Date Modified ${sortOrder.column === "modified" ? (sortOrder.ascending ? "▲" : "▼") : ""}Upload Date ${sortOrder.column === "uploaded" ? (sortOrder.ascending ? "▲" : "▼") : ""}File Size ${sortOrder.column === "size" ? (sortOrder.ascending ? "▲" : "▼") : ""}Uploader ${sortOrder.column === "uploader" ? (sortOrder.ascending ? "▲" : "▼") : ""}Actions
- - ${safeFileName}${safeModified}${safeUploaded}${safeSize}${safeUploader} -
- Download - ${isEditable ? `` : ""} - ${previewButton} - -
-
No files found.
`; - - const bottomControlsHTML = ` -
- - - items per page -
- `; - - fileListContainer.innerHTML = topControlsHTML + tableHTML + tableBody + bottomControlsHTML; - - // Re-attach event listener for the new search input element. - const newSearchInput = document.getElementById("searchInput"); - if (newSearchInput) { - newSearchInput.addEventListener("input", debounce(function () { - window.currentSearchTerm = newSearchInput.value; - window.currentPage = 1; - renderFileTable(folder); - // After re-rendering, restore focus and caret position. - setTimeout(() => { - const freshInput = document.getElementById("searchInput"); - if (freshInput) { - freshInput.focus(); - freshInput.setSelectionRange(freshInput.value.length, freshInput.value.length); - } - }, 0); - }, 300)); - } - - // Add event listeners for header sorting. - document.querySelectorAll("table.table thead th[data-column]").forEach(cell => { - cell.addEventListener("click", function () { - const column = this.getAttribute("data-column"); - sortFiles(column, folder); - }); - }); - - // Add event listeners for checkboxes. - document.querySelectorAll('#fileList .file-checkbox').forEach(checkbox => { - checkbox.addEventListener('change', function (e) { - updateRowHighlight(e.target); - updateFileActionButtons(); - }); - }); - - updateFileActionButtons(); -} - -// Global function to show an image preview modal. -window.previewFile = function (fileUrl, fileName) { - let modal = document.getElementById("filePreviewModal"); - if (!modal) { - modal = document.createElement("div"); - modal.id = "filePreviewModal"; - // Use the same styling as the original image modal. - Object.assign(modal.style, { - display: "none", - position: "fixed", - top: "0", - left: "0", - width: "100vw", - height: "100vh", - backgroundColor: "rgba(0,0,0,0.7)", - display: "flex", - justifyContent: "center", - alignItems: "center", - zIndex: "1000" - }); - modal.innerHTML = ` - `; - document.body.appendChild(modal); - - // Close event for the close button. - document.getElementById("closeFileModal").addEventListener("click", function () { - const video = modal.querySelector("video"); - if (video) { - video.pause(); - video.currentTime = 0; - } - modal.style.display = "none"; - }); - - // Close event when clicking outside the modal content. - modal.addEventListener("click", function (e) { - if (e.target === modal) { - const video = modal.querySelector("video"); - if (video) { - video.pause(); - video.currentTime = 0; - } - modal.style.display = "none"; - } - }); - } - - modal.querySelector("h4").textContent = fileName; - const container = modal.querySelector(".file-preview-container"); - container.innerHTML = ""; // Clear previous content - - const extension = fileName.split('.').pop().toLowerCase(); - - if (/\.(jpg|jpeg|png|gif|bmp|webp|svg|ico|tif|tiff|eps|heic)$/i.test(fileName)) { - // Image preview - const img = document.createElement("img"); - img.src = fileUrl; - img.className = "image-modal-img"; - container.appendChild(img); - } else if (extension === "pdf") { - // PDF preview using with explicit sizing - const embed = document.createElement("embed"); - // Append a timestamp to force reload - const separator = fileUrl.indexOf('?') === -1 ? '?' : '&'; - embed.src = fileUrl + separator + 't=' + new Date().getTime(); - embed.type = "application/pdf"; - embed.style.width = "80vw"; - embed.style.height = "80vh"; - embed.style.border = "none"; - container.appendChild(embed); - } else if (/\.(mp4|webm|mov|ogg)$/i.test(fileName)) { - // Video preview using