基于 vue 3.0.0-beta.x 的 contextmenu 组件

2020/5/6 5:26:32

本文主要是介绍基于 vue 3.0.0-beta.x 的 contextmenu 组件,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

介绍

contextmenu 组件是基于 vue 3.0-beta 的一次试验性开发,其中包括一些新 api 的使用:

  • template refs - 用来给元素或子组件注册引用信息
  • provide and inject - 依赖注入
  • computed - 计算属性
  • reactive - 返回原始对象的响应式代理
  • nextTick - 在下次 DOM 更新循环结束之后执行延迟回调
  • global directive - 此 api 在文档中还没有公布
  • ...

这里要强调一点,Vue 3.0 时代不是函数式编程,只是 API 暴露为函数。将 2.x 中与组件逻辑相关的选项以 API 函数的形式重新设计,因此在 Vue 2.x 中使用的一些选项,如 datacomputed 等,都将抽离成独立的函数来使用,这个可能是 2.0 思想需要转变的地方。

contextmenu 组件一共由四部分组成,下面将分别对其介绍。

ContextMenu

ContextMenu 组件作为 contextmenu 组件的容器,负责重写原生右键菜单的地方,阻止右键点击的默认行为等。

这里主要介绍一些关键逻辑处理实现,代码可能会有忽略的地方。

重写默认菜单 - 阻止默认行为

setup() {
  const registerHandlers = (ref) => {
    let target = ref.el
    target.addEventListener('contextmenu', handleContextMenu)
  }

  const handleContextMenu = (e) => {
    e.preventDefault()
    const { value: menuEl } = contextmenu
    const { pageX: x, pageY: y } = e
    state.isVisible = true
    Vue.nextTick(() => {
      const { left, top } = getMenuPosition(x, y)
      menuEl.style.top = `${top + 5}px`
      menuEl.style.left = `${left + 5}px`
    })
  }
  ...
  return {
    registerHandlers,
    handleContextMenu
  }
}

这里我们通过 DOM 2 级事件 addEventListener 监听 contextmenu,在事件处理程序 handleContextMenu 中,首先通过 preventDefault() 来阻止右键菜单的默认行为,接着就是处理自定义右键菜单的显示位置等等。

菜单定位

...
<template>
  <div>
    <ul 
      class="next-context-menu" 
      v-show="state.isVisible" 
      :style="{height: `${height}px`, width: `${width}px`}" 
      ref="contextmenus">
      <slot></slot>
    </ul>
  </div>
</template>
...

const contextmenu = Vue.ref(null)
const getMenuPosition = (x, y) => {
  const { value: menuEl } = contextmenu
  const menuStyles = { top: y, left: x }
  const { innerWidth, innerHeight } = window;
  const { clientWidth: menuElWidth, clientHeight: menuElHeight } = menuEl
  if (y + menuElHeight > innerHeight) 
    menuStyles.top -= menuElHeight
  if (x + menuElWidth > innerWidth)  
    menuStyles.left -= menuElWidth
  if (menuStyles.top < 0) 
    menuStyles.top = menuElHeight < innerHeight ? (innerHeight - menuElHeight) / 2 : 0
  if (menuStyles.left < 0) 
    menuStyles.left = menuElWidth < innerWidth ? (innerWidth - menuElWidth) / 2 : 0
  return menuStyles
}
...

通过 template-ref 获得真实 dom 节点,根据节点的 clientWidthclientHeight 计算 dom 在浏览器窗口的位置呈现。

实例注入 - 已备子组件使用

setup() {
  Vue.provide('instance', Vue.getCurrentInstance())  
}

通过 provide 向子组件注入 ContextMenu 组件实例,这里的子组件是 ContextMenuItem,这里是为了处理当你点击右键菜单某一项时,来控制整个菜单的显示或隐藏,下面会继续介绍。

ContextMenuItem

ContextMenuItem 是用来呈现菜单项的组件,此组件很简单,当点击某一项时,控制整个右键菜单的显示和隐藏,菜单项的具有两个特性:可点击点击后不隐藏菜单

<template>
  <li class="context-menu-item" :class="itemClass" @click="handleClickItem"> 
    <slot/>
  </li>
</template>

不可点击项

<script>
  ...
  props: {
    disabled: Boolean, 
    hideMenu: {
      type: Boolean,
      default: true,
    },
  },
  setup(props) {
    const rootInstance = inject('instance')
    const itemClass = reactive({
      'context-menu-item-disabled': computed(() => props.disabled)
    })
    return {
      itemClass
    }
  }
</script>

不可点击项具有 disabled 属性,值为布尔,由父级通过 props 传递。

disabled

控制菜单显示隐藏

...
setup() {
  const rootInstance = inject('instance')
  const handleClickItem = (event) => {
    props.hideMenu && rootInstance.ctx.hideMenu()
  }
  return {
    rootInstance,
    handleClickItem,
  }
}

