自定义pop弹框

231 阅读2分钟

自定义click-outside指令

//这是vue2的全局注册自定义指令,不同vue版本的挂载命令不一样,也有可能是mounted这种的
Vue.directive('click-outside', {
  // 当被绑定的元素挂载到 DOM 中时……
  bind(el, binding) {
    function eventHandler(e) {
        if (el.contains(e.target)) {
            return !binding.value
      }
      // 如果绑定的参数是函数,正常情况也应该是函数
      if (binding.value && typeof binding.value === 'function') {
          binding.value(e)
      }
    }
    // 用于销毁前注销事件监听
    el.__click_outside__ = eventHandler
    // 添加事件监听
    document.addEventListener('click', eventHandler)
    document.addEventListener('mousedown', eventHandler)
  },
  unbind(el) {
    // 移除事件监听
    document.removeEventListener('click', el.__click_outside__)
    document.removeEventListener('mousedown', el.__click_outside__)
    // 删除无用属性
    delete el.__click_outside__
  }
})

pop组件内容

这里的pop组件是将pop内容放在触发元素下面,并不是放置在body下面

<!-- 提示框 -->
<template>
  <div v-click-outside="() => showAll = false" class="toolWrapper"     @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave" @click="handleClick" @keyup.enter="handleEnter">
    <div class="origin">
      <slot></slot> 
    </div>
    <div :style="{ top: top + 'px', left: left + 'px', width: width }"
      :class="['otherMessage', isShow && showAll ? 'show' : '']">
      <slot name="content"></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    content: {
      type: String,
      default: ''
    },
    //
    isShow: {
      type: Boolean,
      default: true
    },
    blurType: {
      type: String,
      default: 'mouseEnter'
    },
    contentStyle: {
    },
    top: {
      default: 0
    },
    left: {
      default: 0
    },
    width: {
      type: String,
      default: '100%'
    }
  },
  data() {
    return {
      showAll: false
    }
  },
  methods: {
    handleMouseEnter() {
      this.$emit('MouseEnter')
      if (this.blurType === 'mouseEnter') {
        this.showAll = true
      }
    },
    handleMouseLeave() {
      if (this.blurType === 'mouseEnter') {
        this.showAll = false
      }
    },
    handleClick() {
      this.$emit('propClick')
      if (this.blurType === 'click') {
        this.showAll = !this.showAll
      }
    },
    handleEnter() {
      if (this.blurType === 'enter') {
        this.showAll = true
      }
    }
  }
}
</script>

<style lang="scss" scoped>
.toolWrapper {
  height: 100%;
  position: relative;
  -webkit-app-region: no-drag;
  z-index: 1;

  .origin {
    height: 100%;
    cursor: default;
    -webkit-app-region: no-drag;
  }

  .otherMessage {
    width: 100%;
    border-radius: 4px;
    background: rgba(255, 255, 255, 0.75);
    box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.15);
    backdrop-filter: blur(6px);
    visibility: hidden;
    z-index: 99;
    position: absolute;
  }

  .show {
    visibility: visible;
  }
}
</style>

pop组件(可以放置body下,vue3版本)

<!-- 提示框 -->
<template>
  <div ref="origin" id="origin" v-click-outside="() => showAll = false" class="toolWrapper"
    @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave" @keyup.enter="handleEnter">
    <div :aria-describedby="id" :class="['origin', originChoose ? isShow && showAll ? 'origin-choose' : '' : '']"
      @click="handleClick">
      <slot :popShow="isShow && showAll"></slot>
    </div>
    <template v-if="tobody">
      <Teleport to="body">
        <div v-if="isShow && showAll" ref="otherMessage" :class="['otherMessage', isShow && showAll ? 'show' : '']"
          :id="id" @click="handleContentClick" style="position: absolute;">
          <slot name="content"></slot>
        </div>
      </Teleport>
    </template>
    <template v-else>
      <div ref="otherMessage" :style="contentStyle" :class="['otherMessage', isShow && showAll ? 'show' : '']"
        @click="handleContentClick">
        <slot name="content"></slot>
      </div>
    </template>
  </div>
</template>

<script setup lang="ts">
import { nanoid } from 'nanoid';
import { ref, type StyleValue, useSlots, onMounted, computed, onBeforeMount, watch, nextTick } from 'vue'
interface Props {
  content?: string | number,
  contentStyle?: StyleValue
  isShow?: boolean// 根据props判断是否显示
  blurType?: string
  emitShow?: boolean//触发之后还展示吗
  originChoose?: boolean//pop框出来之后触发元素样式更改吗
  tobody?: boolean//是否渲染到body中
}
const props = withDefaults(defineProps<Props>(), {
  content: '',
  isShow: true,
  blurType: 'mouseEnter'
})
const id = nanoid(10)
const emits = defineEmits(['MouseEnter', 'propClick'])
const origin = ref<any>(null)
const otherMessage = ref<any>(null)
// 获取body的位置信息
const getBodyPosition = () => {
  if (props.tobody) {
    const originPosition = origin.value?.getBoundingClientRect()
    const content = otherMessage.value?.getBoundingClientRect()
    // 弹出框位置与触发元素对齐
    if (otherMessage.value) {
      otherMessage.value!.style.left = originPosition?.left + (originPosition.width / 2) - (content.width / 2) + 'px'
      otherMessage.value!.style.top = originPosition?.top + originPosition.height + 'px'
    }
  }
}
// 根据宽度判断是否省略
// const isHidden = ref(false)
// 移入元素内
const handleMouseEnter = () => {
  emits('MouseEnter')
  if (props.blurType === 'mouseEnter') {
    showAll.value = true
  }
}
// 移出元素
const handleMouseLeave = () => {
  if (props.blurType === 'mouseEnter') {
    showAll.value = false
  }
}
// 点击元素
const handleClick = () => {
  emits('propClick')
  if (props.blurType === 'click') {
    showAll.value = !showAll.value
  }
}
// 点击popup框内元素
const handleContentClick = () => {
  emits('propClick')
  if (!props.emitShow && props.blurType === 'click') {
    showAll.value = !showAll.value
  }

}
// 点击enter键
const handleEnter = () => {
  if (props.blurType === 'enter') {
    showAll.value = true
  }
}
// 获取插槽内元素
const slots = useSlots()
const showAll = ref(false)
watch([props.isShow, showAll], (newVal) => {
  nextTick(() => {
    getBodyPosition()
  })
}, { immediate: true, deep: true })
onMounted(() => {
  if (props.tobody) {
    getBodyPosition()
  }
  window.addEventListener('resize', getBodyPosition)
})
onBeforeMount(() => {
  window.removeEventListener('resize', getBodyPosition)
})
defineExpose({
  showPop: props.isShow && showAll.value
});
</script>

<style lang="scss" scoped>
.toolWrapper {
  height: 100%;
  position: relative;

  .origin {
    height: 100%;
    cursor: default;
  }

  .otherMessage {
    left: 50%;
    transform: translateX(-50%);
    width: 100%;
    border-radius: 4px;
    background: rgba(255, 255, 255);
    box-shadow: 0px 1px 10px 0px rgba(0, 0, 0, 0.05), 15px 0px 10px 0px rgba(0, 0, 0, 0.05);
    visibility: hidden;
    z-index: 99;
    position: absolute;
  }

  .show {
    visibility: visible;
  }
}
</style>