/** * FileActionContextMenu — Floating Right-Click-Menu für FolderTree. * * Wird vom FolderTree gemountet wenn `onContextMenu` auf einer Zeile feuert. * Schließt sich bei Backdrop-Klick, ESC oder nach Aktion-Dispatch. */ import React, { useEffect, useRef } from 'react'; import { type FileAction, type FileActionContext, type FileActionTarget, resolveActionLabel, } from './types'; import { runAction } from './registry'; import styles from './FileActionContextMenu.module.css'; interface Props { /** Sichtbar/positioniert. ``null`` → nicht gemountet. */ anchor: { x: number; y: number } | null; actions: FileAction[]; target: FileActionTarget; ctx: FileActionContext; /** Wird aufgerufen sobald das Menü schließen soll (Backdrop, ESC, nach Action). */ onClose: () => void; /** Optional: Header-Label (z. B. Dateiname). */ title?: string; /** Optionaler Confirm-Provider (z. B. browser native ``window.confirm``). */ confirm?: (title: string, body: string) => boolean | Promise; } export const FileActionContextMenu: React.FC = ({ anchor, actions, target, ctx, onClose, title, confirm, }) => { const menuRef = useRef(null); useEffect(() => { if (!anchor) return; const _onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', _onKey); return () => window.removeEventListener('keydown', _onKey); }, [anchor, onClose]); useEffect(() => { if (!anchor || !menuRef.current) return; menuRef.current.focus(); }, [anchor]); if (!anchor) return null; const adjusted = _adjustToViewport(anchor, menuRef.current); const _handleClick = async (action: FileAction) => { onClose(); await runAction(action, target, ctx, confirm); }; return ( <>
{ e.preventDefault(); onClose(); }} />
{title &&
{title}
} {actions.length === 0 ? (
) : ( actions.map((a, idx) => { const Icon = a.icon; const isDangerCls = a.danger ? `${styles.item} ${styles.danger}` : styles.item; return ( {idx > 0 && a.danger && actions[idx - 1] && !actions[idx - 1].danger && (
)} ); }) )}
); }; function _adjustToViewport( anchor: { x: number; y: number }, menu: HTMLDivElement | null, ): { x: number; y: number } { if (!menu) return anchor; const rect = menu.getBoundingClientRect(); const vw = window.innerWidth; const vh = window.innerHeight; const margin = 4; let x = anchor.x; let y = anchor.y; if (x + rect.width + margin > vw) x = Math.max(margin, vw - rect.width - margin); if (y + rect.height + margin > vh) y = Math.max(margin, vh - rect.height - margin); return { x, y }; } function _formatShortcut(s: string): string { const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform); return s .split('+') .map((part) => { const k = part.trim().toLowerCase(); if (k === 'mod') return isMac ? '\u2318' : 'Ctrl'; if (k === 'shift') return isMac ? '\u21E7' : 'Shift'; if (k === 'alt') return isMac ? '\u2325' : 'Alt'; if (k === 'ctrl') return 'Ctrl'; return k.length === 1 ? k.toUpperCase() : part; }) .join(isMac ? '' : '+'); }