Add folder-strip context menu & combined Create File/Folder dropdown

This commit is contained in:
Ryan
2025-05-23 08:55:09 -04:00
committed by GitHub
parent 9209f7a582
commit 16ccb66d55
8 changed files with 206 additions and 29 deletions

View File

@@ -1,5 +1,37 @@
# Changelog # Changelog
## Changes 5/23/2025 v1.3.8
- **Folder-strip context menu**
- Enabled right-click on items in the new folder strip (above file list) to open the same “Create / Rename / Share / Delete Folder” menu as in the main folder tree.
- Bound `contextmenu` event on each `.folder-item` in `loadFileList` to:
- Prevent the default browser menu
- Highlight the clicked folder-strip item
- Invoke `showFolderManagerContextMenu` with menu entries:
- Create Folder
- Rename Folder
- Share Folder (passes the strips `data-folder` value)
- Delete Folder
- Ensured menu actions are wrapped in arrow functions (`() => …`) so they fire only on menu-item click, not on render.
- Refactored folder-strip injection in `fileListView.js` to:
- Mark each strip item as `draggable="true"` (for drag-and-drop)
- Add `el.addEventListener("contextmenu", …)` alongside existing click/drag handlers
- Clean up global click listener for hiding the context menu
- Prevented premature invocation of `openFolderShareModal` by switching to `action: () => openFolderShareModal(dest)` instead of calling it directly.
- **Create File/Folder dropdown**
- Replaced standalone “Create File” button with a combined dropdown button in the actions toolbar.
- New markup
- Wired up JS handlers in `fileActions.js`:
- `#createFileOption``openCreateFileModal()`
- `#createFolderOption``document.getElementById('createFolderModal').style.display = 'block'`
- Toggled `.dropdown-menu` visibility on button click, and closed on outside click.
- Applied dark-mode support: dropdown background and text colors switch with `.dark-mode` class.
---
## Changes 5/22/2025 v1.3.7 ## Changes 5/22/2025 v1.3.7
- `.folder-strip-container .folder-name` css added to center text below folder material icon. - `.folder-strip-container .folder-name` css added to center text below folder material icon.

View File

@@ -848,11 +848,27 @@ body:not(.dark-mode) .material-icons.pauseResumeBtn:hover {
background-color: #00796B; background-color: #00796B;
} }
#createFileBtn { #createBtn {
background-color: #007bff; background-color: #007bff;
color: white; color: white;
} }
body.dark-mode .dropdown-menu {
background-color: #2c2c2c !important;
border-color: #444 !important;
color: #e0e0e0!important;
}
body.dark-mode .dropdown-menu .dropdown-item {
color: #e0e0e0 !important;
}
.dropdown-item:hover {
background-color: rgba(0,0,0,0.05);
}
body.dark-mode .dropdown-item:hover {
background-color: rgba(255,255,255,0.1);
}
#fileList button.edit-btn { #fileList button.edit-btn {
background-color: #007bff; background-color: #007bff;
color: white; color: white;

View File

@@ -391,9 +391,36 @@
data-i18n-key="download_zip">Download ZIP</button> data-i18n-key="download_zip">Download ZIP</button>
<button id="extractZipBtn" class="btn action-btn btn-sm btn-info" data-i18n-title="extract_zip" <button id="extractZipBtn" class="btn action-btn btn-sm btn-info" data-i18n-title="extract_zip"
data-i18n-key="extract_zip_button">Extract Zip</button> data-i18n-key="extract_zip_button">Extract Zip</button>
<button id="createFileBtn" class="btn action-btn" data-i18n-key="create_file"> <div id="createDropdown" class="dropdown-container" style="position:relative; display:inline-block;">
${t('create_file')} <button id="createBtn" class="btn action-btn" data-i18n-key="create">
</button> ${t('create')} <span class="material-icons" style="font-size:16px;vertical-align:middle;">arrow_drop_down</span>
</button>
<ul
id="createMenu"
class="dropdown-menu"
style="
display: none;
position: absolute;
top: 100%;
left: 0;
margin: 4px 0 0;
padding: 0;
list-style: none;
background: #fff;
border: 1px solid #ccc;
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
z-index: 1000;
min-width: 140px;
"
>
<li id="createFileOption" class="dropdown-item" data-i18n-key="create_file" style="padding:8px 12px; cursor:pointer;">
${t('create_file')}
</li>
<li id="createFolderOption" class="dropdown-item" data-i18n-key="create_folder" style="padding:8px 12px; cursor:pointer;">
${t('create_folder')}
</li>
</ul>
</div>
<!-- Create File Modal --> <!-- Create File Modal -->
<div id="createFileModal" class="modal" style="display:none;"> <div id="createFileModal" class="modal" style="display:none;">
<div class="modal-content"> <div class="modal-content">

