mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-30 09:03:15 +00:00
(feat:docs):add contextMenu to actions
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { SyntheticEvent, useRef, useEffect } from 'react';
|
||||
import { SyntheticEvent, useRef, useEffect, CSSProperties } from 'react';
|
||||
|
||||
export interface MenuOption {
|
||||
icon?: string;
|
||||
@@ -27,82 +27,93 @@ export default function ContextMenu({
|
||||
anchorRef,
|
||||
className = '',
|
||||
position = 'bottom-right',
|
||||
offset = { x: 1, y: 5 },
|
||||
offset = { x: 0, y: 8 },
|
||||
}: ContextMenuProps) {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
menuRef.current &&
|
||||
!menuRef.current.contains(event.target as Node) &&
|
||||
!anchorRef.current?.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getPositionClasses = () => {
|
||||
const positionMap = {
|
||||
'bottom-right': 'translate-x-1 translate-y-5',
|
||||
'bottom-left': '-translate-x-full translate-y-5',
|
||||
'top-right': 'translate-x-1 -translate-y-full',
|
||||
'top-left': '-translate-x-full -translate-y-full',
|
||||
};
|
||||
return positionMap[position];
|
||||
};
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () =>
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [isOpen, setIsOpen]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const getMenuPosition = () => {
|
||||
const getMenuPosition = (): CSSProperties => {
|
||||
if (!anchorRef.current) return {};
|
||||
|
||||
const rect = anchorRef.current.getBoundingClientRect();
|
||||
return {
|
||||
top: `${rect.top + window.scrollY + offset.y}px`,
|
||||
};
|
||||
};
|
||||
const scrollY = window.scrollY || document.documentElement.scrollTop;
|
||||
const scrollX = window.scrollX || document.documentElement.scrollLeft;
|
||||
|
||||
const getOptionStyles = (option: MenuOption, index: number) => {
|
||||
if (option.variant === 'danger') {
|
||||
return `
|
||||
dark:text-red-2000 dark:hover:bg-charcoal-grey
|
||||
text-rosso-corsa hover:bg-bright-gray
|
||||
}`;
|
||||
let top = rect.bottom + scrollY + offset.y;
|
||||
let left = rect.right + scrollX + offset.x;
|
||||
|
||||
// Adjust position based on position prop
|
||||
switch (position) {
|
||||
case 'bottom-left':
|
||||
left = rect.left + scrollX - offset.x;
|
||||
break;
|
||||
case 'top-right':
|
||||
top = rect.top + scrollY - offset.y;
|
||||
break;
|
||||
case 'top-left':
|
||||
top = rect.top + scrollY - offset.y;
|
||||
left = rect.left + scrollX - offset.x;
|
||||
break;
|
||||
// bottom-right is default
|
||||
}
|
||||
|
||||
return `
|
||||
dark:text-bright-gray dark:hover:bg-charcoal-grey
|
||||
text-eerie-black hover:bg-bright-gray
|
||||
}`;
|
||||
return {
|
||||
position: 'fixed',
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={`absolute z-30 ${getPositionClasses()} ${className}`}
|
||||
style={getMenuPosition()}
|
||||
className={`fixed z-50 ${className}`}
|
||||
style={{ ...getMenuPosition() }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
className={`flex w-32 flex-col rounded-xl text-sm shadow-xl md:w-36 dark:bg-charleston-green-2 bg-lotion`}
|
||||
className="flex w-32 flex-col rounded-xl text-sm shadow-xl md:w-36 dark:bg-charleston-green-2 bg-lotion"
|
||||
style={{ minWidth: '144px' }}
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={(event: SyntheticEvent) => {
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
option.onClick(event);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={`${`
|
||||
flex justify-start items-center gap-4 p-3
|
||||
transition-colors duration-200 ease-in-out
|
||||
${index === 0 ? 'rounded-t-xl' : ''}
|
||||
${index === options.length - 1 ? 'rounded-b-xl' : ''}
|
||||
`}${getOptionStyles(option, index)}`}
|
||||
className={`
|
||||
flex justify-start items-center gap-4 p-3
|
||||
transition-colors duration-200 ease-in-out
|
||||
${index === 0 ? 'rounded-t-xl' : ''}
|
||||
${index === options.length - 1 ? 'rounded-b-xl' : ''}
|
||||
${
|
||||
option.variant === 'danger'
|
||||
? 'dark:text-red-2000 dark:hover:bg-charcoal-grey text-rosso-corsa hover:bg-bright-gray'
|
||||
: 'dark:text-bright-gray dark:hover:bg-charcoal-grey text-eerie-black hover:bg-bright-gray'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{option.icon && (
|
||||
<img
|
||||
@@ -110,7 +121,7 @@ export default function ContextMenu({
|
||||
height={option.iconHeight || 16}
|
||||
src={option.icon}
|
||||
alt={option.label}
|
||||
className={`cursor-pointer hover:opacity-75 ${option.iconClassName}`}
|
||||
className={`cursor-pointer hover:opacity-75 ${option.iconClassName || ''}`}
|
||||
/>
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
|
||||
@@ -6,6 +6,11 @@ type DropdownMenuProps = {
|
||||
onSelect: (value: string) => void;
|
||||
defaultValue?: string;
|
||||
icon?: string;
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
anchorRef?: React.RefObject<HTMLElement>;
|
||||
className?: string;
|
||||
position?: 'left' | 'right';
|
||||
};
|
||||
|
||||
export default function DropdownMenu({
|
||||
@@ -14,16 +19,22 @@ export default function DropdownMenu({
|
||||
onSelect,
|
||||
defaultValue = 'none',
|
||||
icon,
|
||||
isOpen: controlledIsOpen,
|
||||
onOpenChange,
|
||||
anchorRef,
|
||||
className,
|
||||
position = 'left',
|
||||
}: DropdownMenuProps) {
|
||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [internalIsOpen, setInternalIsOpen] = React.useState(false);
|
||||
const [selectedOption, setSelectedOption] = React.useState(
|
||||
options.find((option) => option.value === defaultValue) || options[0],
|
||||
);
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
const isOpen =
|
||||
controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
|
||||
const setIsOpen = onOpenChange || setInternalIsOpen;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
@@ -32,6 +43,7 @@ export default function DropdownMenu({
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOption = (optionId: number) => {
|
||||
setIsOpen(false);
|
||||
setSelectedOption(options[optionId]);
|
||||
@@ -44,17 +56,11 @@ export default function DropdownMenu({
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="static inline-block text-left" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
className="flex w-20 cursor-pointer flex-row gap-1 rounded-3xl border-purple-30/25 bg-purple-30 p-2 text-xs text-white hover:bg-[#6F3FD1] focus:outline-none"
|
||||
>
|
||||
{icon && <img src={icon} alt="OptionIcon" className="h-4 w-4" />}
|
||||
{selectedOption.value !== 'never' ? selectedOption.label : name}
|
||||
</button>
|
||||
<div className={`fixed ${className || ''}`} ref={dropdownRef}>
|
||||
<div
|
||||
className={`absolute z-50 right-0 mt-1 w-28 transform rounded-md bg-transparent shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-200 ease-in-out ${
|
||||
className={`w-28 transform rounded-md bg-white dark:bg-dark-charcoal shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-200 ease-in-out ${
|
||||
isOpen
|
||||
? 'scale-100 opacity-100'
|
||||
: 'pointer-events-none scale-95 opacity-0'
|
||||
|
||||
Reference in New Issue
Block a user