mirror of
https://github.com/GiriNeko/YesPlayMusic.git
synced 2025-12-16 13:17:46 +00:00
173 lines
4.1 KiB
TypeScript
173 lines
4.1 KiB
TypeScript
import { css, cx } from '@emotion/css'
|
|
import {
|
|
ForwardedRef,
|
|
forwardRef,
|
|
useLayoutEffect,
|
|
useRef,
|
|
useState,
|
|
} from 'react'
|
|
import { motion } from 'framer-motion'
|
|
import MenuItem, { ContextMenuItem } from './MenuItem'
|
|
|
|
interface PanelProps {
|
|
position: {
|
|
x: number
|
|
y: number
|
|
transformOrigin?: `origin-${'top' | 'bottom'}-${'left' | 'right'}`
|
|
}
|
|
items: ContextMenuItem[]
|
|
onClose: (e: MouseEvent) => void
|
|
forMeasure?: boolean
|
|
classNames?: string
|
|
isSubmenu?: boolean
|
|
}
|
|
|
|
interface SubmenuProps {
|
|
itemRect: DOMRect
|
|
index: number
|
|
}
|
|
|
|
const MenuPanel = forwardRef(
|
|
(
|
|
{ position, items, onClose, forMeasure, classNames, isSubmenu }: PanelProps,
|
|
ref: ForwardedRef<HTMLDivElement>
|
|
) => {
|
|
const [submenuProps, setSubmenuProps] = useState<SubmenuProps | null>(null)
|
|
|
|
return (
|
|
// Container (to add padding for submenus)
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: forMeasure ? 1 : 0.96 }}
|
|
animate={{
|
|
opacity: 1,
|
|
scale: 1,
|
|
transition: {
|
|
duration: 0.1,
|
|
},
|
|
}}
|
|
exit={{ opacity: 0, scale: 0.96 }}
|
|
transition={{ duration: 0.2 }}
|
|
ref={ref}
|
|
className={cx(
|
|
'fixed',
|
|
position.transformOrigin || 'origin-top-left',
|
|
isSubmenu ? 'submenu z-20 px-1' : 'z-10'
|
|
)}
|
|
style={{ left: position.x, top: position.y }}
|
|
>
|
|
{/* The real panel */}
|
|
<div
|
|
className={cx(
|
|
'rounded-12 border border-white/[.06] bg-gray-900/95 p-px py-2.5 shadow-xl outline outline-1 outline-black backdrop-blur-3xl',
|
|
css`
|
|
min-width: 200px;
|
|
`,
|
|
classNames
|
|
)}
|
|
>
|
|
{items.map((item, index) => (
|
|
<MenuItem
|
|
key={index}
|
|
index={index}
|
|
item={item}
|
|
onClose={onClose}
|
|
onSubmenuOpen={(props: SubmenuProps) => setSubmenuProps(props)}
|
|
onSubmenuClose={() => setSubmenuProps(null)}
|
|
className={isSubmenu ? 'submenu' : ''}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Submenu */}
|
|
<SubMenu
|
|
items={
|
|
submenuProps?.index ? items[submenuProps?.index]?.items : undefined
|
|
}
|
|
itemRect={submenuProps?.itemRect}
|
|
onClose={onClose}
|
|
/>
|
|
</motion.div>
|
|
)
|
|
}
|
|
)
|
|
MenuPanel.displayName = 'Menu'
|
|
|
|
export default MenuPanel
|
|
|
|
const SubMenu = ({
|
|
items,
|
|
itemRect,
|
|
onClose,
|
|
}: {
|
|
items?: ContextMenuItem[]
|
|
itemRect?: DOMRect
|
|
onClose: (e: MouseEvent) => void
|
|
}) => {
|
|
const submenuRef = useRef<HTMLDivElement>(null)
|
|
|
|
const [position, setPosition] = useState<{
|
|
x: number
|
|
y: number
|
|
transformOrigin: `origin-${'top' | 'bottom'}-${'left' | 'right'}`
|
|
}>()
|
|
useLayoutEffect(() => {
|
|
if (!itemRect || !submenuRef.current) {
|
|
return
|
|
}
|
|
const item = itemRect
|
|
const submenu = submenuRef.current.getBoundingClientRect()
|
|
|
|
const isRightSide = item.x + item.width + submenu.width <= window.innerWidth
|
|
const x = isRightSide ? item.x + item.width : item.x - submenu.width
|
|
|
|
const isTopSide = item.y - 10 + submenu.height <= window.innerHeight
|
|
const y = isTopSide
|
|
? item.y - 10
|
|
: item.y + item.height + 10 - submenu.height
|
|
|
|
const transformOriginTable = {
|
|
top: {
|
|
right: 'origin-top-left',
|
|
left: 'origin-top-right',
|
|
},
|
|
bottom: {
|
|
right: 'origin-bottom-left',
|
|
left: 'origin-bottom-right',
|
|
},
|
|
} as const
|
|
|
|
setPosition({
|
|
x,
|
|
y,
|
|
transformOrigin:
|
|
transformOriginTable[isTopSide ? 'top' : 'bottom'][
|
|
isRightSide ? 'right' : 'left'
|
|
],
|
|
})
|
|
}, [itemRect])
|
|
|
|
if (!items || !itemRect) {
|
|
return <></>
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<MenuPanel
|
|
position={{ x: 99999, y: 99999 }}
|
|
items={items || []}
|
|
ref={submenuRef}
|
|
onClose={() => {
|
|
// Do nothing
|
|
}}
|
|
forMeasure={true}
|
|
isSubmenu={true}
|
|
/>
|
|
<MenuPanel
|
|
position={position || { x: 99999, y: 99999 }}
|
|
items={items || []}
|
|
onClose={onClose}
|
|
isSubmenu={true}
|
|
/>
|
|
</>
|
|
)
|
|
}
|