zhoucg 1 anno fa
parent
commit
1c300f426c

+ 284 - 0
src/components/yvan-context/components/Context.vue

@@ -0,0 +1,284 @@
+<template>
+  <ul v-show="visible" ref="contextmenu" :class="contextmenuCls" :style="style">
+    <slot></slot>
+  </ul>
+</template>
+
+<script>
+import eventBus from "@/utils/eventBus";
+
+export default {
+  name: 'yvan-print-context',
+  provide() {
+    return {
+      $$contextmenu: this
+    }
+  },
+  props: {
+    eventType: {
+      type: String,
+      default: 'contextmenu'
+    },
+    theme: {
+      type: String,
+      default: 'default'
+    },
+    autoPlacement: {
+      type: Boolean,
+      default: true
+    },
+    disabled: Boolean
+  },
+
+  data() {
+    return {
+      visible: false,
+      references: [],
+      style: {
+        top: 0,
+        left: 0
+      }
+    }
+  },
+  computed: {
+    clickOutsideHandler() {
+      return this.visible ? this.hide : () => {
+      }
+    },
+    isClick() {
+      return this.eventType === 'click'
+    },
+    contextmenuCls() {
+      return ['yvan-print-context', `yvan-print-context--${this.theme}`]
+    }
+  },
+
+  watch: {
+    visible(value) {
+      if (value) {
+        this.$emit('show', this)
+        document.body.addEventListener('click', this.handleBodyClick)
+      } else {
+        this.$emit('hide', this)
+        document.body.removeEventListener('click', this.handleBodyClick)
+      }
+    }
+  },
+  mounted() {
+    document.body.appendChild(this.$el)
+    eventBus.on("showContextMenu", (position) => {
+      this.show(position)
+    })
+    if (window.$$VContextmenu) {
+      window.$$VContextmenu[this.$contextmenuId] = this
+    } else {
+      window.$$VContextmenu = {[this.$contextmenuId]: this}
+    }
+  },
+  beforeDestroy() {
+    document.body.removeChild(this.$el)
+    delete window.$$VContextmenu[this.$contextmenuId]
+    this.references.forEach((ref) => {
+      ref.el.removeEventListener(this.eventType, this.handleReferenceContextmenu)
+    })
+    document.body.removeEventListener('click', this.handleBodyClick)
+  },
+
+  methods: {
+    addRef(ref) {
+      // FIXME: 如何处理 removeRef?
+      this.references.push(ref)
+      ref.el.addEventListener(this.eventType, this.handleReferenceContextmenu)
+    },
+    handleReferenceContextmenu(event) {
+      event.preventDefault()
+      if (this.disabled) {
+        return
+      }
+      const reference = this.references.find((ref) => ref.el.contains(event.target))
+      this.$emit('contextmenu', reference ? reference.vnode : null)
+      const eventX = event.pageX
+      const eventY = event.pageY
+      this.show()
+      this.$nextTick(() => {
+        const contextmenuPosition = {
+          top: eventY,
+          left: eventX
+        }
+        if (this.autoPlacement) {
+          const contextmenuWidth = this.$refs.contextmenu.clientWidth
+          const contextmenuHeight = this.$refs.contextmenu.clientHeight
+          if (contextmenuHeight + eventY >= window.innerHeight) {
+            contextmenuPosition.top -= contextmenuHeight
+          }
+          if (contextmenuWidth + eventX >= window.innerWidth) {
+            contextmenuPosition.left -= contextmenuWidth
+          }
+        }
+        this.style = {
+          top: `${contextmenuPosition.top}px`,
+          left: `${contextmenuPosition.left}px`
+        }
+      })
+    },
+    handleBodyClick(event) {
+      const notOutside =
+          this.$el.contains(event.target) ||
+          (this.isClick && this.references.some((ref) => ref.el.contains(event.target)))
+
+      if (!notOutside) {
+        this.visible = false
+      }
+    },
+    show(position) {
+      Object.keys(window.$$VContextmenu).forEach((key) => {
+        if (key !== this.$contextmenuId) {
+          window.$$VContextmenu[key].hide()
+        }
+      })
+      if (position) {
+        this.style = {
+          top: `${position.top}px`,
+          left: `${position.left}px`
+        }
+      }
+      this.visible = true
+    },
+    hide() {
+      this.visible = false
+    },
+    hideAll() {
+      Object.keys(window.$$VContextmenu).forEach((key) => {
+        window.$$VContextmenu[key].hide()
+      })
+    }
+  }
+}
+</script>
+
+<style>
+.yvan-print-context {
+  position: absolute;
+  padding: 5px 0;
+  margin: 0;
+  background-color: #fff;
+  border: 1px solid #e8e8e8;
+  border-radius: 4px;
+  box-shadow: 2px 2px 8px 0 rgba(150, 150, 150, 0.2);
+  list-style: none;
+  font-size: 14px;
+  white-space: nowrap;
+  cursor: pointer;
+  z-index: 2800;
+  -webkit-tap-highlight-color: transparent;
+}
+
+.yvan-print-context .yvan-print-context-item {
+  padding: 5px 14px;
+  line-height: 1;
+  color: #333;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+}
+
+.yvan-print-context .yvan-print-context-item.yvan-print-context-item--hover {
+  color: #fff;
+}
+
+.yvan-print-context .yvan-print-context-item.yvan-print-context-item--disabled {
+  color: #ccc;
+  cursor: not-allowed;
+}
+
+.yvan-print-context .yvan-print-context-divider {
+  height: 0;
+  margin: 5px 0;
+  border-bottom: 1px solid #e8e8e8;
+}
+
+.yvan-print-context .yvan-print-context-group__menus {
+  padding: 0 5px;
+  margin: 0;
+  list-style: none;
+}
+
+.yvan-print-context .yvan-print-context-group__menus .yvan-print-context-item {
+  display: inline-block;
+  padding: 5px 9px;
+}
+
+.yvan-print-context .yvan-print-context-submenu {
+  position: relative;
+}
+
+.yvan-print-context .yvan-print-context-submenu > .yvan-print-context {
+  position: absolute;
+}
+
+.yvan-print-context .yvan-print-context-submenu > .yvan-print-context.left {
+  left: 0;
+  transform: translateX(-100%);
+}
+
+.yvan-print-context .yvan-print-context-submenu > .yvan-print-context.right {
+  right: 0;
+  transform: translateX(100%);
+}
+
+.yvan-print-context .yvan-print-context-submenu > .yvan-print-context.top {
+  top: -6px;
+}
+
+.yvan-print-context .yvan-print-context-submenu > .yvan-print-context.bottom {
+  bottom: -6px;
+}
+
+.yvan-print-context .yvan-print-context-submenu .yvan-print-context-submenu__title {
+  margin-right: 10px;
+}
+
+.yvan-print-context .yvan-print-context-submenu .yvan-print-context-submenu__icon {
+  position: absolute;
+  right: 5px;
+}
+
+.yvan-print-context .yvan-print-context-submenu .yvan-print-context-submenu__icon::before {
+  content: '\e622';
+}
+
+.yvan-print-context--default .yvan-print-context-item--hover {
+  background-color: #4579e1;
+}
+
+.yvan-print-context--bright .yvan-print-context-item--hover {
+  background-color: #ef5350;
+}
+
+.yvan-print-context--dark .yvan-print-context-item--hover {
+  background-color: #2d3035;
+}
+
+.yvan-print-context--dark {
+  background: #222222;
+  box-shadow: 2px 2px 8px 0 rgba(0, 0, 0, 0.2);
+  border: 1px solid #111111;
+}
+
+.yvan-print-context--dark .yvan-print-context-item {
+  color: #ccc;
+}
+
+.yvan-print-context-item i {
+  padding: 0 10px 0 0;
+}
+
+.yvan-print-context--danger {
+  color: #f54536 !important;
+}
+
+.yvan-print-context--danger:hover {
+  background: #f54536;
+  color: #fff !important;
+}
+</style>

+ 33 - 0
src/components/yvan-context/components/ContextGroup.vue

@@ -0,0 +1,33 @@
+<template>
+  <li class="v-contextmenu-group">
+    <ul :style="menusStyle" class="v-contextmenu-group__menus">
+      <slot></slot>
+    </ul>
+  </li>
+</template>
+
+<script>
+export default {
+  name: 'ContextGroup',
+
+  props: {
+    maxWidth: {
+      type: [Number, String],
+      default: undefined
+    }
+  },
+
+  computed: {
+    menusStyle() {
+      if (!this.maxWidth) {
+        return null
+      }
+
+      return {
+        'max-width': typeof this.maxWidth === 'number' ? `${this.maxWidth}px` : this.maxWidth,
+        'overflow-x': 'auto'
+      }
+    }
+  }
+}
+</script>

+ 74 - 0
src/components/yvan-context/components/ContextItem.vue

@@ -0,0 +1,74 @@
+<template>
+  <li v-if="divider" class="yvan-print-context-divider"></li>
+
+  <li
+      v-else
+      :class="classname"
+      @click="handleClick"
+      @mouseenter="handleMouseenter"
+      @mouseleave="handleMouseleave"
+  >
+    <slot></slot>
+  </li>
+</template>
+
+<script>
+export default {
+  name: 'ContextItem',
+
+  inject: ['$$contextmenu'],
+
+  props: {
+    divider: Boolean,
+    disabled: Boolean,
+    autoHide: {
+      type: Boolean,
+      default: true
+    }
+  },
+
+  data() {
+    return {
+      hover: false
+    }
+  },
+  computed: {
+    classname() {
+      return {
+        'yvan-print-context-item': !this.divider,
+        'yvan-print-context-item--hover': this.hover,
+        'yvan-print-context-item--disabled': this.disabled
+      }
+    }
+  },
+
+  methods: {
+    handleMouseenter(event) {
+      if (this.disabled) {
+        return
+      }
+
+      this.hover = true
+
+      this.$emit('mouseenter', this, event)
+    },
+    handleMouseleave(event) {
+      if (this.disabled) {
+        return
+      }
+
+      this.hover = false
+
+      this.$emit('mouseleave', this, event)
+    },
+
+    handleClick(event) {
+      if (this.disabled) {
+        return
+      }
+      this.$emit('handle-click', this, event)
+      this.autoHide && this.$$contextmenu.hide()
+    }
+  }
+}
+</script>

+ 91 - 0
src/components/yvan-context/components/ContextSubmenu.vue

@@ -0,0 +1,91 @@
+<template>
+  <li :class="classname" @mouseenter="handleMouseenter" @mouseleave="handleMouseleave">
+    <span class="v-contextmenu-submenu__title">
+      <slot name="title">{{ title }}</slot>
+
+      <span class="v-contextmenu-iconfont v-contextmenu-submenu__icon"></span>
+    </span>
+
+    <ul v-show="hover" ref="submenu" :class="submenuCls">
+      <slot></slot>
+    </ul>
+  </li>
+</template>
+
+<script>
+export default {
+  name: 'VContextmenuSubmenu',
+
+  props: {
+    title: {
+      type: String,
+      default: ''
+    },
+    disabled: Boolean
+  },
+
+  data() {
+    return {
+      hover: false,
+      submenuPlacement: []
+    }
+  },
+  computed: {
+    classname() {
+      return {
+        'v-contextmenu-item': true,
+        'v-contextmenu-submenu': true,
+        'v-contextmenu-item--hover': this.hover,
+        'v-contextmenu-item--disabled': this.disabled
+      }
+    },
+    submenuCls() {
+      return ['v-contextmenu', ...this.submenuPlacement]
+    }
+  },
+
+  methods: {
+    handleMouseenter(event) {
+      if (this.disabled) {
+        return
+      }
+
+      const { target } = event
+      const targetDimension = target.getBoundingClientRect()
+
+      this.hover = true
+
+      this.$emit('mouseenter', this, event)
+
+      this.$nextTick(() => {
+        const submenuWidth = this.$refs.submenu.clientWidth
+        const submenuHeight = this.$refs.submenu.clientHeight
+        const submenuPlacement = []
+
+        if (targetDimension.right + submenuWidth >= window.innerWidth) {
+          submenuPlacement.push('left')
+        } else {
+          submenuPlacement.push('right')
+        }
+
+        if (targetDimension.bottom + submenuHeight >= window.innerHeight) {
+          submenuPlacement.push('bottom')
+        } else {
+          submenuPlacement.push('top')
+        }
+
+        this.submenuPlacement = submenuPlacement
+      })
+    },
+    handleMouseleave(event) {
+      if (this.disabled) {
+        return
+      }
+
+      this.hover = false
+
+      this.$emit('mouseleave', this, event)
+    }
+  }
+}
+</script>

+ 8 - 0
src/components/yvan-context/directive.js

@@ -0,0 +1,8 @@
+export default {
+  mounted(el, binding, vnode) {
+    const node = vnode.ctx.refs[binding.arg] || vnode.ctx.refs[binding.value]
+    const contextmenu = Object.prototype.toString.call(node) === '[object Array]' ? node[0] : node
+    contextmenu.addRef({ el, vnode })
+    contextmenu.$contextmenuId = el.id || contextmenu._uid
+  }
+}

+ 7 - 0
src/components/yvan-context/index.js

@@ -0,0 +1,7 @@
+import directive from './directive'
+import Context from './components/Context.vue'
+import ContextItem from './components/ContextItem.vue'
+import ContextGroup from './components/ContextGroup.vue'
+import ContextSubmenu from './components/ContextSubmenu.vue'
+
+export { directive, Context, ContextItem, ContextGroup, ContextSubmenu }