文章目录

  • ​​功能​​
  • ​​演示​​
  • ​​逻辑代码​​
  • ​​业务代码​​
功能
  • 覆盖右键菜单
  • 模拟原生浏览器右键菜单
  • 默认出现在右侧
  • 右侧显示不全则出现在左侧
  • 一级菜单能完整显示二级菜单不能二级小说出现在另一侧 默认出现在右侧
演示

react多级右键菜单_javascript

逻辑代码

监听右键菜单 一级菜单检测

document.addEventListener("contextmenu", (e: any) => {
e.preventDefault();
let x = e.pageX + 2;
let y = e.pageY;
//wrap 是菜单容器ref
if (e.pageX + wrap.current?.offsetWidth > document.body.offsetWidth)
x = e.pageX - wrap.current!.offsetWidth;
if (e.pageY + wrap.current?.offsetHeight > document.body.offsetHeight)
y = document.body.offsetHeight - wrap.current!.offsetHeight - 10;
setMenuLayout({ x, y }); // 设置新的布局 有展开菜单的组件会自动useEffect校验位置
setIsShowMenu(true);//显示菜单
});

根据菜单当前的位置和即将出现的option宽高判断能否完成显示option

/**
* 根据菜单当前的位置和即将出现的option宽高判断能否完成显示option
*/
const check = useCallback((itemRef: React.RefObject<HTMLDivElement>) => {
const item = itemRef.current;
if (item && wrap.current) {
const { bottom, right } = wrap.current.getBoundingClientRect();
if (document.body.offsetHeight < bottom + item.offsetHeight) {
item.style["top"] = "auto";
item.style["bottom"] = "0";
} else {
item.style["top"] = "0";
item.style["bottom"] = "auto";
}
if (document.body.offsetWidth < right + item.offsetWidth) {
item.style["left"] = "auto";
item.style["right"] = "100%";
} else {
item.style["left"] = "100%";
item.style["right"] = "auto";
}
}
}, []);
业务代码
/*
* @Author: hongbin
* @Date: 2022-03-19 21:53:41
* @LastEditors: hongbin
* @LastEditTime: 2022-03-20 21:51:24
* @Description:自定义鼠标右键菜单
*/
import {
createRef,
FC,
memo,
ReactElement,
useCallback,
useImperativeHandle,
useRef,
useState,
} from "react";
import styled from "styled-components";
import { source } from "../../container/LoadingScreen/DataSource";
import { controls } from "../../container/Map/utils";
import useMount from "../../hook/useMount";
import { fadeIn } from "../../styled";
import { chartBG } from "../../styled/GlobalStyle";
import { BGMusicRef } from "../BackGroundMusic";
import MenuItem from "./MenuItem";

interface MenuLayout {
x: number;
y: number;
}

export const CustomMenuRef = createRef<{
show: (e: { pageX: number; pageY: number }) => void;
}>();

interface IProps {}

const CustomMenu: FC<IProps> = (): ReactElement => {
const [isShowMenu, setIsShowMenu] = useState(false); // 是否展示菜单
const [menuLayout, setMenuLayout] = useState<MenuLayout>({ x: 0, y: 0 }); // 菜单左上角位置
const wrap = useRef<HTMLDivElement>(null);

useMount(() => {
document.addEventListener("contextmenu", (e: any) => {
if (e.target.getAttribute("canvas") === "1") return;
e.preventDefault();
let x = e.pageX + 2;
let y = e.pageY;
if (e.pageX + wrap.current?.offsetWidth > document.body.offsetWidth)
x = e.pageX - wrap.current!.offsetWidth;
if (e.pageY + wrap.current?.offsetHeight > document.body.offsetHeight)
y = document.body.offsetHeight - wrap.current!.offsetHeight - 10;
setMenuLayout({ x, y });
setIsShowMenu(true);
});
});

useImperativeHandle(
CustomMenuRef,
() => ({
show: (e: { pageX: number; pageY: number }) => {
//按钮右侧不会显示不全不必判断位置是否合理
setIsShowMenu(true);
setMenuLayout({ x: e.pageX, y: e.pageY });
},
}),
[]
);

/**
* 根据菜单当前的位置和即将出现的option宽高判断能否完成显示option
*/
const check = useCallback((itemRef: React.RefObject<HTMLDivElement>) => {
const item = itemRef.current;
if (item && wrap.current) {
const { bottom, right } = wrap.current.getBoundingClientRect();
if (document.body.offsetHeight < bottom + item.offsetHeight) {
item.style["top"] = "auto";
item.style["bottom"] = "0";
} else {
item.style["top"] = "0";
item.style["bottom"] = "auto";
}
if (document.body.offsetWidth < right + item.offsetWidth) {
item.style["left"] = "auto";
item.style["right"] = "100%";
} else {
item.style["left"] = "100%";
item.style["right"] = "auto";
}
}
}, []);

return (
<Container
style={{ visibility: isShowMenu ? "visible" : "hidden" }}
onClick={(e: any) => {
if (
e.target.nodeName === "SECTION" ||
e.target.getAttribute("name") === "1"
)
setIsShowMenu(false);
}}
>
<Menu
ref={wrap}
layout={menuLayout}
onMouseLeave={() => setIsShowMenu(false)}
>
<SubTitle>快捷菜单</SubTitle>
{BGMusicRef.current?.audio ? (
<MenuItem
text={
BGMusicRef.current.audio.paused ? "开启背景音乐" : "关闭背景音乐"
}
onClick={BGMusicRef.current.toggle}
/>
) : null}
<MenuItem
text={controls.autoRotate ? "关闭镜头旋转" : "开启镜头旋转"}
onClick={() => {
controls.autoRotate = !controls.autoRotate;
}}
/>
<MenuItem
text={"数据来源"}
option={source.map(({ name, href }) => ({
title: name,
onClick: () => {
window.open(href, "_blank");
},
}))}
renderCheck={menuLayout}
check={check}
/>
</Menu>
</Container>
);
};

