u
This commit is contained in:
1 parent
b24015fbae
commit
6d4f1ee402
19 files changed
+782
-108
No files matched your search
@@ -7,7 +7,6 @@ $popup-prefix: '#{$prefix}-popup';
|
||||
position: absolute;
|
||||
background-color: $bg-color-container;
|
||||
color: $text-color-base;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
border-radius: $border-radius;
|
||||
z-index: 1000;
|
||||
@@ -29,7 +28,9 @@ $popup-prefix: '#{$prefix}-popup';
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
// 上方弹出,箭头在底部
|
||||
&[data-popup-placement^='top'] > &-arrow {
|
||||
bottom: -4px;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PropType } from 'vue'
|
||||
import type { PropType, VNode } from 'vue'
|
||||
import { arrow, autoUpdate, flip, offset, useFloating } from '@floating-ui/vue'
|
||||
|
||||
import {
|
||||
@@ -17,6 +17,8 @@ import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
export default defineComponent({
|
||||
name: 'APopup',
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
|
||||
trigger: {
|
||||
type: [String, Array] as PropType<
|
||||
'hover' | 'click' | 'focus' | 'contextMenu' | Array<'hover' | 'click' | 'focus' | 'contextMenu'>
|
||||
@@ -33,7 +35,7 @@ export default defineComponent({
|
||||
},
|
||||
showArrow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
destroyOnClose: {
|
||||
type: Boolean,
|
||||
@@ -60,18 +62,19 @@ export default defineComponent({
|
||||
default: () => ({}),
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
type: [String, Object, Function] as unknown as () => string | VNode | (() => VNode),
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
emits: ['update:visible'],
|
||||
setup(props, { slots, emit }) {
|
||||
const prefixCls = getPrefixCls('popup')
|
||||
|
||||
const triggerRef = ref<HTMLElement | null>(null) // 触发 引用
|
||||
const floatingRef = ref<HTMLElement | null>(null) // float 引用
|
||||
const arrowRef = ref<HTMLElement | null>(null) // 箭头引用
|
||||
const visible = ref(false)
|
||||
const hasRendered = ref(false)
|
||||
const localVisible = ref(props.visible)
|
||||
const hasRendered = ref(props.visible)
|
||||
let cleanup: (() => void) | null = null
|
||||
|
||||
const { floatingStyles, middlewareData, placement, update } = useFloating(
|
||||
@@ -132,25 +135,43 @@ export default defineComponent({
|
||||
floatEl.style.width = `${triggerEl.offsetWidth}px`
|
||||
}
|
||||
|
||||
// 准备浮动框
|
||||
const prepareFloating = async () => {
|
||||
const showPopup = async () => {
|
||||
if (!hasRendered.value)
|
||||
hasRendered.value = true
|
||||
await nextTick()
|
||||
|
||||
fitFloatingWidth()
|
||||
localVisible.value = true
|
||||
|
||||
setupAutoUpdate()
|
||||
emit('update:visible', true)
|
||||
}
|
||||
|
||||
const showPopup = async () => {
|
||||
await prepareFloating()
|
||||
visible.value = true
|
||||
}
|
||||
const hidePopup = async () => {
|
||||
localVisible.value = false
|
||||
|
||||
const hidePopup = () => {
|
||||
visible.value = false
|
||||
clearAutoUpdate()
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const togglePopup = async () => {
|
||||
await prepareFloating()
|
||||
visible.value = !visible.value
|
||||
if (!hasRendered.value)
|
||||
hasRendered.value = true
|
||||
await nextTick()
|
||||
|
||||
fitFloatingWidth()
|
||||
|
||||
localVisible.value = !localVisible.value
|
||||
await nextTick()
|
||||
|
||||
if (localVisible.value) {
|
||||
setupAutoUpdate()
|
||||
}
|
||||
else {
|
||||
clearAutoUpdate()
|
||||
}
|
||||
|
||||
emit('update:visible', localVisible.value)
|
||||
}
|
||||
|
||||
// 事件绑定
|
||||
@@ -181,29 +202,57 @@ export default defineComponent({
|
||||
const triggerEl = getDOM(triggerRef.value)
|
||||
const floatingEl = getDOM(floatingRef.value)
|
||||
if (!triggerEl?.contains(target) && !floatingEl?.contains(target)) {
|
||||
visible.value = false
|
||||
localVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 Teleport 挂载目标
|
||||
const getTeleportTarget = () => {
|
||||
if (props.appendTo instanceof HTMLElement)
|
||||
return props.appendTo
|
||||
if (typeof props.appendTo === 'string') {
|
||||
const targetEl = ref<HTMLElement>(document.body)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.appendTo instanceof HTMLElement) {
|
||||
targetEl.value = props.appendTo
|
||||
}
|
||||
else if (typeof props.appendTo === 'string') {
|
||||
const el = document.querySelector(props.appendTo)
|
||||
if (el)
|
||||
return el
|
||||
targetEl.value = el as HTMLElement
|
||||
}
|
||||
return 'body'
|
||||
}
|
||||
})
|
||||
|
||||
// 渲染 trigger slot
|
||||
const renderTriggerSlot = () => {
|
||||
const slotContent = slots.reference?.() || slots.default?.() || []
|
||||
if (slotContent.length === 1)
|
||||
return cloneVNode(slotContent[0], { ref: triggerRef, ...eventProps })
|
||||
// 只有一个子节点时
|
||||
if (slotContent.length === 1) {
|
||||
const vnode = slotContent[0]
|
||||
return cloneVNode(vnode, {
|
||||
...eventProps,
|
||||
ref: (el: any) => {
|
||||
// 使用 getDOM 解析真实 DOM
|
||||
triggerRef.value = getDOM(el)
|
||||
|
||||
// 保留用户传入的 ref
|
||||
const originalRef = (vnode as any).ref
|
||||
if (typeof originalRef === 'function') {
|
||||
originalRef(el)
|
||||
}
|
||||
else if (originalRef && typeof originalRef === 'object') {
|
||||
;(originalRef as any).value = el
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 多个子节点时,包一层 span
|
||||
return (
|
||||
<span ref={triggerRef} style="display:inline-flex;" {...eventProps}>
|
||||
<span
|
||||
style="display:inline-flex;"
|
||||
{...eventProps}
|
||||
ref={(el) => {
|
||||
triggerRef.value = getDOM(el)
|
||||
}}
|
||||
>
|
||||
{slotContent.map(v => cloneVNode(v))}
|
||||
</span>
|
||||
)
|
||||
@@ -218,24 +267,29 @@ export default defineComponent({
|
||||
clearAutoUpdate()
|
||||
})
|
||||
|
||||
watch(visible, async (val) => {
|
||||
if (val) {
|
||||
setupAutoUpdate()
|
||||
}
|
||||
else {
|
||||
clearAutoUpdate()
|
||||
}
|
||||
})
|
||||
watch(() => props.visible, (val) => {
|
||||
localVisible.value = val
|
||||
}, { immediate: true })
|
||||
|
||||
const renderContent = (content: string | VNode | (() => VNode)) => {
|
||||
if (typeof content === 'function')
|
||||
return content()
|
||||
if (typeof content === 'string') {
|
||||
return <div class={`${prefixCls}-content`}>{content}</div>
|
||||
}
|
||||
return content
|
||||
}
|
||||
return () => {
|
||||
// 懒加载
|
||||
const shouldRenderPopup = props.destroyOnClose ? visible.value : hasRendered.value
|
||||
const shouldRenderPopup = props.destroyOnClose
|
||||
? localVisible.value // 需要销毁时,只在显示时渲染
|
||||
: localVisible.value || hasRendered.value // 不销毁时保持已渲染
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderTriggerSlot()}
|
||||
|
||||
<Teleport to={getTeleportTarget()}>
|
||||
<Teleport to={targetEl.value}>
|
||||
{shouldRenderPopup && (
|
||||
<div
|
||||
style={{
|
||||
@@ -250,7 +304,7 @@ export default defineComponent({
|
||||
appear
|
||||
>
|
||||
<div
|
||||
v-show={visible.value}
|
||||
v-show={localVisible.value}
|
||||
ref={floatingRef}
|
||||
class={[prefixCls, props.popupClass]}
|
||||
style={{
|
||||
@@ -260,7 +314,7 @@ export default defineComponent({
|
||||
}}
|
||||
data-popup-placement={placement.value}
|
||||
>
|
||||
{slots.content ? slots.content() : props.content}
|
||||
{slots.content ? slots.content() : renderContent(props.content)}
|
||||
{props.showArrow && (
|
||||
<span
|
||||
class={`${prefixCls}-arrow`}
|
||||
|
||||
Reference in new issue
Block a user