美文网首页
vue3 多级右键菜单 升级版

vue3 多级右键菜单 升级版

作者: 无题syl | 来源:发表于2022-05-09 11:34 被阅读0次

    多级菜单指令化

    Directive
    v-contextmenu="菜单数据"
    参考一个开源项目 PPTIST

    结果样式

    image.png

    目录结构

    指令


    image.png

    菜单template 组件


    image.png

    引入

    vue3 使用指令 要在main.js中引入

    import Directive from '@/plugins/directive'
    app.use(Directive)
    

    一 · 指令目录结构 文件

    contextmenu.ts

    import { Directive, createVNode, render, DirectiveBinding } from 'vue'
    import ContextmenuComponent from '@/components/Contextmenu/index.vue'
    
    const CTX_CONTEXTMENU_HANDLER = 'CTX_CONTEXTMENU_HANDLER'
    
    const contextmenuListener = (el: HTMLElement, event: MouseEvent, binding: DirectiveBinding) => {
      event.stopPropagation()
      event.preventDefault()
    
      const menus = binding.value(el)
      if (!menus) return
    
      let container: HTMLDivElement | null = null
    
      // 移除右键菜单并取消相关的事件监听
      const removeContextmenu = () => {
        if (container) {
          document.body.removeChild(container)
          container = null
        }
        el.classList.remove('contextmenu-active')
        document.body.removeEventListener('scroll', removeContextmenu)  
        window.removeEventListener('resize', removeContextmenu)
      }
    
      // 创建自定义菜单
      const options = {
        axis: { x: event.x, y: event.y },
        el,
        menus,
        removeContextmenu,
      }
      container = document.createElement('div')
      const vm = createVNode(ContextmenuComponent, options, null)
      render(vm, container)
      document.body.appendChild(container)
    
      // 为目标节点添加菜单激活状态的className
      el.classList.add('contextmenu-active')
    
      // 页面变化时移除菜单
      document.body.addEventListener('scroll', removeContextmenu)
      window.addEventListener('resize', removeContextmenu)
    }
    
    const ContextmenuDirective: Directive = {
      mounted(el: HTMLElement, binding) {
        el[CTX_CONTEXTMENU_HANDLER] = (event: MouseEvent) => contextmenuListener(el, event, binding)
        el.addEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
      },
    
      unmounted(el: HTMLElement) {
        if (el && el[CTX_CONTEXTMENU_HANDLER]) {
          el.removeEventListener('contextmenu', el[CTX_CONTEXTMENU_HANDLER])
          delete el[CTX_CONTEXTMENU_HANDLER]
        }
      },
    }
    
    export default ContextmenuDirective
    

    plugin directive index.ts

    import { App } from 'vue'
    
    import Contextmenu from './contextmenu'
    import ClickOutside from './clickOutside'
    
    export default {
      install(app: App) {
        app.directive('contextmenu', Contextmenu)
        app.directive('click-outside', ClickOutside)
      }
    }
    
    

    二· 菜单组件 单文件组件

    MenuContent.vue

    <template>
      <ul class="menu-content">
        <template v-for="(menu, index) in menus" :key="menu.text || index">
          <li
            v-if="!menu.hide"
            class="menu-item"
            @click.stop="handleClickMenuItem(menu)"
            :class="{'divider': menu.divider, 'disable': menu.disable}"
          >
            <div 
              class="menu-item-content"   
              :class="{
                'has-children': menu.children,
                'has-handler': menu.handler,
              }" 
              v-if="!menu.divider"
            >
              <span class="text">{{menu.text}}</span>
              <span class="sub-text" v-if="menu.subText && !menu.children">{{menu.subText}}</span>
    
              <menu-content 
                class="sub-menu"
                :menus="menu.children" 
                v-if="menu.children && menu.children.length"
                :handleClickMenuItem="handleClickMenuItem" 
              />
            </div>
          </li>
        </template>
      </ul>
    </template>
    
    <script lang="ts">
    import { PropType, defineComponent } from 'vue'
    import { ContextmenuItem } from './types'
    
    export default defineComponent({
      name: 'menu-content',
      props: {
        menus: {
          type: Array as PropType<ContextmenuItem[]>,
          required: true,
        },
        handleClickMenuItem: {
          type: Function,
          required: true,
        },
      },
    })
    </script>
    
    <style lang="scss" scoped>
    $menuWidth: 170px;
    $menuHeight: 30px;
    $subMenuWidth: 120px;
    
    .menu-content {
      width: $menuWidth;
      padding: 5px 0;
      background: #fff;
      border: 1px solid $borderColor;
      box-shadow: $boxShadow;
      border-radius: 2px;
      list-style: none;
      margin: 0;
    }
    .menu-item {
      padding: 0 20px;
      color: #555;
      font-size: 12px;
      transition: all $transitionDelayFast;
      white-space: nowrap;
      height: $menuHeight;
      line-height: $menuHeight;
      background-color: #fff;
      cursor: pointer;
    
      &:not(.disable):hover > .menu-item-content > .sub-menu {
        display: block;
      }
    
      &:not(.disable):hover > .has-children.has-handler::after {
        transform: scale(1);
      }
    
      &:hover:not(.disable) {
        background-color: rgba($color: $themeColor, $alpha: .2);
      }
    
      &.divider {
        height: 1px;
        overflow: hidden;
        margin: 5px;
        background-color: #e5e5e5;
        line-height: 0;
        padding: 0;
      }
    
      &.disable {
        color: #b1b1b1;
        cursor: no-drop;
      }
    }
    .menu-item-content {
      display: flex;
      align-items: center;
      justify-content: space-between;
      position: relative;
    
      &.has-children::before {
        content: '';
        display: inline-block;
        width: 8px;
        height: 8px;
        border-width: 1px;
        border-style: solid;
        border-color: #666 #666 transparent transparent;
        position: absolute;
        right: 0;
        top: 50%;
        transform: translateY(-50%) rotate(45deg);
      }
      &.has-children.has-handler::after {
        content: '';
        display: inline-block;
        width: 1px;
        height: 24px;
        background-color: #f1f1f1;
        position: absolute;
        right: 18px;
        top: 3px;
        transform: scale(0);
        transition: transform $transitionDelay;
      }
    
      .sub-text {
        opacity: 0.6;
      }
      .sub-menu {
        width: $subMenuWidth;
        position: absolute;
        display: none;
        left: 112%;
        top: -6px;
      }
    }
    </style>
    

    index.vue

    <template>
      <div 
        class="mask"
        @contextmenu.prevent="removeContextmenu()"
        @mousedown="removeContextmenu()"
      ></div>
    
      <div 
        class="contextmenu"
        :style="{
          left: style.left + 'px',
          top: style.top + 'px',
        }"
        @contextmenu.prevent
      >
        <MenuContent 
          :menus="menus"
          :handleClickMenuItem="handleClickMenuItem" 
        />
      </div>
    </template>
    
    <script lang="ts">
    import { computed, defineComponent, PropType } from 'vue'
    import { ContextmenuItem, Axis } from './types'
    
    import MenuContent from './MenuContent.vue'
    
    export default defineComponent({
      name: 'contextmenu',
      components: {
        MenuContent,
      },
      props: {
        axis: {
          type: Object as PropType<Axis>,
          required: true,
        },
        el: {
          type: Object as PropType<HTMLElement>,
          required: true,
        },
        menus: {
          type: Array as PropType<ContextmenuItem[]>,
          required: true,
        },
        removeContextmenu: {
          type: Function,
          required: true,
        },
      },
      setup(props) {
        const style = computed(() => {
          const MENU_WIDTH = 170
          const MENU_HEIGHT = 30
          const DIVIDER_HEIGHT = 11
          const PADDING = 5
    
          const { x, y } = props.axis
          const menuCount = props.menus.filter(menu => !(menu.divider || menu.hide)).length
          const dividerCount = props.menus.filter(menu => menu.divider).length
    
          const menuWidth = MENU_WIDTH
          const menuHeight = menuCount * MENU_HEIGHT + dividerCount * DIVIDER_HEIGHT + PADDING * 2
    
          const screenWidth = document.body.clientWidth
          const screenHeight = document.body.clientHeight
    
          return {
            left: screenWidth <= x + menuWidth ? x - menuWidth : x,
            top: screenHeight <= y + menuHeight ? y - menuHeight : y,
          }
        })
    
        const handleClickMenuItem = (item: ContextmenuItem) => {
          if (item.disable) return
          if (item.children && !item.handler) return
          if (item.handler) item.handler(props.el)
          props.removeContextmenu()
        }
    
        return {
          style,
          handleClickMenuItem,
        }
      },
    })
    </script>
    
    <style lang="scss">
    .mask {
      position: fixed;
      left: 0;
      top: 0;
      width: 100vw;
      height: 100vh;
      z-index: 9998;
    }
    .contextmenu {
      position: fixed;
      z-index: 9999;
      user-select: none;
    }
    </style>
    

    type.ts

    export interface ContextmenuItem {
      text?: string;
      subText?: string;
      divider?: boolean;
      disable?: boolean;
      hide?: boolean;
      children?: ContextmenuItem[];
      handler?: (el: HTMLElement) => void;
    }
    
    export interface Axis {
      x: number;
      y: number;
    }
    

    三·用法

    
       v-contextmenu="contextmenusThumbnails"
    
    import { ContextmenuItem } from '@/components/Contextmenu/types'
        const contextmenusThumbnails = (): ContextmenuItem[] => {
          return [
            {
              text: '粘贴',
              subText: 'Ctrl + V',
              handler: pasteSlide,
            },
            {
              text: '全选',
              subText: 'Ctrl + A',
              handler: selectAllSlide,
            },
            {
              text: '新建页面',
              subText: 'Enter',
              handler: createSlide,
            },
            {
              text: '开始演示',
              subText: 'Ctrl + F',
              handler: enterScreening,
            },
          ]
        }
    
    
    

    相关文章

      网友评论

          本文标题:vue3 多级右键菜单 升级版

          本文链接:https://www.haomeiwen.com/subject/fqkhurtx.html