export default memo(CustomMenu);

const Container = styled.section`
position: fixed;
z-index: 999999;
/**
* 多一些宽高 不会在边界处自动使用布局 导致width,height获取不正确
*/
width: 130vw;
height: 130vh;
animation: ${fadeIn} 0.1s linear;
`;

const Menu = styled.div<{ layout: MenuLayout }>`
padding: 1vh;
${chartBG};
position: absolute;
border-radius: 1vh;
font-size: 1vw;
color: #205120;
box-shadow: 5px 3px 11px 5px #364637bd;

${({ layout: { x, y } }) => `top: ${y}px;left:${x}px`};
`;

const SubTitle = styled.span`
color: #023802;
font-size: 0.9em;
font-weight: bold;
`;
/*
* @Author: hongbin
* @Date: 2022-03-19 22:04:32
* @LastEditors: hongbin
* @LastEditTime: 2022-03-24 21:22:48
* @Description:MenuItem 菜单项
*/
import { FC, ReactElement, useEffect, useRef, useState } from "react";
import styled, { css } from "styled-components";
import { fadeIn } from "../../styled";

interface IProps {
text: string;
onClick?: () => void;
option?: { title: string; onClick: () => void }[];
renderCheck?: any;
check?: (ref: React.RefObject<HTMLDivElement>) => void;
}

const MenuItem: FC<IProps> = ({
text,
onClick,
option,
renderCheck,
check,
}): ReactElement => {
const [showOption, setShowOption] = useState(false);
const optionRef = useRef<HTMLDivElement>(null);

/**
* 布局发生变化时就获取option应该出现的位置 现获取位置会产生闪烁 因为和上次的css可能不同
*/
useEffect(() => {
if (renderCheck) {
check && check(optionRef);
}
}, [renderCheck, check]);

return (
<Container
{...(option?.length
? {
onMouseLeave: () => setShowOption(false),
onMouseEnter: () => setShowOption(true),
}
: {})}
hoverActive={!!onClick}
onClick={e => {
option?.length && e.stopPropagation();
onClick && onClick();
}}
>
{/* @ts-ignore */}
<p name='1'>{text}</p>
{option?.length ? (
<>
{ArrowIcon}
<OptionBox
ref={optionRef}
style={{ visibility: showOption ? "visible" : "hidden" }}
>
{option.map(({ title, onClick }) => (
<MenuItem key={title} text={title} onClick={onClick} />
))}
</OptionBox>
</>
) : null}
</Container>
);
};

export default MenuItem;

const Container = styled.div.attrs({ name: "1" })<{ hoverActive?: boolean }>`
padding: 0.8vh 1vw;
background-color: #83c984;
border-radius: 0.7vh;
margin: 1vh 0;
transition: 0.3s ease;
transition-property: background-color, color, font-weight;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
svg {
width: 0.8em;
height: 0.8em;
}
:hover {
p {
transition: color 0.2s ease, font-weight 0.2s ease;
font-weight: bold;
color: #073f08;
}
background-color: #3e843e;

path {
fill: #112e11;
}
${({ hoverActive }) =>
!hoverActive &&
css`
background-color: #4da14d;
`}
}
`;

const OptionBox = styled.div`
position: absolute;
padding: 0.5vh 0.8vh 0.5vh 0.8vh;
background: #83c984c9;
font-size: 0.8em;
border-radius: inherit;
/* top: 0;
left: 100%; */
box-shadow: 5px 3px 11px 5px #364637bd;
animation: ${fadeIn} 0.2s 0.1s linear;
& > div {
margin: 0.5vh 0;
}
p {
display: block;
white-space: nowrap;
padding: 0.2vh;
font-weight: normal;
:hover {
font-weight: bold;
}
}
/* :hover {
background: inherit;
} */
`;

const ArrowIcon = (
<svg
viewBox='0 0 1024 1024'
version='1.1'
xmlns='http://www.w3.org/2000/svg'
p-id='2852'
width='32'
height='32'
>
<path
d='M761.056 532.128c0.512-0.992 1.344-1.824 1.792-2.848 8.8-18.304 5.92-40.704-9.664-55.424L399.936 139.744c-19.264-18.208-49.632-17.344-67.872 1.888-18.208 19.264-17.376 49.632 1.888 67.872l316.96 299.84-315.712 304.288c-19.072 18.4-19.648 48.768-1.248 67.872 9.408 9.792 21.984 14.688 34.56 14.688 12 0 24-4.48 33.312-13.44l350.048-337.376c0.672-0.672 0.928-1.6 1.6-2.304 0.512-0.48 1.056-0.832 1.568-1.344C757.76 538.88 759.2 535.392 761.056 532.128z'
p-id='2853'
fill='#205120'
></path>
</svg>
);