Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m54s
142 lines
4.1 KiB
React
142 lines
4.1 KiB
React
import { useState, useEffect, useRef } from 'react'
|
|
import { IconDotsVertical } from '@tabler/icons-react'
|
|
|
|
/**
|
|
* ContextMenu - компонент контекстного меню для действий
|
|
* Поддерживает правый клик и dropdown меню
|
|
*/
|
|
function ContextMenu({
|
|
items, // Array of { label, icon, onClick, variant, divider }
|
|
children, // Trigger element
|
|
align = 'right' // 'left' | 'right'
|
|
}) {
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
const [position, setPosition] = useState({ x: 0, y: 0 })
|
|
const [useContextPosition, setUseContextPosition] = useState(false)
|
|
const menuRef = useRef(null)
|
|
const triggerRef = useRef(null)
|
|
|
|
const handleContextMenu = (e) => {
|
|
e.preventDefault()
|
|
setPosition({ x: e.clientX, y: e.clientY })
|
|
setUseContextPosition(true)
|
|
setIsOpen(true)
|
|
}
|
|
|
|
const handleClick = () => {
|
|
if (triggerRef.current) {
|
|
const rect = triggerRef.current.getBoundingClientRect()
|
|
setPosition({
|
|
x: align === 'right' ? rect.right : rect.left,
|
|
y: rect.bottom
|
|
})
|
|
setUseContextPosition(false)
|
|
setIsOpen(!isOpen)
|
|
}
|
|
}
|
|
|
|
const handleItemClick = (onClick) => {
|
|
setIsOpen(false)
|
|
if (onClick) {
|
|
onClick()
|
|
}
|
|
}
|
|
|
|
// Закрытие при клике вне меню
|
|
useEffect(() => {
|
|
const handleClickOutside = (e) => {
|
|
if (menuRef.current && !menuRef.current.contains(e.target) &&
|
|
triggerRef.current && !triggerRef.current.contains(e.target)) {
|
|
setIsOpen(false)
|
|
}
|
|
}
|
|
|
|
if (isOpen) {
|
|
document.addEventListener('mousedown', handleClickOutside)
|
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
}
|
|
}, [isOpen])
|
|
|
|
return (
|
|
<div
|
|
ref={triggerRef}
|
|
className="context-menu-trigger"
|
|
onContextMenu={handleContextMenu}
|
|
style={{ display: 'inline-block' }}
|
|
>
|
|
{children ? (
|
|
<div onClick={handleClick}>
|
|
{children}
|
|
</div>
|
|
) : (
|
|
<button
|
|
className="btn btn-ghost-secondary btn-icon btn-sm"
|
|
onClick={handleClick}
|
|
aria-label="Открыть меню"
|
|
title="Показать действия"
|
|
>
|
|
<IconDotsVertical size={18} />
|
|
</button>
|
|
)}
|
|
|
|
{isOpen && (
|
|
<>
|
|
<div
|
|
ref={menuRef}
|
|
className="dropdown-menu show"
|
|
style={{
|
|
position: 'fixed',
|
|
left: useContextPosition ? `${position.x}px` : align === 'right' ? 'auto' : `${position.x}px`,
|
|
right: useContextPosition ? 'auto' : align === 'right' ? `${window.innerWidth - position.x}px` : 'auto',
|
|
top: `${position.y}px`,
|
|
zIndex: 9999,
|
|
minWidth: '180px'
|
|
}}
|
|
>
|
|
{items.map((item, index) => {
|
|
if (item.divider) {
|
|
return <div key={index} className="dropdown-divider"></div>
|
|
}
|
|
|
|
const Icon = item.icon
|
|
const variantClass = item.variant === 'danger' ? 'text-danger' : ''
|
|
|
|
return (
|
|
<button
|
|
key={index}
|
|
className={`dropdown-item ${variantClass}`}
|
|
onClick={() => handleItemClick(item.onClick)}
|
|
disabled={item.disabled}
|
|
>
|
|
{Icon && <Icon size={18} className="me-2" />}
|
|
{item.label}
|
|
{item.shortcut && (
|
|
<kbd className="ms-auto">{item.shortcut}</kbd>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Backdrop для мобильных */}
|
|
<div
|
|
className="dropdown-backdrop"
|
|
style={{
|
|
position: 'fixed',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
zIndex: 9998
|
|
}}
|
|
onClick={() => setIsOpen(false)}
|
|
></div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default ContextMenu
|
|
|