菜单项具由 hideMenu 属性,通过 props 接受,如何值 true(默认值),则当点击此菜单项时,隐藏整个菜单,如果值 false,则点击后不隐藏菜单。

<context-menu width="100" ref="contextmenu">
  <context-menu-item :hideMenu="false">不隐藏菜单</context-menu-item>
</context-menu>

ContextMenuGroup

ContextMenuGroup 组件可以将菜单项分类成不同的组别,ContextMenuGroup 组件具有 name 属性,用来显示不同组件的名称。

name

<template>
  <ul class="context-menu-group">
    <span v-if="name" class="context-menu-group-name">{{name}}</span>
    <slot></slot>
  </ul>
</template>

<script>
export default {
  name: 'ContextMenuGroup',
  props: {
    name: {
      type: String,
      required: true
    }
  }
}

此组件很简单,模版具有插槽,用来显示子内容,name 属性通过 props 传递。

<context-menu width="100" ref="contextmenu">
  <context-menu-group name="分组标题">
    <context-menu-item>item11</context-menu-item>
  </context-menu-group>
</context-menu>

ContextMenuSub

ContextMenuSub 组件定义子菜单的逻辑,它的内部主要包括处理子菜单的渲染和位置呈现。

当用户使用鼠标划过具有子菜单的菜单项时,会根据鼠标点击的位置计算子菜单应该显示在哪个位置。

const setDynamicClass = (e) => {
  const dynamicClassName = []
  const { target } = e
  const { innerWidth, innerHeight } = window
  const { clientWidth: subMenuWidth, clientHeight: subMenuHeight} = contextmenuSub.value
  const rect = target.getBoundingClientRect()
  if (rect.bottom + subMenuHeight > innerHeight) {
    dynamicClassName.push('bottom')         /** position.bottom = 0 */
  } else dynamicClassName.push('top')       /** position.top = 0 */
  if (rect.right + subMenuWidth > innerWidth) {
    dynamicClassName.push('left')           /** position.left = 0 */
  } else dynamicClassName.push('right')     /** position.right = 0 */
  return dynamicClassName
}

全局指令

定义全局指令 v-contextmenu 的目的是需要告诉浏览器用户点击哪个区域用来显示自定义的右键菜单。

在 Vue 2.x 中,注册一个全局指令可能像这样:

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

但是在 Vue 3.0 中,不存在全局 Vue,全局 API 请看整个 RFC

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

app.config.globalProperties.customProperty = () => {}

app.mount(App, '#app')

因此,定义一个全局指令需要这样:

import App from './App.vue'
const app = createApp(App)
app.directive(/* ... */)
app.mount(App, '#app')

但是具体是怎么实现一个全局 directive, RFC 上就没有再继续介绍了,尝试通过 2.0 的写法会报错,最后通过源码定义得知 3.0 没有 inserted 方法,它接受两个参数,第一个 name 为指令名称,第二个参数是一个钩子函数。

const app = {
  ...
  directive(name, directive) {
    {
        validateDirectiveName(name);
    }
    if (!directive) {
        return context.directives[name];
    }
    if ( context.directives[name]) {
        warn(`Directive "${name}" has already been registered in target app.`);
    }
    context.directives[name] = directive;
    return app;
  },
  ...
}

因此,在 3.0 中自定义一个全局指令大概像这样:

const app = createApp(App)
app.directive('contextmenu', (el, binding, vnode) => {
  const instance = vnode.dirs[0].instance
  instance.contextmenu.registerHandlers({el, vnode})
})

钩子函数中的参数同 2.0,这个全局的 v-contextmenu 用来控制 ContextMenu 组件中 contextmenu 事件的触发对象。

最后一个完整的组件使用像这个:

<template>
<div>
  <context-menu width="100" ref="contextmenu">
    <context-menu-group name="分组标题">
        <context-menu-item>item11</context-menu-item>
        <context-menu-item disabled>item11</context-menu-item>
        <context-menu-item @click="handleSelect">item11</context-menu-item>
      </context-menu-group>
  </context-menu>
  <!-- 触发自定义右键菜单的区域,点击其他区域显示默认右键菜单  -->
  <div v-contextmenu>
    <div style="width: 100%; height: 200px; background: #fdf9e1;"></div>
  </div>
</div>
</template>

最后

本文所介绍的内容是基于 Vue 3.0 的一次开发体验,文中所述的 API 不代表最终的结果,以官网发布为准。最后,如本文开头所介绍的那样,Vue 3.0 时代不是函数式编程,只是 API 暴露为函数。说实话,Vue 这次重写,让人有点不太适应,感觉太碎很别扭(手动狗头)。

实现过程中还有很多问题待解决,有兴趣的话可以持续关注。



这篇关于基于 vue 3.0.0-beta.x 的 contextmenu 组件的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程