mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-15 18:01:24 +08:00
382 lines
9.2 KiB
TypeScript
382 lines
9.2 KiB
TypeScript
|
import {
|
||
|
cloneVNode,
|
||
|
computed,
|
||
|
Fragment,
|
||
|
getCurrentInstance,
|
||
|
h,
|
||
|
nextTick,
|
||
|
toDisplayString,
|
||
|
toRef,
|
||
|
Transition,
|
||
|
ref,
|
||
|
renderSlot,
|
||
|
withDirectives,
|
||
|
} from 'vue'
|
||
|
import { NOOP } from '@vue/shared'
|
||
|
import { createPopper } from '@popperjs/core'
|
||
|
import { ClickOutside } from '@element-plus/directives'
|
||
|
import {
|
||
|
generateId,
|
||
|
isHTMLElement,
|
||
|
isString,
|
||
|
refAttacher,
|
||
|
} from '@element-plus/utils/util'
|
||
|
import { getFirstValidNode } from '@element-plus/utils/vnode'
|
||
|
import { stop } from '@element-plus/utils/dom'
|
||
|
import PopupManager from '@element-plus/utils/popup-manager'
|
||
|
import throwError from '@element-plus/utils/error'
|
||
|
|
||
|
import useTeleport from '../use-teleport'
|
||
|
import useTimeout from '../use-timeout'
|
||
|
import { useModelToggle } from '../use-model-toggle'
|
||
|
import { useTransitionFallthrough } from '../use-transition-fallthrough'
|
||
|
import { usePopperOptions } from './use-popper-options'
|
||
|
import { useTargetEvents, DEFAULT_TRIGGER } from './use-target-events'
|
||
|
|
||
|
import type {
|
||
|
CSSProperties,
|
||
|
ComponentPublicInstance,
|
||
|
ExtractPropTypes,
|
||
|
PropType,
|
||
|
} from 'vue'
|
||
|
import type {
|
||
|
Instance as PopperInstance,
|
||
|
Options,
|
||
|
Placement,
|
||
|
PositioningStrategy,
|
||
|
} from '@popperjs/core'
|
||
|
import type { RefElement } from '@element-plus/utils/types'
|
||
|
import type { Trigger } from './use-target-events'
|
||
|
|
||
|
export type Effect = 'light' | 'dark'
|
||
|
export type Offset = [number, number] | number
|
||
|
|
||
|
type ElementType = ComponentPublicInstance | HTMLElement
|
||
|
|
||
|
export const DARK_EFFECT = 'dark'
|
||
|
export const LIGHT_EFFECT = 'light'
|
||
|
|
||
|
const DEFAULT_FALLBACK_PLACEMENTS = []
|
||
|
|
||
|
export const usePopperProps = {
|
||
|
// the arrow size is an equailateral triangle with 10px side length, the 3rd side length ~ 14.1px
|
||
|
// adding a offset to the ceil of 4.1 should be 5 this resolves the problem of arrow overflowing out of popper.
|
||
|
arrowOffset: {
|
||
|
type: Number,
|
||
|
default: 5,
|
||
|
},
|
||
|
appendToBody: {
|
||
|
type: Boolean,
|
||
|
default: true,
|
||
|
},
|
||
|
autoClose: {
|
||
|
type: Number,
|
||
|
default: 0,
|
||
|
},
|
||
|
boundariesPadding: {
|
||
|
type: Number,
|
||
|
default: 0,
|
||
|
},
|
||
|
content: {
|
||
|
type: String,
|
||
|
default: '',
|
||
|
},
|
||
|
class: {
|
||
|
type: String,
|
||
|
default: '',
|
||
|
},
|
||
|
style: Object,
|
||
|
hideAfter: {
|
||
|
type: Number,
|
||
|
default: 200,
|
||
|
},
|
||
|
cutoff: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
},
|
||
|
disabled: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
},
|
||
|
effect: {
|
||
|
type: String as PropType<Effect>,
|
||
|
default: DARK_EFFECT,
|
||
|
},
|
||
|
enterable: {
|
||
|
type: Boolean,
|
||
|
default: true,
|
||
|
},
|
||
|
manualMode: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
},
|
||
|
showAfter: {
|
||
|
type: Number,
|
||
|
default: 0,
|
||
|
},
|
||
|
offset: {
|
||
|
type: Number,
|
||
|
default: 12,
|
||
|
},
|
||
|
placement: {
|
||
|
type: String as PropType<Placement>,
|
||
|
default: 'bottom' as Placement,
|
||
|
},
|
||
|
popperClass: {
|
||
|
type: String,
|
||
|
default: '',
|
||
|
},
|
||
|
pure: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
},
|
||
|
// Once this option were given, the entire popper is under the users' control, top priority
|
||
|
popperOptions: {
|
||
|
type: Object as PropType<Options>,
|
||
|
default: () => null,
|
||
|
},
|
||
|
showArrow: {
|
||
|
type: Boolean,
|
||
|
default: true,
|
||
|
},
|
||
|
strategy: {
|
||
|
type: String as PropType<PositioningStrategy>,
|
||
|
default: 'fixed' as PositioningStrategy,
|
||
|
},
|
||
|
transition: {
|
||
|
type: String,
|
||
|
default: 'el-fade-in-linear',
|
||
|
},
|
||
|
trigger: {
|
||
|
type: [String, Array] as PropType<Trigger>,
|
||
|
default: DEFAULT_TRIGGER,
|
||
|
},
|
||
|
visible: {
|
||
|
type: Boolean,
|
||
|
default: undefined,
|
||
|
},
|
||
|
stopPopperMouseEvent: {
|
||
|
type: Boolean,
|
||
|
default: true,
|
||
|
},
|
||
|
gpuAcceleration: {
|
||
|
type: Boolean,
|
||
|
default: true,
|
||
|
},
|
||
|
fallbackPlacements: {
|
||
|
type: Array as PropType<Placement[]>,
|
||
|
default: () => DEFAULT_FALLBACK_PLACEMENTS,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
export const usePopper = () => {
|
||
|
|
||
|
const vm = getCurrentInstance()
|
||
|
const props = vm.props as ExtractPropTypes<typeof usePopperProps>
|
||
|
const { slots } = vm
|
||
|
|
||
|
const arrowRef = ref<RefElement>(null)
|
||
|
const triggerRef = ref<ElementType>(null)
|
||
|
const popperRef = ref<RefElement>(null)
|
||
|
|
||
|
const popperOptions = usePopperOptions(arrowRef)
|
||
|
const popperStyle = ref<CSSProperties>({ zIndex: PopupManager.nextZIndex() })
|
||
|
const visible = ref(false)
|
||
|
const isManual = computed(() => props.manualMode || props.trigger === 'manual')
|
||
|
|
||
|
const popperId = `el-popper-${generateId()}`
|
||
|
let popperInstance: Nullable<PopperInstance> = null
|
||
|
|
||
|
const {
|
||
|
renderTeleport,
|
||
|
showTeleport,
|
||
|
hideTeleport,
|
||
|
} = useTeleport(popupRenderer, toRef(props, 'appendToBody'))
|
||
|
|
||
|
const { show, hide } = useModelToggle({
|
||
|
indicator: visible,
|
||
|
onShow,
|
||
|
onHide,
|
||
|
})
|
||
|
|
||
|
const { registerTimeout, cancelTimeout } = useTimeout()
|
||
|
|
||
|
// event handlers
|
||
|
|
||
|
function onShow() {
|
||
|
popperStyle.value.zIndex = PopupManager.nextZIndex()
|
||
|
nextTick(initializePopper)
|
||
|
}
|
||
|
|
||
|
function onHide() {
|
||
|
hideTeleport()
|
||
|
nextTick(detachPopper)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* The calling mechanism here is:
|
||
|
* when the visibility gets changed, let's say we change it to true
|
||
|
* the delayShow gets called which initializes a global root node for the popper content
|
||
|
* to insert in, then it register a timer for calling the show method, which changes the flag to
|
||
|
* true, then calls onShow method.
|
||
|
* So the order of invocation is: delayedShow -> timer(show) -> set the indicator to true -> onShow
|
||
|
*/
|
||
|
|
||
|
function delayShow() {
|
||
|
if (isManual.value || props.disabled) return
|
||
|
// renders out the teleport element root.
|
||
|
showTeleport()
|
||
|
registerTimeout(show, props.showAfter)
|
||
|
}
|
||
|
|
||
|
function delayHide() {
|
||
|
if (isManual.value) return
|
||
|
registerTimeout(hide, props.hideAfter)
|
||
|
}
|
||
|
|
||
|
function onToggle() {
|
||
|
if (visible.value) {
|
||
|
delayShow()
|
||
|
} else {
|
||
|
delayHide()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function detachPopper() {
|
||
|
popperInstance?.destroy?.()
|
||
|
popperInstance = null
|
||
|
}
|
||
|
|
||
|
function onPopperMouseEnter() {
|
||
|
// if trigger is click, user won't be able to close popper when
|
||
|
// user tries to move the mouse over popper contents
|
||
|
if (props.enterable && props.trigger !== 'click') {
|
||
|
cancelTimeout()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function onPopperMouseLeave() {
|
||
|
const { trigger } = props
|
||
|
const shouldPrevent =
|
||
|
(isString(trigger) && (trigger === 'click' || trigger === 'focus')) ||
|
||
|
// we'd like to test array type trigger here, but the only case we need to cover is trigger === 'click' or
|
||
|
// trigger === 'focus', because that when trigger is string
|
||
|
// trigger.length === 1 and trigger[0] === 5 chars string is mutually exclusive.
|
||
|
// so there will be no need to test if trigger is array type.
|
||
|
(trigger.length === 1 &&
|
||
|
(trigger[0] === 'click' || trigger[0] === 'focus'))
|
||
|
if (shouldPrevent) return
|
||
|
delayHide()
|
||
|
}
|
||
|
|
||
|
function initializePopper() {
|
||
|
if (!visible.value || popperInstance !== null) {
|
||
|
return
|
||
|
}
|
||
|
const unwrappedTrigger = triggerRef.value
|
||
|
const $el = isHTMLElement(unwrappedTrigger)
|
||
|
? unwrappedTrigger
|
||
|
: (unwrappedTrigger as ComponentPublicInstance).$el
|
||
|
popperInstance = createPopper($el, popperRef.value, popperOptions.value)
|
||
|
popperInstance.update()
|
||
|
}
|
||
|
|
||
|
const {
|
||
|
onAfterEnter,
|
||
|
onAfterLeave,
|
||
|
onBeforeEnter,
|
||
|
onBeforeLeave,
|
||
|
} = useTransitionFallthrough()
|
||
|
|
||
|
const events = useTargetEvents(delayShow, delayHide, onToggle)
|
||
|
|
||
|
const arrowRefAttacher = refAttacher(arrowRef)
|
||
|
const popperRefAttacher = refAttacher(popperRef)
|
||
|
const triggerRefAttacher = refAttacher(triggerRef)
|
||
|
|
||
|
// renderers
|
||
|
function popupRenderer() {
|
||
|
const mouseUpAndDown = props.stopPopperMouseEvent ? stop : NOOP
|
||
|
return h(
|
||
|
Transition,
|
||
|
{
|
||
|
name: props.transition,
|
||
|
onAfterEnter,
|
||
|
onAfterLeave,
|
||
|
onBeforeEnter,
|
||
|
onBeforeLeave,
|
||
|
},
|
||
|
{
|
||
|
default: () => () => visible.value ? h('div',
|
||
|
{
|
||
|
'aria-hidden': false,
|
||
|
class: [
|
||
|
props.popperClass,
|
||
|
'el-popper',
|
||
|
`is-${props.effect}`,
|
||
|
props.pure ? 'is-pure' : '',
|
||
|
],
|
||
|
style: popperStyle.value,
|
||
|
id: popperId,
|
||
|
ref: popperRefAttacher,
|
||
|
role: 'tooltip',
|
||
|
onMouseenter: onPopperMouseEnter,
|
||
|
onMouseleave: onPopperMouseLeave,
|
||
|
onClick: stop,
|
||
|
onMousedown: mouseUpAndDown,
|
||
|
onMouseup: mouseUpAndDown,
|
||
|
},
|
||
|
[
|
||
|
renderSlot(slots, 'default', {}, () => [toDisplayString(props.content)]),
|
||
|
arrowRenderer(),
|
||
|
],
|
||
|
) : null,
|
||
|
},
|
||
|
)
|
||
|
}
|
||
|
|
||
|
function arrowRenderer() {
|
||
|
return props.showArrow
|
||
|
? h(
|
||
|
'div',
|
||
|
{
|
||
|
ref: arrowRefAttacher,
|
||
|
class: 'el-popper__arrow',
|
||
|
'data-popper-arrow': '',
|
||
|
},
|
||
|
null,
|
||
|
)
|
||
|
: null
|
||
|
}
|
||
|
|
||
|
function triggerRenderer(triggerProps) {
|
||
|
const trigger = slots.trigger?.()
|
||
|
const firstElement = getFirstValidNode(trigger, 1)
|
||
|
if (!firstElement) throwError('renderTrigger', 'trigger expects single rooted node')
|
||
|
return cloneVNode(firstElement, triggerProps, true)
|
||
|
}
|
||
|
|
||
|
function render() {
|
||
|
|
||
|
const trigger = triggerRenderer({
|
||
|
'aria-describedby': popperId,
|
||
|
class: props.class,
|
||
|
style: props.style,
|
||
|
ref: triggerRefAttacher,
|
||
|
...events,
|
||
|
})
|
||
|
return h(Fragment, null, [
|
||
|
isManual.value
|
||
|
? trigger
|
||
|
: withDirectives(trigger, [[ClickOutside, delayHide]]),
|
||
|
renderTeleport(),
|
||
|
])
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
render,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|