vue3+ts开发vue3-context-menu插件

2,881 阅读2分钟

描述

右键菜单因该还是是比较常见的功能,一般都是通过触发事件控制一个菜单的显示隐藏,尤其是在可视化编辑器中更为突出,而且vue3现在社区也不完善,所以就想用vue3+ts写一款右键菜单组件,支持vue3,ts

技术栈

  • vue3
  • typescript
  • lodash
  • vue3(hooks)

正式开始

1. 新建types.ts文件,该文件用作vue3-context-menu的类型声明。

export type Position = {
  x: number
  y: number
}

export type ContextMenuItem = {
  label: string
  icon?: string
  disabled?: boolean
  handler?: (...rest: any[]) => void
  children?: ContextMenuItem[]
}

export type ContextMenuProps = {
  menuClass?: string
  menus: ContextMenuItem[]
  position?: Position
  width?: number
}

export type ItemContentProps = {
  item: ContextMenuItem
  handler: (...rest: any[]) => void
}

export type CreateContextOptions = ContextMenuProps & {
  event: MouseEvent
}

2. 新建index.tsx文件,该文件用作vue3-context-menu的视图层。

全局参数
import { defineComponent, FunctionalComponent, PropType, Transition } from 'vue'
import { Menu } from 'ant-design-vue'
import { ItemContentProps, Position, ContextMenuItem } from './types'
import SvgIcon from '../svg-icon/index.vue'
const Item: FunctionalComponent<ItemContentProps> = props => {
  const { item, handler } = props
  return (
    <div onClick={e => handler(item, e)}>
      {!!item.icon && <SvgIcon class="mr8" name={item.icon} />}
      <span>{item.label}</span>
    </div>
  )
}

导出模板组件,该组件未创建挂载
export default defineComponent({
  name: 'ContextMenu',
  inheritAttrs: false,
  emits: ['update:visible'],
  props: {
    width: {
      type: Number,
      default: 160
    },
    menuClass: {
      type: String,
      default: ''
    },
    position: {
      type: Object as PropType<Position>,
      default: () => ({ x: 0, y: 0 })
    },
    menus: {
      type: Array as PropType<ContextMenuItem[]>,
      default: () => []
    },
    visible: {
      type: Boolean,
      default: false
    }
  },
  mounted() {
    document.body.addEventListener('click', this.hide)
  },
  unmounted() {
    document.body.removeEventListener('click', this.hide)
  },
  methods: {
    hide() {
      this.$emit('update:visible', false)
    },
    handleAction(item: ContextMenuItem, e: MouseEvent) {
      const { handler, disabled } = item
      if (disabled) {
        return
      }
      e.stopPropagation()
      e.preventDefault()
      handler && handler(item, e)
      this.hide()
    }
  },
  render() {
    const self = this // eslint-disable-line
    const { visible } = this

    function renderMenuItem(menus: ContextMenuItem[]) {
      return menus.map(item => {
        const { disabled, label, children } = item

        if (!children || !children.length) {
          return (
            <Menu.Item disabled={disabled} class="context-menu-item" key={label}>
              <Item item={item} handler={self.handleAction} />
            </Menu.Item>
          )
        }
        return (
          <Menu.SubMenu key={label} disabled={disabled} popupClassName="context-menu-popup">
            {{
              // slots
              title: () => <Item item={item} handler={self.handleAction} />,
              default: () => renderMenuItem(children)
            }}
          </Menu.SubMenu>
        )
      })
    }
    return (
      <Transition name="com-fade-in" appear>
        {visible && (
          <div>
            <Menu
              class={`context-menu ${this.menuClass}`}
              mode="vertical"
              style={{
                width: this.width + 'px',
                top: this.position.y + 'px',
                left: this.position.x + 'px'
              }}
            >
              {renderMenuItem(this.menus)}
            </Menu>
          </div>
        )}
      </Transition>
    )
  }
})

2. 新建create-context-menu.ts文件,该文件用作vue3-context-menu的逻辑层,采用vue中的hooks思想进行编写。

全局参数
import { h, ComponentPublicInstance, ref, getCurrentInstance } from 'vue'

export type CreateContextMenuProps = ContextMenuProps & {
  event: MouseEvent
}

export type ContextMenuInstance = ComponentPublicInstance<
  {},
  {
    open: (opts: ContextMenuProps) => void
    close: () => void
  }
>

const defaultProps: ContextMenuProps = {
  width: 160,
  menuClass: '',
  position: { x: 0, y: 0 },
  menus: []
}

let ins: ContextMenuInstance | null = null
生成右键信息和模板
function createInstance() {
  const comp = {
    setup() {
      const visible = ref(false)

      let attrs: Record<string, unknown> = {}
      const open = (opts: ContextMenuProps) => {
        visible.value = true
        attrs = opts
      }
     
      const close = () => {
        visible.value = false
      }

      const toggle = (val: boolean) => {
        visible.value = val
      }

      const render = () => {
        attrs = {
          ...attrs,
          visible: visible.value,
          'onUpdate:visible': toggle
        }

        return h(ContextMenu, attrs)
      }

      // 重新渲染函数
      ;(getCurrentInstance() as any).render = render

      return {
        open,
        close
      }
    }
  }
  // mountComponent方法省略,就是一个`vue`中的`createApp`方法,用来挂载组件的
  const { instance } = mountComponent(comp, 'context-menu-wrapper-root')

  return instance
}
获取createInstance创建的信息
function getInstance() {
  if (ins) {
    return ins
  }
  ins = createInstance() as ContextMenuInstance
  return ins
}
将创建的右键组件信息,以一个对象方式导出,动态创建组件和删除组件,类似于vue2中的 vue.extend(),这样方便我们在操作层创建组件,解决了在视图层直接显示组件的缺点
function createContextMenu(options: CreateContextMenuProps) {
  const { event } = options
  event.preventDefault()
  const props = Object.assign({}, defaultProps, omit(options, ['event']), {
    position: {
      x: event.clientX,
      y: event.clientY
    }
  })
  const mInstance = getInstance()
  mInstance.close()
  setTimeout(() => {
    mInstance.open(props)
  }, 20)
  return mInstance
}

export { createContextMenu }

使用

// 该代码省略了很多
<template>
  <div class="com-page p20">
    <a-row type="flex" class="menu-container" align="middle" @contextmenu="onContainerRightClick">
      <ComImage className="image" :src="logo" alt="logo" />
      <h2 class="title">{{ data.title }}</h2>
      <section class="font-size-16">{{ data.description }}</section>
    </a-row>
  </div>
</template>
 <script lang="ts">
import { defineComponent, reactive, ref } from 'vue'
import { createContextMenu } from '@/components/context-menu/create-context-menu'
export default defineComponent({
  setup() {
    function onContainerRightClick(e: MouseEvent) {
      createContextMenu({
        event: e,
        menus: [
          {
            label: '编辑',
            handler() {
              visible.value = true
            }
          },
          {
            label: '复制标题',
            handler(item, event: MouseEvent) {
              copy(form.title, event)
              message.destroy()
              message.success('复制成功,ctrl+v进行粘贴')
            }
          }
        ]
      })
    }
  }
})
</script>

右键.gif

总结

预览地址,账号:lgf@163.com,密码:123456。

ts版源码地址

js版源码地址

vue3-context-menu使用文档

vue3-context-menu插件来自ant-simple-pro里面,ant-simple-pro有很多用vue3+ts开发的插件。

ant-simple-pro简洁,美观,快速上手,支持3大框架。