自定义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>