前端黑科技:React项目中鼠标右键菜单功能的2种实现方式

3,878 阅读10分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

前言

前端黑科技:按住Ctrl键或Shift键多选Table表格和Tree树节点的实现过程这篇文章中,介绍了在项目中如何使用Ctrl键或Shift键去对表格的行或树节点进行多选操作。那这次要聊的主题依然是该一个项目需求中的另一个黑科技,之所以称其为黑科技,是因为在网上还没有遇见像我们这样复杂的需求交互,所以将其分享出来给你们提供一个思路参考,避免后续工作中遇见同样的需求时无所适从。

好,这次要聊的前端黑科技就是如何实现一个鼠标右键菜单功能,并且可以点击操作实现对应的菜单功能

背景

和之前的流程一样,还是来说一下需求背景,这样容易理解文章将要说的主题。其实背景就是,对CAD原有功能的扩展,上一篇前端黑科技中就介绍了要对CAD的参照管理功能进行重写,重写就是需要原有功能需要保留并在上面进行扩展,这里要说的右键菜单也是如此。CAD自有的右键功能太过于基础,没有批量操作相关的菜单。如下图所示:

动画1.gif

而需求需要做到的效果有如下3种情况的右键菜单,如下:

image.png

image.png

image.png

所以需求就是上面展示的图示,也是最后完成的效果图。如果你有相关类似的需要处理鼠标右键的需求,你可以参考我下面的实现思路。为了尽可能满足匹配你将遇到的需求,下面将列出2种方式供你选择。

实现过程

下面就用Dropdown下拉菜单和react-contexify插件这2种实现方式去讲解。采用Dropdown下拉菜单的方式呢就是自己去自定义实现,而使用react-contexify插件的方式呢就是多掌握一种实现方式,作为Dropdown下拉菜单翻车时的备选方案。

为了方便理解,Dropdown下拉菜单实现的方式是用在树结构类型的数据上的,而使用react-contexify插件实现的场景则是用在Table表格类型上的。

注:技术栈依然是React!!!

一、Dropdown下拉菜单实现右键菜单

Dropdown下拉菜单实现右键菜单的方式在antd示例中是有示例的ant.design/components/…, 就照着它的示例代码就可以实现。

image.png

看上面的代码实现可以知道,需要将Dropdown套在一个触发它显示隐藏的节点上,然后为它添加右键事件和右键菜单menu即可。

好了,弄清楚使用方法,那就照葫芦画瓢来实现我们的需求。

首先需要为每个树节点套一个Dropdown,这里可能有疑问就是为什么要以每个树节点作为触发条件而不是让整个树作为触发条件呢?原因有二:其一,如果触发条件为整棵树的话,点击树节点的空白区域也能出现右键菜单,这是不符合需求逻辑的;其二,那就是要去利用每个树节点的信息判断相应的树节点该出现什么样的右键菜单这一需求。所以就需要为每个树节点套上Dropdown

怎么为每个树节点套上Dropdown?那就是之前说到的,需要自定义树节点,我的处理方式是将树结构的数据通过递归去添加。如下:

/**
 * 自定义树节点
 * @param list
 */
const loop = (list: any[]): any[] => list.map((v: any) => {
    let titleNode = (
        <Dropdown overlay={renderTreeRightMenu()} trigger={['contextMenu']}>
            <div className="f12 tree-title">{v.name}</div>
        </Dropdown>
    )
    return {
        title: titleNode,
        key: v.key,
        dataRef: v,
        children: ObjTest.isNotEmptyArray(v.xref) ? loop(v.xref) : [],
    }
});

树组件中就像下图所示去使用:

image.png

这里需要说一下的是,我们判断右键出现什么样的菜单是使用Tree树本身提供的右键事件,这样就可以拿到每个树节点的信息去做判断,从而渲染属于每个节点对应的右键菜单。如下:

/**
 * 树节点右键
 * @param event
 * @param node
 */
const treeRightClickHandle = ({event, node}) => {
    event.preventDefault();
    const {dataRef} = node || {};
    const {itemType, operFlag, xrefStatus, key} = dataRef || {};
    setItemType(isMode(itemType)); // 显示右键菜单类型判断
    setNested(operFlag < 1 ? true : false); // 右键菜单项是否禁用标识
    setIsDisabled(xrefStatus == 4 ? true : false); // 某些指定的右键菜单项是否禁用标识
    setSelectTreeNode(dataRef);
    setSelectKeys([key]);
}

