I. Component introduction
ElPopper is not included in the official documentation of Element-Plus. ElPopper is used internally in other components, such as Select, Cascade, ColorPicker, etc.
The popover component provided in the official document is ElPopover. The relationship between the two components is very close. The ElPopper component is analyzed here first.
Second, source code analysis
2.1 in popper. Ts
The ElPopper component and its children are written using the Render function.
Popper. ts corresponds to the pop-up content
// packages\components\popper\src\renderers\popper.ts
/ / parameters:
// props: Properties passed in
// children: array of children
export default function renderPopper(props: IRenderPopperProps, children: VNode[],) {
// Decompose the corresponding properties from props
const {
effect,
name,
stopPopperMouseEvent,
popperClass,
popperStyle,
popperRef,
pure,
popperId,
visibility,
onMouseenter,
onMouseleave,
onAfterEnter,
onAfterLeave,
onBeforeEnter,
onBeforeLeave,
} = props
// dynamic class
const kls = [
popperClass,
'el-popper'.'is-' + effect,
pure ? 'is-pure' : ' ',]Stop = (e: Event) => e.topPropagation ()
const mouseUpAndDown = stopPopperMouseEvent ? stop : NOOP
/ / render function
return h(
// Outer component, using vUE official transition component
Transition,
// Attributes, mainly transition events
{
name,
'onAfterEnter': onAfterEnter,
'onAfterLeave': onAfterLeave,
'onBeforeEnter': onBeforeEnter,
'onBeforeLeave': onBeforeLeave,
},
// slots
{
// Default slot
// withCtx withDirectives are vUE official internal directives,
default: withCtx(() = > [withDirectives(
// The inner element, on which some of the attributes passed in are bound
h(
'div',
{
'aria-hidden': String(! visibility),class: kls,
style: popperStyle ?? {},
id: popperId,
ref: popperRef ?? 'popperRef'.role: 'tooltip',
onMouseenter,
onMouseleave,
onClick: stop,
onMousedown: mouseUpAndDown,
onMouseup: mouseUpAndDown,
},
children,
),
// specify v-show="visibility"
[[vShow, visibility]],
)]),
},
)
* <transition :name="name"> * <div v-show="visibility" :aria-hidden="! visibility" :class="kls" ref="popperRef" role="tooltip"@mouseenter= ""@mouseleave= ""@click="">
* <slot />
* </div>
* </transition>
*/
}
Copy the code
Conclusion:
popper.ts
providesrenderPopper
Method that takes two arguments: an array of passed attributes and child elements;renderPopper
Method, the outer layer is usedTransition
The inner layer uses the div tag to bind the corresponding attribute to the layer’s div tag, and the children element passed in as the child element of the layer’s div.
2.2 the trigger. Ts
Trigger. ts corresponds to the trigger part
// packages\components\popper\src\renderers\trigger.ts
interface IRenderTriggerProps extends Record<string, unknown> {
ref: string| Ref<ComponentPublicInstance | HTMLElement> onClick? : EventHandler onMouseover? : EventHandler onMouseleave? : EventHandler onFocus? : EventHandler }/ / parameters:
// 1, array of elements 2, passed attributes
export default function renderTrigger(trigger: VNode[], extraProps: IRenderTriggerProps) {
// Get the first valid VNode
const firstElement = getFirstValidNode(trigger, 1)
// An error message is displayed if there is no valid VNode
if(! firstElement) throwError('renderTrigger'.'trigger expects single rooted node')
// Use vue's cloneVNode API to blend the extraProps and firstElement props
return cloneVNode(firstElement, extraProps, true)}Copy the code
Conclusion:
trigger.ts
The logic is simple: find the first valid VNode and add some additional attributes to the VNode.
2.3 arrow. Ts
Arrow. ts is the arrow part of the frame
// packages\components\popper\src\renderers\arrow.ts
// Parameters: 1, whether to display the arrow
export default function renderArrow(showArrow: boolean) {
// Decide whether to display arrows according to showArrow
return showArrow
? h(
'div',
{
ref: 'arrowRef'.class: 'el-popper__arrow'.'data-popper-arrow': ' ',},null,
)
: h(Comment, null.' ')}Copy the code
The logic of arrow.ts is simpler, depending on the showArrow argument passed in.
2.4 the index. The vue
Having introduced the three child components of ElPopper, let’s now look at the index.vue of the component itself
// packages\components\popper\src\index.vue
export default defineComponent({
name: compName,
props: defaultProps,
emits: [UPDATE_VISIBLE_EVENT, 'after-enter'.'after-leave'.'before-enter'.'before-leave'].setup(props, ctx) {
// If the user does not write trigger slot, an error message is displayed: trigger must be provided
if(! ctx.slots.trigger) { throwError(compName,'Trigger must be provided')}// Call usePopper and return popper status data
const popperStates = usePopper(props, ctx)
// Encapsulate the mandatory destruction method
const forceDestroy = () = > popperStates.doDestroy(true)
// Call initializePopper when mounting
onMounted(popperStates.initializePopper)
// Destroy during uninstallation
onBeforeUnmount(forceDestroy)
// initializePopper is called when keep-alive is activated
onActivated(popperStates.initializePopper)
// Destroy when invalid
onDeactivated(forceDestroy)
// Return status data
return popperStates
},
render() {
// Deconstruct the properties from this, some of which are returned by the setup function and some of which are generated when the VUE component is initialized
const {
$slots,
appendToBody,
class: kls,
style,
effect,
hide,
onPopperMouseEnter,
onPopperMouseLeave,
onAfterEnter,
onAfterLeave,
onBeforeEnter,
onBeforeLeave,
popperClass,
popperId,
popperStyle,
pure,
showArrow,
transition,
visibility,
stopPopperMouseEvent,
} = this
// Whether to trigger mode manually
const isManual = this.isManualMode()
/ / arrow
const arrow = renderArrow(showArrow)
/ / box
const popper = renderPopper(
/ / property
{
effect,
name: transition,
popperClass,
popperId,
popperStyle,
pure,
stopPopperMouseEvent,
onMouseenter: onPopperMouseEnter,
onMouseleave: onPopperMouseLeave,
onAfterEnter,
onAfterLeave,
onBeforeEnter,
onBeforeLeave,
visibility,
},
// children
[
// Default slot contents
renderSlot($slots, 'default', {}, () = > {
return [toDisplayString(this.content)]
}),
/ / arrow
arrow,
],
)
// trigger slot
const_t = $slots.trigger? . ()// trigger props
const triggerProps = {
'aria-describedby': popperId,
class: kls,
style,
ref: 'triggerRef'. this.events, }// Depending on whether the trigger is manually triggered, decide whether to add clickOutside to the trigger
const trigger = isManual
? renderTrigger(_t, triggerProps)
: withDirectives(renderTrigger(_t, triggerProps), [[ClickOutside, hide]])
// Fragment is a built-in Fragment of vue that does not render any nodes
return h(Fragment, null.// children
[
// Trigger element
trigger,
// Teleport is a portal component built into Vue that renders child elements to elements specified by to
// The to value here is body, that is, the part of the popbox is mounted under the body
h(
Teleport as any.// Vue did not support createVNode for Teleport
{
to: 'body'.// If appendToBody is false, the portal is disabled
disabled: !appendToBody,
},
// Popup content
[popper],
),
])
},
})
Copy the code
Conclusion:
- in
setup
Method callusePopper
Method to generate popper state data; And register lifecycle hook functions to be called at mount/activation timeinitializePopper
Method, called when uninstalling/invalidatingdoDestroy(true)
; - in
render
In the function, the corresponding attribute value is first obtained from this by deconstructing the assignment. callrenderArrow/renderPopper/renderTrigger
Generate the corresponding sub-module, pass in the attribute value required by the corresponding module when calling; - use
Teleport
Portal component that mounts the Popper section to the body
2.5 use – in popper/index. Ts
In index.vue above, the userPopper method is called within the setup function. Let’s take a closer look at this method:
// packages\components\popper\src\use-popper\index.ts
export default function(props: IPopperOptions, { emit }: SetupContext
,
[]>) {
// arrow trigger popper
const arrowRef = ref<RefElement>(null)
const triggerRef = ref(null) as Ref<ElementType>
const popperRef = ref<RefElement>(null)
// Generate a unique random ID
const popperId = `el-popper-${generateId()}`
let popperInstance: Nullable<PopperInstance> = null
// Delay the occurrence of the timer, that is, after the display is triggered, delay the display for a period of time
let showTimer: Nullable<TimeoutHandle> = null
// Automatic hiding timer, that is, after displaying the pop-up box, it will automatically hide after a period of time
let hideTimer: Nullable<TimeoutHandle> = null
// Trigger focus state
let triggerFocused = false
// Whether to trigger mode manually
const isManualMode = () = > props.manualMode || props.trigger === 'manual'
// The dynamic style of popper, where PopupManager generates a zIndex value
const popperStyle = ref<CSSProperties>({ zIndex: PopupManager.nextZIndex() })
// Call usePopperOptions to generate popperOptions
const popperOptions = usePopperOptions(props, {
arrow: arrowRef,
})
// Const props. Visible ();
const state = reactive({
visible:!!!!! props.visible, })// visible has been taken by props.visible, avoiding name collision
// Either marking type here or setter parameter
// Use the get and set methods to evaluate the property visibility
const visibility = computed<boolean> ({get() {
if (props.disabled) {
return false
} else {
return isBool(props.visible) ? props.visible : state.visible
}
},
set(val) {
if (isManualMode()) return
isBool(props.visible)
? emit(UPDATE_VISIBLE_EVENT, val)
: (state.visible = val)
},
})
// Internal show method
function _show() {
// autoClose Indicates the delay of automatic hiding after the user control pop-up appears. If the value is 0, it will not be hidden automatically
if (props.autoClose > 0) {
// call _hide when time is up
hideTimer = window.setTimeout(() = > {
_hide()
}, props.autoClose)
}
visibility.value = true
}
// The internal hide method
function _hide() {
visibility.value = false
}
// Clear timers, including showTimer and hideTimer
function clearTimers() {
clearTimeout(showTimer)
clearTimeout(hideTimer)
}
/ / display
const show = () = > {
if (isManualMode() || props.disabled) return
// Clear the timer
clearTimers()
if (props.showAfter === 0) {
_show()
} else {
// Use a timer when there is a delay display
showTimer = window.setTimeout(() = > {
_show()
}, props.showAfter)
}
}
/ / hide
const hide = () = > {
if (isManualMode()) return
// Clear the timer
clearTimers()
if (props.hideAfter > 0) {
// If there is a delay in closing, use a timer
hideTimer = window.setTimeout(() = > {
close()
}, props.hideAfter)
} else {
close()
}
}
/ / close
const close = () = > {
_hide()
if (props.disabled) {
// If disabled, call destruction
doDestroy(true)}}// Mouse over the popper event handler
function onPopperMouseEnter() {
// Trigger in non-click mode, mouse over popper to clear autohide timer
if(props.enterable && props.trigger ! = ='click') {
clearTimeout(hideTimer)
}
}
// Mouse away from the popper event handler
function onPopperMouseLeave() {
const { trigger } = props
const shouldPrevent =
(isString(trigger) && (trigger === 'click' || trigger === 'focus')) ||
(trigger.length === 1 &&
(trigger[0= = ='click' || trigger[0= = ='focus'))
// When trigger is click or focus, it does not trigger the closing of the frame
if (shouldPrevent) return
// Trigger closes the hover, since manual mode is checked in the hide method, so it closes only in hover mode
hide()
}
// Initialize popper
function initializePopper() {
// The $() method is an element-plus custom that fetches the value of a ref
// function $
(ref: Ref
) {
// return ref.value
/ /}
if(! $(visibility)) {// Return in hidden state
return
}
const unwrappedTrigger = $(triggerRef)
// Determine whether the trigger element is an HTML native or a Vue component
// If it is a component, take its $el
const _trigger = isHTMLElement(unwrappedTrigger)
? unwrappedTrigger
: (unwrappedTrigger as ComponentPublicInstance).$el
// Key: createPopper is a method provided by the PopperJS library
// createPopper receives three parameters: 1. Reference, which is the object that triggers the pop-up; Popper: the popper element; PopperOptions: popperbox configuration
popperInstance = createPopper(_trigger, $(popperRef), $(popperOptions))
/ / call the update
popperInstance.update()
}
/ / destroy
function doDestroy(forceDestroy? :boolean) {
/* istanbul ignore if */
if(! popperInstance || ($(visibility) && ! forceDestroy))return
detachPopper()
}
function detachPopper() {
// Call the popperjs destroy methodpopperInstance? .destroy?.() popperInstance =null
}
const events = {} as PopperEvents
// Displays the toggle side effects function
function onVisibilityChange(toState: boolean) {
if (toState) {
// Update zIndex when it becomes visible
popperStyle.value.zIndex = PopupManager.nextZIndex()
// call initializePopper again
initializePopper()
}
}
// When the switch is displayed, the switch side effects function is called
watch(visibility, onVisibilityChange)
// In non-automatic trigger mode
if(! isManualMode()) {// Define the switch display method
const toggleState = () = > {
if ($(visibility)) {
hide()
} else {
show()
}
}
// Define the event handler
const popperEventsHandler = (e: Event) = > {
// Prevent bubbling
e.stopPropagation()
switch (e.type) {
// Click the event
case 'click': {
if (triggerFocused) {
// Set the current focus to non-focus
triggerFocused = false
} else {
// If the current state is not focus, switch the display state
toggleState()
}
break
}
// Mouse enter event
case 'mouseenter': {
/ / show
show()
break
}
// Mouse out event
case 'mouseleave': {
/ / hide
hide()
break
}
/ / focus event
case 'focus': {
// Set it to focus and display it
triggerFocused = true
show()
break
}
// Out-of-focus events
case 'blur': {
// Set it to non-focus and hide it
triggerFocused = false
hide()
break}}}// Bind events according to the trigger type
const triggerEventsMap: Partial<Record<TriggerType, (keyof PopperEvents)[]>> = {
// type of click to bind the onClick event
click: ['onClick'].// hover type, bind onMouseenter onMouseleave event
hover: ['onMouseenter'.'onMouseleave'].// Focus, bind the onFocus onBlur event
focus: ['onFocus'.'onBlur'],}const mapEvents = (t: TriggerType) = > {
triggerEventsMap[t].forEach(event= > {
The popperEventsHandler function handles the event type internally
events[event] = popperEventsHandler
})
}
if (isArray(props.trigger)) {
Object.values(props.trigger).forEach(mapEvents)
} else {
mapEvents(props.trigger as TriggerType)
}
}
// When popperOptions changes, use the popperJS setOptions method to reset and update
watch(popperOptions, val= > {
if(! popperInstance)return
popperInstance.setOptions(val)
popperInstance.update()
})
return {
doDestroy,
show,
hide,
onPopperMouseEnter,
onPopperMouseLeave,
onAfterEnter: () = > {
emit('after-enter')},onAfterLeave: () = > {
detachPopper()
emit('after-leave')},onBeforeEnter: () = > {
emit('before-enter')},onBeforeLeave: () = > {
emit('before-leave')
},
initializePopper,
isManualMode,
arrowRef,
events,
popperId,
popperInstance,
popperRef,
popperStyle,
triggerRef,
visibility,
}
}
Copy the code
UsePopper summary:
usePopper
Method, defined the attributes and methods to maintain the display state of the frame;usePopper
Method to invoke a third-party librarypopperjs
Control the actual display position of the frame;usePopper
Where, istrigger
Bind the corresponding event handler function to control the display and hiding of the frame.