View File

@@ -3,7 +3,7 @@ import { loadAdminConfigFunc } from './auth.js';
import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js'; import { showToast, toggleVisibility, attachEnterKeyListener } from './domUtils.js';
import { sendRequest } from './networkUtils.js'; import { sendRequest } from './networkUtils.js';
const version = "v1.3.7"; const version = "v1.3.8";
const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`; const adminTitle = `${t("admin_panel")} <small style="font-size:12px;color:gray;">${version}</small>`;
// ————— Inject updated styles ————— // ————— Inject updated styles —————

View File

@@ -39,7 +39,7 @@ export function updateFileActionButtons() {
const moveBtn = document.getElementById("moveSelectedBtn"); const moveBtn = document.getElementById("moveSelectedBtn");
const zipBtn = document.getElementById("downloadZipBtn"); const zipBtn = document.getElementById("downloadZipBtn");
const extractZipBtn = document.getElementById("extractZipBtn"); const extractZipBtn = document.getElementById("extractZipBtn");
const createBtn = document.getElementById("createFileBtn"); const createBtn = document.getElementById("createBtn");
const anyFiles = fileCheckboxes.length > 0; const anyFiles = fileCheckboxes.length > 0;
const anySelected = selectedCheckboxes.length > 0; const anySelected = selectedCheckboxes.length > 0;

View File

@@ -688,4 +688,35 @@ document.addEventListener("DOMContentLoaded", () => {
attachEnterKeyListener("downloadFileModal", "confirmSingleDownloadButton"); attachEnterKeyListener("downloadFileModal", "confirmSingleDownloadButton");
}); });
document.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('createBtn');
const menu = document.getElementById('createMenu');
const fileOpt = document.getElementById('createFileOption');
const folderOpt= document.getElementById('createFolderOption');
// Toggle dropdown on click
btn.addEventListener('click', (e) => {
e.stopPropagation();
menu.style.display = menu.style.display === 'block' ? 'none' : 'block';
});
// Create File
fileOpt.addEventListener('click', () => {
menu.style.display = 'none';
openCreateFileModal(); // your existing function
});
// Create Folder
folderOpt.addEventListener('click', () => {
menu.style.display = 'none';
document.getElementById('createFolderModal').style.display = 'block';
document.getElementById('newFolderName').focus();
});
// Close if you click anywhere else
document.addEventListener('click', () => {
menu.style.display = 'none';
});
});
window.renameFile = renameFile; window.renameFile = renameFile;

View File

@@ -16,7 +16,16 @@ import { t } from './i18n.js';
import { bindFileListContextMenu } from './fileMenu.js'; import { bindFileListContextMenu } from './fileMenu.js';
import { openDownloadModal } from './fileActions.js'; import { openDownloadModal } from './fileActions.js';
import { openTagModal, openMultiTagModal } from './fileTags.js'; import { openTagModal, openMultiTagModal } from './fileTags.js';
import { getParentFolder, updateBreadcrumbTitle, setupBreadcrumbDelegation } from './folderManager.js'; import {
getParentFolder,
updateBreadcrumbTitle,
setupBreadcrumbDelegation,
showFolderManagerContextMenu,
hideFolderManagerContextMenu,
openRenameFolderModal,
openDeleteFolderModal
} from './folderManager.js';
import { openFolderShareModal } from './folderShareModal.js';
import { import {
folderDragOverHandler, folderDragOverHandler,
folderDragLeaveHandler, folderDragLeaveHandler,
@@ -240,17 +249,40 @@ export async function loadFileList(folderParam) {
if (!data.files || Object.keys(data.files).length === 0) { if (!data.files || Object.keys(data.files).length === 0) {
fileListContainer.textContent = t("no_files_found"); fileListContainer.textContent = t("no_files_found");
// hide summary // hide summary + slider
const summaryElem = document.getElementById("fileSummary"); const summaryElem = document.getElementById("fileSummary");
if (summaryElem) summaryElem.style.display = "none"; if (summaryElem) summaryElem.style.display = "none";
// hide slider
const sliderContainer = document.getElementById("viewSliderContainer"); const sliderContainer = document.getElementById("viewSliderContainer");
if (sliderContainer) sliderContainer.style.display = "none"; if (sliderContainer) sliderContainer.style.display = "none";
// hide folder strip // show/hide folder strip *even when there are no files*
const strip = document.getElementById("folderStripContainer"); let strip = document.getElementById("folderStripContainer");
if (strip) strip.style.display = "none"; if (!strip) {
strip = document.createElement("div");
strip.id = "folderStripContainer";
strip.className = "folder-strip-container";
actionsContainer.parentNode.insertBefore(strip, fileListContainer);
}
if (window.showFoldersInList && subfolders.length) {
strip.innerHTML = subfolders.map(sf => `
<div class="folder-item" data-folder="${sf.full}">
<i class="material-icons">folder</i>
<div class="folder-name">${escapeHTML(sf.name)}</div>
</div>
`).join("");
strip.style.display = "flex";
strip.querySelectorAll(".folder-item").forEach(el => {
el.addEventListener("click", () => {
const dest = el.dataset.folder;
window.currentFolder = dest;
localStorage.setItem("lastOpenedFolder", dest);
updateBreadcrumbTitle(dest);
loadFileList(dest);
});
});
} else {
strip.style.display = "none";
}
updateFileActionButtons(); updateFileActionButtons();
return []; return [];
@@ -360,34 +392,73 @@ export async function loadFileList(folderParam) {
strip.className = "folder-strip-container"; strip.className = "folder-strip-container";
actionsContainer.parentNode.insertBefore(strip, actionsContainer); actionsContainer.parentNode.insertBefore(strip, actionsContainer);
} }
if (window.showFoldersInList && subfolders.length) { if (window.showFoldersInList && subfolders.length) {
strip.innerHTML = subfolders.map(sf => ` strip.innerHTML = subfolders.map(sf => `
<div class="folder-item" data-folder="${sf.full}" draggable="true"> <div class="folder-item" data-folder="${sf.full}" draggable="true">
<i class="material-icons">folder</i> <i class="material-icons">folder</i>
<div class="folder-name">${escapeHTML(sf.name)}</div> <div class="folder-name">${escapeHTML(sf.name)}</div>
</div> </div>
`).join(""); `).join("");
strip.style.display = "flex"; strip.style.display = "flex";
// wire up each foldertile
strip.querySelectorAll(".folder-item").forEach(el => { strip.querySelectorAll(".folder-item").forEach(el => {
// clicktonavigate // 1) click to navigate
el.addEventListener("click", () => { el.addEventListener("click", () => {
const dest = el.dataset.folder; const dest = el.dataset.folder;
window.currentFolder = dest; window.currentFolder = dest;
localStorage.setItem("lastOpenedFolder", dest); localStorage.setItem("lastOpenedFolder", dest);
updateBreadcrumbTitle(dest); updateBreadcrumbTitle(dest);
document.querySelectorAll(".folder-option.selected") document.querySelectorAll(".folder-option.selected").forEach(o => o.classList.remove("selected"));
.forEach(o => o.classList.remove("selected")); document.querySelector(`.folder-option[data-folder="${dest}"]`)?.classList.add("selected");
document.querySelector(`.folder-option[data-folder="${dest}"]`)
?.classList.add("selected");
loadFileList(dest); loadFileList(dest);
}); });
// drag & drop handlers // 2) drag & drop
el.addEventListener("dragover", folderDragOverHandler); el.addEventListener("dragover", folderDragOverHandler);
el.addEventListener("dragleave", folderDragLeaveHandler); el.addEventListener("dragleave", folderDragLeaveHandler);
el.addEventListener("drop", folderDropHandler); el.addEventListener("drop", folderDropHandler);
// 3) right-click context menu
el.addEventListener("contextmenu", e => {
e.preventDefault();
e.stopPropagation();
const dest = el.dataset.folder;
window.currentFolder = dest;
localStorage.setItem("lastOpenedFolder", dest);
// highlight the strip tile
strip.querySelectorAll(".folder-item.selected").forEach(i => i.classList.remove("selected"));
el.classList.add("selected");
// reuse folderManager menu
const menuItems = [
{
label: t("create_folder"),
action: () => document.getElementById("createFolderModal").style.display = "block"
},
{
label: t("rename_folder"),
action: () => openRenameFolderModal()
},
{
label: t("folder_share"),
action: () => openFolderShareModal(dest)
},
{
label: t("delete_folder"),
action: () => openDeleteFolderModal()
}
];
showFolderManagerContextMenu(e.pageX, e.pageY, menuItems);
});
}); });
// one global click to hide any open context menu
document.addEventListener("click", hideFolderManagerContextMenu);
} else { } else {
strip.style.display = "none"; strip.style.display = "none";
} }

View File

@@ -551,7 +551,7 @@ export function loadFolderList(selectedFolder) {
document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal); document.getElementById("renameFolderBtn").addEventListener("click", openRenameFolderModal);
document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal); document.getElementById("deleteFolderBtn").addEventListener("click", openDeleteFolderModal);
function openRenameFolderModal() { export function openRenameFolderModal() {
const selectedFolder = window.currentFolder || "root"; const selectedFolder = window.currentFolder || "root";
if (!selectedFolder || selectedFolder === "root") { if (!selectedFolder || selectedFolder === "root") {
showToast("Please select a valid folder to rename."); showToast("Please select a valid folder to rename.");
@@ -614,7 +614,7 @@ document.getElementById("submitRenameFolder").addEventListener("click", function
}); });
}); });
function openDeleteFolderModal() { export function openDeleteFolderModal() {
const selectedFolder = window.currentFolder || "root"; const selectedFolder = window.currentFolder || "root";
if (!selectedFolder || selectedFolder === "root") { if (!selectedFolder || selectedFolder === "root") {
showToast("Please select a valid folder to delete."); showToast("Please select a valid folder to delete.");
@@ -718,7 +718,7 @@ document.getElementById("submitCreateFolder").addEventListener("click", async ()
}); });
// ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ---------- // ---------- CONTEXT MENU SUPPORT FOR FOLDER MANAGER ----------
function showFolderManagerContextMenu(x, y, menuItems) { export function showFolderManagerContextMenu(x, y, menuItems) {
let menu = document.getElementById("folderManagerContextMenu"); let menu = document.getElementById("folderManagerContextMenu");
if (!menu) { if (!menu) {
menu = document.createElement("div"); menu = document.createElement("div");
@@ -765,7 +765,7 @@ function showFolderManagerContextMenu(x, y, menuItems) {
menu.style.display = "block"; menu.style.display = "block";
} }
function hideFolderManagerContextMenu() { export function hideFolderManagerContextMenu() {
const menu = document.getElementById("folderManagerContextMenu"); const menu = document.getElementById("folderManagerContextMenu");
if (menu) { if (menu) {
menu.style.display = "none"; menu.style.display = "none";
@@ -796,7 +796,7 @@ function folderManagerContextMenuHandler(e) {
}, },
{ {
label: t("folder_share"), label: t("folder_share"),
action: () => { openFolderShareModal(); } action: () => { openFolderShareModal(folder); }
}, },
{ {
label: t("delete_folder"), label: t("delete_folder"),