然后就是需要写你需要的菜单节点了,这里我是写了3个菜单,方便理解,实际项目中你可以根据条件来判断菜单内容节点。如下:

/**
 * 本图/专业/多选节点右键菜单
 * @param type
 */
const rightMenuLevel1 = (type: number) => {
    return (
        <Menu className="right-menu">
            <Menu.Item onClick={() => rightMenuHandle(type, 1)}>全部卸载</Menu.Item>
            <Menu.Item onClick={() => rightMenuHandle(type, 2)}>全部重载</Menu.Item>
            <Menu.Item onClick={() => rightMenuHandle(type, 3)}>全部拆离</Menu.Item>
            <Menu.Item onClick={() => rightMenuHandle(type, 4)}>全部绑定</Menu.Item>
            <Menu.Divider />
            <Menu.Item onClick={() => rightMenuHandle(type, 5)}>全部改为附着</Menu.Item>
            <Menu.Item onClick={() => rightMenuHandle(type, 6)}>全部改为覆盖</Menu.Item>
            <Menu.Divider />
            <Menu.Item onClick={() => rightMenuHandle(type, 7)}>全部设为绝对路径</Menu.Item>
            <Menu.Item onClick={() => rightMenuHandle(type, 8)}>全部设为相对路径</Menu.Item>
            <Menu.Item onClick={() => rightMenuHandle(type, 9)}>全部删除路径</Menu.Item>
        </Menu>
    )
}


/**
 * 一级参展节点右键菜单
 */
const rightMenuLevel2 = () => {
    return (
        <Menu>
            <Menu.Item disabled={isDisabled} onClick={openDwgHandle}>打开</Menu.Item>
            <Menu.Item onClick={quickLookDwgHandle}>快速看图</Menu.Item>
            <Menu.Item onClick={openFolderHandle}>打开所在文件夹</Menu.Item>
            <Menu.Divider />
            <Menu.Item onClick={rightMenuStickyHandle}>附着</Menu.Item>
            <Menu.Item onClick={() => rightMenuHandle(3, 1)}>卸载</Menu.Item>
            <Menu.Item onClick={() => rightMenuHandle(3, 2)}>重载</Menu.Item>
            <Menu.Item disabled={nested} onClick={() => rightMenuHandle(3, 3)}>拆离</Menu.Item>
            <Menu.Item disabled={nested || isDisabled} onClick={() => rightMenuHandle(3, 4)}>绑定</Menu.Item>
            <Menu.Divider />
            <Menu.Item disabled={nested} onClick={() => rightMenuHandle(3, 5)}>改为附着</Menu.Item>
            <Menu.Item disabled={nested} onClick={() => rightMenuHandle(3, 6)}>改为覆盖</Menu.Item>
            <Menu.Divider />
            <Menu.Item disabled={nested} onClick={() => rightMenuHandle(3, 7)}>设为绝对路径</Menu.Item>
            <Menu.Item disabled={nested || !curDoc.realPath} onClick={() => rightMenuHandle(3, 8)}>设为相对路径</Menu.Item>
            <Menu.Item disabled={nested} onClick={() => rightMenuHandle(3, 9)}>删除路径</Menu.Item>
        </Menu>
    )
}


/**
 * 子图纸右键菜单
 */
const rightMenuLevel3 = () => {
    return (
        <Menu>
            <Menu.Item disabled={isDisabled} onClick={openDwgHandle}>打开</Menu.Item>
            <Menu.Item onClick={quickLookDwgHandle}>快速看图</Menu.Item>
            <Menu.Item onClick={openFolderHandle}>打开所在文件夹</Menu.Item>
        </Menu>
    )
}

这里要注意2点:

  1. 如果你想让右键不出现,需要返回一个空标签(<></>),如果返回null会报错!!!
  2. 如果你使用的antd的版本低于4.20.0是可以使用Menu.Item编写菜单项的;但是如果你的版本>=4.20.0则需要使用items集合方式去编写菜单项。

image.png

