253 lines
7.7 KiB
JavaScript
253 lines
7.7 KiB
JavaScript
// fileMenu.js
|
||
import { t } from './i18n.js?v={{APP_QVER}}';
|
||
import { updateRowHighlight } from './domUtils.js?v={{APP_QVER}}';
|
||
import {
|
||
handleDeleteSelected, handleCopySelected, handleMoveSelected,
|
||
handleDownloadZipSelected, handleExtractZipSelected,
|
||
renameFile, openCreateFileModal
|
||
} from './fileActions.js?v={{APP_QVER}}';
|
||
import { previewFile, buildPreviewUrl } from './filePreview.js?v={{APP_QVER}}';
|
||
import { editFile } from './fileEditor.js?v={{APP_QVER}}';
|
||
import { canEditFile, fileData, downloadSelectedFilesIndividually } from './fileListView.js?v={{APP_QVER}}';
|
||
import { openTagModal, openMultiTagModal } from './fileTags.js?v={{APP_QVER}}';
|
||
import { escapeHTML } from './domUtils.js?v={{APP_QVER}}';
|
||
|
||
|
||
const MENU_ID = 'fileContextMenu';
|
||
|
||
function qMenu() { return document.getElementById(MENU_ID); }
|
||
function setText(btn, key) { btn.querySelector('span').textContent = t(key); }
|
||
|
||
// One-time: localize labels
|
||
function localizeMenu() {
|
||
const m = qMenu(); if (!m) return;
|
||
const map = {
|
||
'create_file': 'create_file',
|
||
'delete_selected': 'delete_selected',
|
||
'copy_selected': 'copy_selected',
|
||
'move_selected': 'move_selected',
|
||
'download_zip': 'download_zip',
|
||
'extract_zip': 'extract_zip',
|
||
'tag_selected': 'tag_selected',
|
||
'preview': 'preview',
|
||
'edit': 'edit',
|
||
'rename': 'rename',
|
||
'tag_file': 'tag_file',
|
||
// NEW:
|
||
'download_plain': 'download_plain'
|
||
};
|
||
Object.entries(map).forEach(([action, key]) => {
|
||
const el = m.querySelector(`.mi[data-action="${action}"]`);
|
||
if (el) setText(el, key);
|
||
});
|
||
}
|
||
|
||
// Show/hide items based on selection state
|
||
function configureVisibility({ any, one, many, anyZip, canEdit }) {
|
||
const m = qMenu(); if (!m) return;
|
||
|
||
const show = (sel, on) => sel.forEach(el => el.hidden = !on);
|
||
|
||
show(m.querySelectorAll('[data-when="always"]'), true);
|
||
show(m.querySelectorAll('[data-when="any"]'), any);
|
||
show(m.querySelectorAll('[data-when="one"]'), one);
|
||
show(m.querySelectorAll('[data-when="many"]'), many);
|
||
show(m.querySelectorAll('[data-when="zip"]'), anyZip);
|
||
show(m.querySelectorAll('[data-when="can-edit"]'), canEdit);
|
||
|
||
// Hide separators at edges or duplicates
|
||
cleanupSeparators(m);
|
||
}
|
||
|
||
function cleanupSeparators(menu) {
|
||
const kids = Array.from(menu.children);
|
||
let lastWasSep = true; // leading seps hidden
|
||
kids.forEach((el, i) => {
|
||
if (el.classList.contains('sep')) {
|
||
const hide = lastWasSep || (i === kids.length - 1);
|
||
el.hidden = hide || el.hidden; // keep hidden if already hidden by state
|
||
lastWasSep = !el.hidden;
|
||
} else if (!el.hidden) {
|
||
lastWasSep = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Position menu within viewport
|
||
function placeMenu(x, y) {
|
||
const m = qMenu(); if (!m) return;
|
||
|
||
// make visible to measure
|
||
m.hidden = false;
|
||
m.style.left = '0px';
|
||
m.style.top = '0px';
|
||
|
||
// force a max-height via CSS fallback if styles didn't load yet
|
||
const pad = 8;
|
||
const vh = window.innerHeight, vw = window.innerWidth;
|
||
const mh = Math.min(vh - pad*2, 600); // JS fallback limit
|
||
m.style.maxHeight = mh + 'px';
|
||
|
||
// measure now that it's flow-visible
|
||
const r0 = m.getBoundingClientRect();
|
||
let nx = x, ny = y;
|
||
|
||
// If it would overflow right, shift left
|
||
if (nx + r0.width > vw - pad) nx = Math.max(pad, vw - r0.width - pad);
|
||
// If it would overflow bottom, try placing it above the cursor
|
||
if (ny + r0.height > vh - pad) {
|
||
const above = y - r0.height - 4;
|
||
ny = (above >= pad) ? above : Math.max(pad, vh - r0.height - pad);
|
||
}
|
||
|
||
// Guard top/left minimums
|
||
nx = Math.max(pad, nx);
|
||
ny = Math.max(pad, ny);
|
||
|
||
m.style.left = `${nx}px`;
|
||
m.style.top = `${ny}px`;
|
||
}
|
||
|
||
export function hideFileContextMenu() {
|
||
const m = qMenu();
|
||
if (m) m.hidden = true;
|
||
}
|
||
|
||
function currentSelection() {
|
||
const checks = Array.from(document.querySelectorAll('#fileList .file-checkbox'));
|
||
// checkbox values are ESCAPED names (because buildFileTableRow used safeFileName)
|
||
const selectedEsc = checks.filter(cb => cb.checked).map(cb => cb.value);
|
||
const escSet = new Set(selectedEsc);
|
||
|
||
// map back to real file objects by comparing escaped(f.name)
|
||
const files = fileData.filter(f => escSet.has(escapeHTML(f.name)));
|
||
|
||
const any = files.length > 0;
|
||
const one = files.length === 1;
|
||
const many = files.length > 1;
|
||
const anyZip = files.some(f => f.name.toLowerCase().endsWith('.zip'));
|
||
const file = one ? files[0] : null;
|
||
const canEditFlag = !!(file && canEditFile(file.name));
|
||
|
||
// also return the raw names if any caller needs them
|
||
return {
|
||
files, // <— real file objects for modals
|
||
all: files.map(f => f.name),
|
||
any, one, many, anyZip,
|
||
file,
|
||
canEdit: canEditFlag
|
||
};
|
||
}
|
||
|
||
export function fileListContextMenuHandler(e) {
|
||
e.preventDefault();
|
||
|
||
// Check row if needed
|
||
const row = e.target.closest('tr');
|
||
if (row) {
|
||
const cb = row.querySelector('.file-checkbox');
|
||
if (cb && !cb.checked) {
|
||
cb.checked = true;
|
||
updateRowHighlight(cb);
|
||
}
|
||
}
|
||
|
||
const state = currentSelection();
|
||
configureVisibility(state);
|
||
placeMenu(e.clientX, e.clientY);
|
||
|
||
// Stash for click handlers
|
||
window.__filr_ctx_state = state;
|
||
}
|
||
|
||
// --- add near top ---
|
||
let __ctxBoundOnce = false;
|
||
|
||
function docClickClose(ev) {
|
||
const m = qMenu(); if (!m || m.hidden) return;
|
||
if (!m.contains(ev.target)) hideFileContextMenu();
|
||
}
|
||
function docKeyClose(ev) {
|
||
if (ev.key === 'Escape') hideFileContextMenu();
|
||
}
|
||
|
||
function menuClickDelegate(ev) {
|
||
const btn = ev.target.closest('.mi[data-action]');
|
||
if (!btn) return;
|
||
ev.stopPropagation();
|
||
|
||
// CLOSE MENU FIRST so it can’t overlay the modal
|
||
hideFileContextMenu();
|
||
|
||
const action = btn.dataset.action;
|
||
const s = window.__filr_ctx_state || currentSelection();
|
||
const folder = window.currentFolder || 'root';
|
||
|
||
switch (action) {
|
||
case 'create_file': openCreateFileModal(); break;
|
||
case 'delete_selected': handleDeleteSelected(new Event('click')); break;
|
||
case 'copy_selected': handleCopySelected(new Event('click')); break;
|
||
case 'move_selected': handleMoveSelected(new Event('click')); break;
|
||
case 'download_zip': handleDownloadZipSelected(new Event('click')); break;
|
||
case 'extract_zip': handleExtractZipSelected(new Event('click')); break;
|
||
case 'download_plain':
|
||
// Uses current checkbox selection; limit enforced in fileListView
|
||
downloadSelectedFilesIndividually(s.files);
|
||
break;
|
||
|
||
case 'tag_selected':
|
||
openMultiTagModal(s.files); // s.files are the real file objects
|
||
break;
|
||
|
||
case 'preview':
|
||
if (s.file) previewFile(buildPreviewUrl(folder, s.file.name), s.file.name);
|
||
break;
|
||
|
||
case 'edit':
|
||
if (s.file && s.canEdit) editFile(s.file.name, folder);
|
||
break;
|
||
|
||
case 'rename':
|
||
if (s.file) renameFile(s.file.name, folder);
|
||
break;
|
||
|
||
case 'tag_file':
|
||
if (s.file) openTagModal(s.file);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// keep your renderFileTable wrapper as-is
|
||
|
||
export function bindFileListContextMenu() {
|
||
const container = document.getElementById('fileList');
|
||
const menu = qMenu();
|
||
if (!container || !menu) return;
|
||
|
||
localizeMenu();
|
||
|
||
// Open on right click in the table
|
||
container.oncontextmenu = fileListContextMenuHandler;
|
||
|
||
// Bind once
|
||
if (!__ctxBoundOnce) {
|
||
document.addEventListener('click', docClickClose);
|
||
document.addEventListener('keydown', docKeyClose);
|
||
menu.addEventListener('click', menuClickDelegate); // handles actions
|
||
__ctxBoundOnce = true;
|
||
}
|
||
}
|
||
|
||
// Rebind after table render (keeps your original behavior)
|
||
(function () {
|
||
const orig = window.renderFileTable;
|
||
if (typeof orig === 'function') {
|
||
window.renderFileTable = function (folder) {
|
||
orig(folder);
|
||
bindFileListContextMenu();
|
||
};
|
||
} else {
|
||
// If not present yet, bind once DOM is ready
|
||
document.addEventListener('DOMContentLoaded', bindFileListContextMenu, { once: true });
|
||
}
|
||
})(); |