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.tsprovidesrenderPopperMethod that takes two arguments: an array of passed attributes and child elements;
  • renderPopperMethod, the outer layer is usedTransitionThe 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.tsThe 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:

  • insetupMethod callusePopperMethod to generate popper state data; And register lifecycle hook functions to be called at mount/activation timeinitializePopperMethod, called when uninstalling/invalidatingdoDestroy(true);
  • inrenderIn the function, the corresponding attribute value is first obtained from this by deconstructing the assignment. callrenderArrow/renderPopper/renderTriggerGenerate the corresponding sub-module, pass in the attribute value required by the corresponding module when calling;
  • useTeleportPortal 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:

  • usePopperMethod, defined the attributes and methods to maintain the display state of the frame;
  • usePopperMethod to invoke a third-party librarypopperjsControl the actual display position of the frame;
  • usePopperWhere, istriggerBind the corresponding event handler function to control the display and hiding of the frame.