到这里,树结构的鼠标右键菜单的实现过程就如上所述。期间有些逻辑代码没有完整贴出,省略的都是些不相关的代码判断,最核心的代码还是上述代码,只要你耐心的看完你就一定能搞得出来和我一样的效果。如果你照着上面搞出来的效果没有我的理想那一定是样式没有搞对,所以我还是把样式代码也贴出来供你参考。如下:

.ant-dropdown-menu-item {
  padding: 0 8px;
  font-size: 12px;
  line-height: 22px;
}

.ant-dropdown-menu-item:hover {
  background: #4393e6;
  color: #fff;
}

最后,一起再来看看Dropdown下拉菜单实现右键菜单的最终效果。如下:

动画2.gif

二、react-contexify插件实现右键菜单

上面Dropdown下拉菜单方式的效果还是挺理想的,下面就来看看别人的插件又是如何使用的。

react-contexify这个插件也是得到antd认可的,在antd社区精选组件中可以看到右键菜单中react-contexify就占有一席之地。如下:

image.png

所以下面,我们就用它来实现产品需求。

首先通过命令安装,如下:

yarn add react-contexify 或者 npm install --save react-contexify

然后在项目中使用:

import { Menu as RightMenu, Item, Separator, useContextMenu} from 'react-contexify';
import 'react-contexify/dist/ReactContexify.css';

这里可以看到,我们对Menu进行了重命名,那是因为在Dropdown下拉菜单实现方式中使用了antdMenu组件生成菜单项,所以同一项目中使用需要对其重命名一下。

然后按照它github上的示例代码,我们需要定义唯一菜单ID常量用于区分是项目中的哪一个菜单。所以项目中定义一个Menu1,如下:

const MENU_ID = 'Menu1';

还有就是react-contexify需要在组件中定义一个useContextMenu钩子,用于触发菜单的显示。如下:

// table模式菜单
const { show } = useContextMenu({
    id: MENU_ID,
});

那到这里前期的工作算是准备完成了,下面就要真刀真枪的写逻辑代码了→

上一篇前端黑科技文章中有提到Table表格是自己使用div组装形成的,所以需要为每一行上的div增加一个右键事件。如下:

image.png

如果你使用的是antdTable组件,可以通过onRow中的onContentMenu事件进行设置。如下:

image.png

然后就来看看右键事件的逻辑代码,如下:

/**
 * table模式右键
 * @param event
 * @param record
 */
const handleContextMenu = (event: any, record: any) => {
    event.preventDefault();
    const {itemType, operFlag, xrefStatus} = record || {};
    setItemType(isMode(itemType)); // 显示右键菜单类型判断
    setNested(operFlag < 1 ? true : false); // 右键菜单项是否禁用标识
    setIsDisabled(xrefStatus == 4 ? true : false); // 某些指定的右键菜单项是否禁用标识
    setSelectTreeNode(record);
    show(event, {
        props: {
            key: 'value'
        }
    })
}

Dropdown下拉菜单中没啥区别,唯一区别的就是这里需要触发插件定义的钩子函数,才能弹出右键菜单。至于传不传参数需要根据自己的业务来抉择,我的项目中就不需要传递参数,而是使用state来传递的。

细心的朋友可以发现,按照背景需求中提到的,是会有3种类型的菜单,但是这里我只设置了一个菜单ID,那其他菜单是该如何触发的呢?这就是下面要接着说的,其实就只需要设置一个菜单ID即可,至于出现是啥类型的菜单是根据条件进行判断实现的。如下:

{/*表格时的右键菜单*/}
<RightMenu id={MENU_ID}>
    {renderTableRightMenu()}
</RightMenu>

将上述代码放置于组件的最后或封成公用组件再引入使用。这里我是直接放在组件的代码后的,如下:

image.png

然后在来看renderTableRightMenu()这个方法里面的代码逻辑。如下:

/**
 * 渲染表格模式的右键菜单
 */
