u
This commit is contained in:
1 parent
5a3f484b8b
commit
0172becffb
4 files changed
+675
-106
No files matched your search
@@ -1,31 +1,70 @@
|
|||||||
@use '../_styles/_variables.scss' as *;
|
@use '../_styles/_variables.scss' as *;
|
||||||
@use 'sass:color';
|
@use 'sass:color';
|
||||||
|
|
||||||
$popup-prefix: "#{$prefix}-popup";
|
$popup-prefix: '#{$prefix}-popup';
|
||||||
|
|
||||||
.#{$popup-prefix} {
|
.#{$popup-prefix} {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: #333;
|
background-color: $bg-color-container;
|
||||||
color: #fff;
|
color: $text-color-base;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
box-shadow: $shadow-popup;
|
||||||
|
text-align: center;
|
||||||
&-arrow {
|
&-arrow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
z-index: 1;
|
||||||
|
&::before {
|
||||||
&-arrow::before {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
content: '';
|
content: '';
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background: #333;
|
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
|
// background: $bg-color-container;
|
||||||
|
background: red;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 上方弹出,箭头在底部
|
||||||
|
&[data-popup-placement^='top'] > &-arrow {
|
||||||
|
bottom: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下方弹出,箭头在顶部
|
||||||
|
&[data-popup-placement^='bottom'] > &-arrow {
|
||||||
|
top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左侧弹出,箭头在右侧
|
||||||
|
&[data-popup-placement^='left'] > &-arrow {
|
||||||
|
right: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧弹出,箭头在左侧
|
||||||
|
&[data-popup-placement^='right'] > &-arrow {
|
||||||
|
left: -4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 普通淡入淡出动画 */
|
||||||
|
.popup-fade-enter-from,
|
||||||
|
.popup-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-fade-enter-to,
|
||||||
|
.popup-fade-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-fade-enter-active,
|
||||||
|
.popup-fade-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
@@ -1,140 +1,284 @@
|
|||||||
import type { VNode } from 'vue'
|
import type { PropType, VNode } from 'vue'
|
||||||
import { arrow, offset, useFloating } from '@floating-ui/vue'
|
import { arrow, autoUpdate, flip, offset, useFloating } from '@floating-ui/vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
cloneVNode,
|
cloneVNode,
|
||||||
defineComponent,
|
defineComponent,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
onBeforeUnmount,
|
||||||
onMounted,
|
onMounted,
|
||||||
ref,
|
ref,
|
||||||
Teleport,
|
Teleport,
|
||||||
|
Transition,
|
||||||
watch,
|
watch,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
|
|
||||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'APopup',
|
name: 'APopup',
|
||||||
props: {
|
props: {
|
||||||
trigger: {
|
trigger: {
|
||||||
type: String as () => 'hover' | 'click' | 'focus',
|
type: [String, Array] as PropType<
|
||||||
|
'hover' | 'click' | 'focus' | 'contextMenu' | Array<'hover' | 'click' | 'focus' | 'contextMenu'>
|
||||||
|
>,
|
||||||
default: 'hover',
|
default: 'hover',
|
||||||
},
|
},
|
||||||
|
placement: {
|
||||||
|
type: String as PropType<
|
||||||
|
'top' | 'bottom' | 'left' | 'right'
|
||||||
|
| 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
|
||||||
|
| 'left-start' | 'left-end' | 'right-start' | 'right-end'
|
||||||
|
>,
|
||||||
|
default: 'bottom',
|
||||||
|
},
|
||||||
|
showArrow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
destroyOnClose: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
appendTo: {
|
||||||
|
type: [String, Object] as PropType<string | HTMLElement>,
|
||||||
|
default: 'body',
|
||||||
|
},
|
||||||
|
autoFitPopupMinWidth: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
autoFitPopupWidth: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
popupClass: {
|
||||||
|
type: [String, Array, Object],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
popupStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup(props, { slots }) {
|
setup(props, { slots }) {
|
||||||
const prefixCls = getPrefixCls('popup')
|
const prefixCls = getPrefixCls('popup')
|
||||||
|
|
||||||
const triggerRef = ref<HTMLElement | null>(null)
|
const triggerRef = ref<HTMLElement | null>(null) // 触发 引用
|
||||||
const floatingRef = ref<HTMLElement | null>(null)
|
const floatingRef = ref<HTMLElement | null>(null) // float 引用
|
||||||
const arrowRef = ref<HTMLElement | null>(null)
|
const arrowRef = ref<HTMLElement | null>(null) // 箭头引用
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
|
const hasRendered = ref(false)
|
||||||
|
let cleanup: (() => void) | null = null
|
||||||
|
|
||||||
// 浮层样式和箭头数据
|
const { floatingStyles, middlewareData, placement, update } = useFloating(
|
||||||
const floatingStyles = ref({})
|
|
||||||
const middlewareData = ref({ arrow: {} as any })
|
|
||||||
|
|
||||||
let floatingInstance: ReturnType<typeof useFloating> | null = null
|
|
||||||
|
|
||||||
// 初始化 FloatingUI,确保触发节点 DOM 已挂载
|
|
||||||
const initFloating = async () => {
|
|
||||||
if (!triggerRef.value || floatingInstance)
|
|
||||||
return
|
|
||||||
await nextTick()
|
|
||||||
const { x, y, strategy, middlewareData: md, update } = useFloating(
|
|
||||||
triggerRef,
|
triggerRef,
|
||||||
floatingRef,
|
floatingRef,
|
||||||
{
|
{
|
||||||
placement: 'top',
|
placement: props.placement,
|
||||||
middleware: [offset(10), arrow({ element: arrowRef })],
|
middleware: [offset(10), flip(), arrow({ element: arrowRef })],
|
||||||
},
|
// whileElementsMounted: autoUpdate,
|
||||||
)
|
|
||||||
floatingInstance = { x, y, strategy, middlewareData: md, update }
|
|
||||||
|
|
||||||
// watch 可见性变化,更新位置
|
|
||||||
watch(
|
|
||||||
visible,
|
|
||||||
(v) => {
|
|
||||||
if (v && floatingInstance?.update)
|
|
||||||
floatingInstance.update()
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// watch x/y 变化,实时更新样式
|
// 获取真实DOM
|
||||||
watch([x, y, md], () => {
|
function getDOM(refValue: any): HTMLElement | null {
|
||||||
floatingStyles.value = {
|
if (!refValue)
|
||||||
position: strategy.value,
|
return null
|
||||||
left: `${x.value ?? 0}px`,
|
if (refValue instanceof HTMLElement)
|
||||||
top: `${y.value ?? 0}px`,
|
return refValue
|
||||||
}
|
if (refValue.$el instanceof HTMLElement)
|
||||||
middlewareData.value = md.value
|
return refValue.$el
|
||||||
}, { immediate: true })
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// popup 显隐
|
// 清理监听滚动
|
||||||
const showPopup = () => { visible.value = true; initFloating() }
|
const clearAutoUpdate = () => {
|
||||||
const hidePopup = () => { visible.value = false }
|
if (cleanup) {
|
||||||
const togglePopup = () => { visible.value = !visible.value; initFloating() }
|
cleanup()
|
||||||
|
cleanup = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动监听滚动
|
||||||
|
const setupAutoUpdate = async () => {
|
||||||
|
await nextTick()
|
||||||
|
const triggerEl = getDOM(triggerRef.value)
|
||||||
|
const floatingEl = getDOM(floatingRef.value)
|
||||||
|
if (triggerEl && floatingEl) {
|
||||||
|
clearAutoUpdate()
|
||||||
|
cleanup = autoUpdate(triggerEl, floatingEl, update, {
|
||||||
|
ancestorScroll: true,
|
||||||
|
ancestorResize: true,
|
||||||
|
elementResize: true,
|
||||||
|
layoutShift: true,
|
||||||
|
})
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 浮动框宽度适配
|
||||||
|
const fitFloatingWidth = () => {
|
||||||
|
const triggerEl = getDOM(triggerRef.value)
|
||||||
|
const floatEl = getDOM(floatingRef.value)
|
||||||
|
if (!triggerEl || !floatEl)
|
||||||
|
return
|
||||||
|
if (props.autoFitPopupMinWidth)
|
||||||
|
floatEl.style.minWidth = `${triggerEl.offsetWidth}px`
|
||||||
|
if (props.autoFitPopupWidth)
|
||||||
|
floatEl.style.width = `${triggerEl.offsetWidth}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备浮动框
|
||||||
|
const prepareFloating = async () => {
|
||||||
|
if (!hasRendered.value)
|
||||||
|
hasRendered.value = true
|
||||||
|
fitFloatingWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
const showPopup = async () => {
|
||||||
|
await prepareFloating()
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidePopup = () => {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePopup = async () => {
|
||||||
|
await prepareFloating()
|
||||||
|
visible.value = !visible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事件绑定
|
||||||
|
const triggers = Array.isArray(props.trigger) ? props.trigger : [props.trigger]
|
||||||
const eventProps: Record<string, (e: Event) => void> = {}
|
const eventProps: Record<string, (e: Event) => void> = {}
|
||||||
if (props.trigger === 'hover') {
|
|
||||||
|
if (triggers.includes('hover')) {
|
||||||
eventProps.onMouseenter = showPopup
|
eventProps.onMouseenter = showPopup
|
||||||
eventProps.onMouseleave = hidePopup
|
eventProps.onMouseleave = hidePopup
|
||||||
}
|
}
|
||||||
else if (props.trigger === 'click') {
|
if (triggers.includes('click')) {
|
||||||
eventProps.onClick = togglePopup
|
eventProps.onClick = togglePopup
|
||||||
}
|
}
|
||||||
else if (props.trigger === 'focus') {
|
if (triggers.includes('focus')) {
|
||||||
eventProps.onFocus = showPopup
|
eventProps.onFocus = showPopup
|
||||||
eventProps.onBlur = hidePopup
|
eventProps.onBlur = hidePopup
|
||||||
}
|
}
|
||||||
|
if (triggers.includes('contextMenu')) {
|
||||||
|
eventProps.onContextmenu = (e: Event) => {
|
||||||
|
e.preventDefault()
|
||||||
|
togglePopup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击其他地方关闭
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as Node
|
||||||
|
const triggerEl = getDOM(triggerRef.value)
|
||||||
|
const floatingEl = getDOM(floatingRef.value)
|
||||||
|
if (!triggerEl?.contains(target) && !floatingEl?.contains(target)) {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 Teleport 挂载目标
|
||||||
|
const getTeleportTarget = () => {
|
||||||
|
if (props.appendTo instanceof HTMLElement)
|
||||||
|
return props.appendTo
|
||||||
|
if (typeof props.appendTo === 'string') {
|
||||||
|
const el = document.querySelector(props.appendTo)
|
||||||
|
if (el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
return 'body'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染 trigger slot
|
||||||
|
const renderTriggerSlot = () => {
|
||||||
|
const slotContent = slots.reference?.() || slots.default?.() || []
|
||||||
|
if (slotContent.length === 1)
|
||||||
|
return cloneVNode(slotContent[0], { ref: triggerRef, ...eventProps })
|
||||||
|
return (
|
||||||
|
<span ref={triggerRef} style="display:inline-flex;" {...eventProps}>
|
||||||
|
{slotContent.map(v => cloneVNode(v))}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside)
|
||||||
|
clearAutoUpdate()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(visible, async (val) => {
|
||||||
|
if (val) {
|
||||||
|
setupAutoUpdate()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
clearAutoUpdate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
const slotContent = slots.reference?.() || slots.default?.() || []
|
// 懒加载
|
||||||
let triggerSlot: VNode | null = null
|
const shouldRenderPopup = props.destroyOnClose ? visible.value : hasRendered.value
|
||||||
|
|
||||||
if (slotContent.length === 1) {
|
|
||||||
const child = slotContent[0]
|
|
||||||
const isHoisted = child.patchFlag != null && child.patchFlag === -1
|
|
||||||
|
|
||||||
if (!isHoisted) {
|
|
||||||
triggerSlot = cloneVNode(child, { ref: triggerRef, ...eventProps })
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
triggerSlot = (
|
|
||||||
<span ref={triggerRef} style="display:inline-flex;" {...eventProps}>
|
|
||||||
{slotContent.map(v => cloneVNode(v))}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
triggerSlot = (
|
|
||||||
<span ref={triggerRef} style="display:inline-flex;" {...eventProps}>
|
|
||||||
{slotContent.map(v => cloneVNode(v))}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{triggerSlot}
|
{renderTriggerSlot()}
|
||||||
|
|
||||||
<Teleport to="body">
|
<Teleport to={getTeleportTarget()}>
|
||||||
{visible.value && (
|
{shouldRenderPopup && (
|
||||||
<div ref={floatingRef} class={prefixCls} style={floatingStyles.value}>
|
<div
|
||||||
Popup 内容
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0px',
|
||||||
|
left: '0px',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Transition
|
||||||
|
name="popup-fade"
|
||||||
|
appear
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show={visible.value}
|
||||||
|
ref={floatingRef}
|
||||||
|
class={[prefixCls, props.popupClass]}
|
||||||
|
style={{
|
||||||
|
...floatingStyles.value,
|
||||||
|
position: 'absolute',
|
||||||
|
...props.popupStyle,
|
||||||
|
}}
|
||||||
|
data-popup-placement={placement.value}
|
||||||
|
>
|
||||||
|
{slots.content ? slots.content() : props.content}
|
||||||
|
{props.showArrow && (
|
||||||
<div
|
<div
|
||||||
class={`${prefixCls}-arrow`}
|
class={`${prefixCls}-arrow`}
|
||||||
ref={arrowRef}
|
ref={arrowRef}
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
left:
|
||||||
left: middlewareData.value.arrow?.x != null
|
middlewareData.value.arrow?.x != null
|
||||||
? `${middlewareData.value.arrow.x}px`
|
? `${middlewareData.value.arrow.x}px`
|
||||||
: undefined,
|
: '',
|
||||||
top: middlewareData.value.arrow?.y != null
|
top:
|
||||||
|
middlewareData.value.arrow?.y != null
|
||||||
? `${middlewareData.value.arrow.y}px`
|
? `${middlewareData.value.arrow.y}px`
|
||||||
: undefined,
|
: '',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import type { PropType, VNode } from 'vue'
|
||||||
|
import { arrow, autoUpdate, flip, offset, useFloating } from '@floating-ui/vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
cloneVNode,
|
||||||
|
defineComponent,
|
||||||
|
nextTick,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
Teleport,
|
||||||
|
Transition,
|
||||||
|
watch,
|
||||||
|
} from 'vue'
|
||||||
|
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'APopup',
|
||||||
|
props: {
|
||||||
|
trigger: {
|
||||||
|
type: [String, Array] as PropType<
|
||||||
|
'hover' | 'click' | 'focus' | 'contextMenu' | Array<'hover' | 'click' | 'focus' | 'contextMenu'>
|
||||||
|
>,
|
||||||
|
default: 'hover',
|
||||||
|
},
|
||||||
|
placement: {
|
||||||
|
type: String as PropType<
|
||||||
|
'top' | 'bottom' | 'left' | 'right'
|
||||||
|
| 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end'
|
||||||
|
| 'left-start' | 'left-end' | 'right-start' | 'right-end'
|
||||||
|
>,
|
||||||
|
default: 'bottom',
|
||||||
|
},
|
||||||
|
showArrow: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
destroyOnClose: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
appendTo: {
|
||||||
|
type: [String, Object] as PropType<string | HTMLElement>,
|
||||||
|
default: 'body',
|
||||||
|
},
|
||||||
|
autoFitPopupMinWidth: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
autoFitPopupWidth: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
popupClass: {
|
||||||
|
type: [String, Array, Object],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
popupStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { slots }) {
|
||||||
|
const prefixCls = getPrefixCls('popup')
|
||||||
|
|
||||||
|
const triggerRef = ref<HTMLElement | null>(null) // 触发 引用
|
||||||
|
const floatingRef = ref<HTMLElement | null>(null) // float 引用
|
||||||
|
const innerFloatingRef = ref<HTMLElement | null>(null) // 真正的 float 引用
|
||||||
|
const arrowRef = ref<HTMLElement | null>(null) // 箭头引用
|
||||||
|
const visible = ref(false)
|
||||||
|
const hasRendered = ref(false)
|
||||||
|
|
||||||
|
const { floatingStyles, middlewareData, placement, update } = useFloating(
|
||||||
|
triggerRef,
|
||||||
|
floatingRef,
|
||||||
|
{
|
||||||
|
placement: props.placement,
|
||||||
|
middleware: [offset(10), flip(), arrow({ element: arrowRef })],
|
||||||
|
whileElementsMounted: autoUpdate,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
const prepareFloating = async () => {
|
||||||
|
// 首次渲染标记
|
||||||
|
if (!hasRendered.value)
|
||||||
|
hasRendered.value = true
|
||||||
|
|
||||||
|
// 等待 DOM 更新
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
// 绑定浮动元素引用
|
||||||
|
floatingRef.value = innerFloatingRef.value
|
||||||
|
update() // 先更新位置
|
||||||
|
|
||||||
|
// 获取触发元素的原生 DOM
|
||||||
|
const triggerEl: HTMLElement | null = (() => {
|
||||||
|
const val = triggerRef.value
|
||||||
|
if (!val)
|
||||||
|
return null
|
||||||
|
return (val as any)?.$el || val
|
||||||
|
})()
|
||||||
|
|
||||||
|
// 自动设置宽度或最小宽度
|
||||||
|
if (triggerEl && innerFloatingRef.value) {
|
||||||
|
if (props.autoFitPopupMinWidth) {
|
||||||
|
innerFloatingRef.value.style.minWidth = `${triggerEl.offsetWidth}px`
|
||||||
|
}
|
||||||
|
if (props.autoFitPopupWidth) {
|
||||||
|
innerFloatingRef.value.style.width = `${triggerEl.offsetWidth}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const showPopup = async () => {
|
||||||
|
await prepareFloating()
|
||||||
|
visible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const hidePopup = () => {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePopup = async () => {
|
||||||
|
await prepareFloating()
|
||||||
|
visible.value = !visible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggers = Array.isArray(props.trigger) ? props.trigger : [props.trigger]
|
||||||
|
const eventProps: Record<string, (e: Event) => void> = {}
|
||||||
|
|
||||||
|
if (triggers.includes('hover')) {
|
||||||
|
eventProps.onMouseenter = showPopup
|
||||||
|
eventProps.onMouseleave = hidePopup
|
||||||
|
}
|
||||||
|
if (triggers.includes('click')) {
|
||||||
|
eventProps.onClick = togglePopup
|
||||||
|
}
|
||||||
|
if (triggers.includes('focus')) {
|
||||||
|
eventProps.onFocus = showPopup
|
||||||
|
eventProps.onBlur = hidePopup
|
||||||
|
}
|
||||||
|
if (triggers.includes('contextMenu')) {
|
||||||
|
eventProps.onContextmenu = (e: Event) => {
|
||||||
|
e.preventDefault()
|
||||||
|
togglePopup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击其他地方关闭
|
||||||
|
const onClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as Node
|
||||||
|
const triggerEl = (triggerRef.value as any)?.$el || triggerRef.value
|
||||||
|
const floatingEl = floatingRef.value
|
||||||
|
|
||||||
|
if (!triggerEl?.contains(target) && !floatingEl?.contains(target)) {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', onClickOutside)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', onClickOutside)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Teleport 挂载目标
|
||||||
|
const teleportTarget = () => {
|
||||||
|
if (props.appendTo instanceof HTMLElement)
|
||||||
|
return props.appendTo
|
||||||
|
if (typeof props.appendTo === 'string') {
|
||||||
|
const el = document.querySelector(props.appendTo)
|
||||||
|
if (el)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
return 'body'
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const slotContent = slots.reference?.() || slots.default?.() || []
|
||||||
|
let triggerSlot: VNode | null = null
|
||||||
|
|
||||||
|
if (slotContent.length === 1) {
|
||||||
|
const child = slotContent[0]
|
||||||
|
triggerSlot = cloneVNode(child, { ref: triggerRef, ...eventProps })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
triggerSlot = (
|
||||||
|
<span ref={triggerRef} style="display:inline-flex;" {...eventProps}>
|
||||||
|
{slotContent.map(v => cloneVNode(v))}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 懒加载标志
|
||||||
|
const shouldRenderPopup = props.destroyOnClose ? visible.value : hasRendered.value
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{triggerSlot}
|
||||||
|
|
||||||
|
<Teleport to={teleportTarget()}>
|
||||||
|
{shouldRenderPopup && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0px',
|
||||||
|
left: '0px',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Transition
|
||||||
|
name="popup-fade"
|
||||||
|
appear
|
||||||
|
onAfterLeave={() => {
|
||||||
|
floatingRef.value = null
|
||||||
|
update()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show={visible.value}
|
||||||
|
ref={innerFloatingRef}
|
||||||
|
class={[prefixCls, props.popupClass]}
|
||||||
|
style={{
|
||||||
|
...floatingStyles.value,
|
||||||
|
position: 'absolute',
|
||||||
|
...props.popupStyle,
|
||||||
|
}}
|
||||||
|
data-popup-placement={placement.value}
|
||||||
|
>
|
||||||
|
{slots.content ? slots.content() : props.content}
|
||||||
|
{props.showArrow && (
|
||||||
|
<div
|
||||||
|
class={`${prefixCls}-arrow`}
|
||||||
|
ref={arrowRef}
|
||||||
|
style={{
|
||||||
|
left:
|
||||||
|
middlewareData.value.arrow?.x != null
|
||||||
|
? `${middlewareData.value.arrow.x}px`
|
||||||
|
: '',
|
||||||
|
top:
|
||||||
|
middlewareData.value.arrow?.y != null
|
||||||
|
? `${middlewareData.value.arrow.y}px`
|
||||||
|
: '',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Teleport>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,22 +1,145 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
</script>
|
export default defineComponent({
|
||||||
|
setup() {
|
||||||
|
const placements = [
|
||||||
|
'top',
|
||||||
|
'top-start',
|
||||||
|
'top-end',
|
||||||
|
'right',
|
||||||
|
'right-start',
|
||||||
|
'right-end',
|
||||||
|
'bottom',
|
||||||
|
'bottom-start',
|
||||||
|
'bottom-end',
|
||||||
|
'left',
|
||||||
|
'left-start',
|
||||||
|
'left-end',
|
||||||
|
] as const
|
||||||
|
|
||||||
<template>
|
return { placements }
|
||||||
<div class="page-center">
|
},
|
||||||
<APopup trigger="click">
|
})
|
||||||
<button style="margin-right: 12px;">
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
style="height: 300px; overflow: auto; border: 1px solid #ccc; padding: 20px;"
|
||||||
|
>
|
||||||
|
<div style="height: 600px; position: relative;">
|
||||||
|
<APopup
|
||||||
|
trigger="click" placement="top" auto-fit-popup-min-width content="你好"
|
||||||
|
>
|
||||||
|
<AButton status="primary" style="margin-top: 44px;">
|
||||||
测试
|
测试
|
||||||
</button>
|
</AButton>
|
||||||
</APopup>
|
</APopup>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
<div class="demo-circle">
|
||||||
|
<h3>12方向Popup示例</h3>
|
||||||
|
<div class="circle-container">
|
||||||
|
<div
|
||||||
|
v-for="placement in placements"
|
||||||
|
:key="placement"
|
||||||
|
class="circle-item"
|
||||||
|
:class="placement"
|
||||||
|
>
|
||||||
|
<APopup trigger="click" :placement="placement" content="1234567891">
|
||||||
|
<button>{{ placement }}</button>
|
||||||
|
</APopup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-center {
|
.demo-circle {
|
||||||
|
padding: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center; /* 水平居中 */
|
flex-direction: column;
|
||||||
align-items: center; /* 垂直居中 */
|
align-items: center;
|
||||||
height: 800px; /* 高度撑满整个视口 */
|
}
|
||||||
}
|
|
||||||
</style>
|
.circle-container {
|
||||||
|
position: relative;
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
border: 1px dashed #ccc;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 每个方向的按钮位置 */
|
||||||
|
.circle-item {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上 */
|
||||||
|
.circle-item.top {
|
||||||
|
top: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.circle-item.top-start {
|
||||||
|
top: 0;
|
||||||
|
left: 25%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.circle-item.top-end {
|
||||||
|
top: 0;
|
||||||
|
left: 75%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右 */
|
||||||
|
.circle-item.right {
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
.circle-item.right-start {
|
||||||
|
top: 25%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
.circle-item.right-end {
|
||||||
|
top: 75%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下 */
|
||||||
|
.circle-item.bottom {
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.circle-item.bottom-start {
|
||||||
|
bottom: 0;
|
||||||
|
left: 25%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
.circle-item.bottom-end {
|
||||||
|
bottom: 0;
|
||||||
|
left: 75%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左 */
|
||||||
|
.circle-item.left {
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
.circle-item.left-start {
|
||||||
|
top: 25%;
|
||||||
|
left: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
.circle-item.left-end {
|
||||||
|
top: 75%;
|
||||||
|
left: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in new issue
Block a user