const renderTableRightMenu = (): React.ReactNode => {
    if (itemType === 0) {
        return null;
    }
    if (itemType === 1 || selectItems.length > 1) {
        return (
            <>
                <Item onClick={() => rightMenuHandle(2, 1)}>全部卸载</Item>
                <Item onClick={() => rightMenuHandle(2, 2)}>全部重载</Item>
                <Item onClick={() => rightMenuHandle(2, 3)}>全部拆离</Item>
                <Item onClick={() => rightMenuHandle(2, 4)}>全部绑定</Item>
                <Separator />
                <Item onClick={() => rightMenuHandle(2, 5)}>全部改为附着</Item>
                <Item onClick={() => rightMenuHandle(2, 6)}>全部改为覆盖</Item>
                <Separator />
                <Item onClick={() => rightMenuHandle(2, 7)}>全部设为绝对路径</Item>
                <Item onClick={() => rightMenuHandle(2, 8)}>全部设为相对路径</Item>
                <Item onClick={() => rightMenuHandle(2, 9)}>全部删除路径</Item>
            </>
        )
    }
    if (itemType === 3) {
        return (
            <>
                <Item disabled={isDisabled} onClick={openDwgHandle}>打开</Item>
                <Item onClick={quickLookDwgHandle}>快速看图</Item>
                <Item onClick={openFolderHandle}>打开所在文件夹</Item>
            </>
        )

    }
    return (
        <>
            <Item disabled={isDisabled} onClick={openDwgHandle}>打开</Item>
            <Item onClick={quickLookDwgHandle}>快速看图</Item>
            <Item onClick={openFolderHandle}>打开所在文件夹</Item>
            <Separator />
            <Item onClick={rightMenuStickyHandle}>附着</Item>
            <Item onClick={() => rightMenuHandle(3, 1)}>卸载</Item>
            <Item onClick={() => rightMenuHandle(3, 2)}>重载</Item>
            <Item disabled={nested} onClick={() => rightMenuHandle(3, 3)}>拆离</Item>
            <Item disabled={nested || isDisabled} onClick={() => rightMenuHandle(3, 4)}>绑定</Item>
            <Separator />
            <Item disabled={nested} onClick={() => rightMenuHandle(3, 5)}>改为附着</Item>
            <Item disabled={nested} onClick={() => rightMenuHandle(3, 6)}>改为覆盖</Item>
            <Separator />
            <Item disabled={nested} onClick={() => rightMenuHandle(3, 7)}>设为绝对路径</Item>
            <Item disabled={nested || !curDoc.realPath} onClick={() => rightMenuHandle(3, 8)}>设为相对路径</Item>
            <Item disabled={nested} onClick={() => rightMenuHandle(3, 9)}>删除路径</Item>
        </>
    );
}

从上面代码可以发现,使用react-contexify的菜单项的标签和使用Dropdown下拉菜单中的有点区别:第一个区别是react-contexify使用的是ItemDropdown下拉菜单使用的则是Menu.Item;再一个区别就是如果右键触发不想出现菜单可以使用null进行返回,而Dropdown下拉菜单则是需要返回空标签(<></>)才可以,否则会报错。两者的相同点就是在禁用菜单项的属性上都是可以使用disabled进行禁用的。

到这里,react-contexify插件实现右键菜单功能也介绍完了。还是那句话,只要你耐心看完本篇文章,你也一定能像我一样实现右键菜单功能需求的。如果你要搞的效果和我一样,那我还是只有把样式代码给你贴出来供你参考一下,避免到时候你来喷我说效果不一致。样式代码如下:

.react-contexify {
  min-width: 90px;
  .react-contexify__item {
    .react-contexify__item__content {
      padding: 2px 8px;
      font-size: 12px;
    }
  }
}

同样,最后还是一起来看看react-contexify插件实现的最终效果。如下:

动画3.gif

到此,前端中实现右键菜单功能的2种方式就介绍完了。这要我说这功能就是前端的黑科技,因为不常用,很少有产品这样去思考产品,但是有人这样去思考了,那就称它为前端黑科技也无妨。幸好我们最后是把需求给完美解决了,所以真的要逼自己一把,不然你根本不知道自己蕴藏多大能量呢。

最后,看文至此的各位老板,如果实际开发中有什么问题欢迎前来勾兑勾兑,在下定当知无不言言无不尽🙏。

往期精彩文章

后语

伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走呗^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。

src=http___p6.itc.cn_q_70_images03_20210104_70f8545500034a5bae5f1695a7ce3da0.jpeg&refer=http___p6.itc.webp