This article was originally published at: github.com/bigo-fronte… Welcome to follow and reprint.

Demand analysis:

  • Components can be set up placement, support top, bottom
  • Bubble popup pop-up position calculation, boundary calculation, support to set the boundary range
  • Supports clicking on areas outside the popover element, and the popover closes
  • Supports customization of pop-up contents
  • Effect preview:

Implementation scheme

Extend is an extension instance constructor that creates a Vue subclass with initialization options, which can be extended at instantiation time, and finally bound to the element using the $mount method.

Let’s write a simple demo

  • Write the index.vue file
<template> <div v-if="visible" class="popover"> <div ref="arrowDom" :style="{left: `${arrowLeft}px`}" class="popover__arrow"></div> <div class="popover__content"> <div class="popover__content-p1">{{ txt }} </div> </div> </div> </template> <script> export default { name: 'Popover', props: { txt: { type: String, default: }}, data: () => ({arrowLeft: 51, visible: false, positionStyle: {left: '15px', top: '69.578px'}}), methods: { open() { this.visible = ! this.visible; }}}; </script> <style lang="scss"> .popover { position: absolute; } </style>Copy the code

Note: currently the component receives a TXT argument, and the popover position information is fixed by CSS, js only provides the display logic. The bubble popover is absolutely positioned for the body element

  • Write index. Js. When the button is clicked, dynamically appends the popover element to the body
// index.js
import Vue from 'vue';
// Import the index.vue we wrote earlier
import DialogCompt from './index.vue';

let component;
// Ensure that only one component instance exists
const createComponents = function() {
  if(! component) {const DialogConstructor = Vue.extend(DialogCompt);
    component = new DialogConstructor({
      el: document.createElement('div')}); }return component;
};

const preview = (options) = > {
  component = createComponents();
  document.body.appendChild(component.$el);
  component.txt = options.txt;
  component.open();
};

export default preview;
Copy the code
  • Used in the project
import preview from './index';
methods: {
     // Component call
      peview({
        txt: 'I am the word, the word, the word, the word, the word.'
      });
}
Copy the code

After the above three steps, a simple popover will appear, preview the effect:

Handle click interactions

  • If you click on the popover itself, the popover does not disappear

You can determine if the currently clicked object is in the popover range using the Node. Contains method

  • Click on an element outside the popover and the popover disappears

The general approach is to add a click event to the document after the popover. Now let’s modify the index.vue above

<script> Methods: {documentEventHandler() {// If you click on the popover itself, it does not trigger the hidden logic if (! this.$el.contains(evt.target)) { this.close(); } }, close() { this.visible = false; / / remember to remove the click event popups was off the document. The removeEventListener (' click ', enclosing documentEventHandler); }, open() { // ... Enclosing $nextTick (() = > {/ / popup window display, after registration document object for the click event document. The addEventListener (' click ', enclosing documentEventHandler); }); / /... } } </script>Copy the code

Effect preview:

The position calculation

  • Gets information about the target element

This requires that the click object be passed inside the component when it is called

showPop(event) {
  // Component call, pass the event inside the component, convenient component inside to get the target element size information
  review({
    event,
    txt1: 'I am the word, the word, the word, the word, the word.'
  });
}
Copy the code
  • Gets information about the size of the clicked object

With the event object passed in, we get the current currentTarget: event.currenttarget; Then use Element. GetBoundingClientRect () this method obtain the currentTarget, left, right, top, bottom information

// index.js
const preview = (options) = > {
  component = createComponents();
  const event = options.event;
  event.stopPropagation();
  const currentTarget = event.currentTarget;
  const { left, right, bottom, top } = currentTarget.getBoundingClientRect();
  component.targetPosition = { left, right, bottom, top };
  component.placement = options.placement;
  // other code
};
Copy the code
  • Calculate the left and top values of pop-ups

Obtain the left and top values at the upper left corner of the popover according to the annotation above. The idea is as follows:

  • 1. Obtain the distance between the target element and the left, right, top, and bottom edges of the screen
  • 2. According to the idea of horizontally aligning the target element with the popover, calculate the left value, where the left and right boundary issues need to be considered
  • 3. The top value of the popover is calculated according to the height of the popover itself, the height of the arrow, the height of the gap, and the height between the top distance of the target element and the top distance between the bottom distance of the target element and the top distance of the screen. The upper and lower boundary issues also need to be considered here

Left = the distance from the target element to the right of the screen + the target element width / 2 – the width of the popover itself / 2

The same goes for popover top: top = The distance from the top of the target element to the top of the screen – the width of the popover itself – the height of the arrow – the offset

Top = The distance below the target element from the top of the screen + the height of the arrow + the offset

Props: {targetPosition: {type: Object, default: () => ({})}, // The popup position is now the default at the bottom of the trigger element placement: {type: MinLeft: {type: Number, default: 30}, // minRight: {type: Number, default: 30}, // Popover distance the distance between the trigger elements, default 8px offset: {type: Number, default: 8}}, data: () => ({arrowLeft: 0, positionStyle: { left: '-100%', top: '0' } }), method: { open() { // ... This.$nextTick(() => {this.calcPosition(); }); / /... }, calcPosition() {const {width, height} = this.$el.getBoundingClientRect(); const targetWidth = this.targetPosition.right - this.targetPosition.left; // Let popLeft = this.targetPosition.left + targetWidth / 2-width / 2; / / calculate the minimum distance from the right of the screen window displacement const maxLeft = document. The body. The clientWidth - width - this. MinRight * window. Rem / 72; if (popLeft < this.minLeft * window.rem / 72) { popLeft = this.minLeft * window.rem / 72; } else if (popLeft > maxLeft) { popLeft = maxLeft; } const arrowWidth = this.$refs.arrowDom.getBoundingClientRect().width; const arrowHeight = this.$refs.arrowDom.getBoundingClientRect().height; let popTop; If (this. Placement = = = 'bottom') {/ / display the button at the bottom of the popTop = this. TargetPosition. Bottom + this. Offset * window in rem / 72 + arrowHeight; } else {// display on top of button popTop = this.targetPosition.top-height-arrowheight - this.offset * window.rem / 72; } // Arrows are positioned for popovers, If (popLeft === this.minleft * window.rem / 72) {this.arrowLeft = this.targetPosition.left - this.minLeft * window.rem / 72 + targetWidth / 2 - arrowWidth / 2; } else if (popLeft === maxLeft) { this.arrowLeft = this.targetPosition.left + targetWidth / 2 - popLeft - arrowWidth / 2; } else { this.arrowLeft = (popLeft + width - popLeft) / 2 - arrowWidth / 2; } this.positionStyle = { left: `${popLeft}px`, top: `${popTop}px` }; }}Copy the code

During the test, it was found that if the page content was longer than one screen, the positioning of popup would be offset after the page was rolled and the button was clicked. Why?? Oh!!!!!! Originally, we only calculated the distance between the clicked element and the top of the viewport before, but the popover is positioned for the whole body, so the actual top value of the popover also needs to include the scrolling distance of the page. Document. The documentElement. ScrollTop is calculated, remember to do the compatibility, can be reference. The distance between the element and the top of the body should be: the height of the element from the top of the viewport + the height of the container roll

// index.js
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
this.positionStyle = {
  left: `${popLeft}px`.top: `${popTop + scrollTop}px`
};
Copy the code

Now scroll the page, click the button, and the preview popup position is displayed properly.

Write support for custom content popover components

The above implementation can only implement fixed template content, if the later encounter other bubble popover, but when the content is different, this component will have to change to use, so there is another implementation: use named slot to implement popover framework, template, CSS parts can be defined by the caller. Ideally, the user would call this (assuming our component is im-popover) :

<im-popover v-model="visible"> <div><! - the popup window content - > < / div > < button slot = "trigger" > trigger element < / button > < / im - popover >Copy the code

The popover content is the default slot, the button element needs to declare slot=”trigger”

  • To do a basic one, we click on the trigger element to display the popover content
<! --ImPopover.vue--> <template> <div class="im-popover__box" @click="handleClick"> <! <div v-show="visible" ref="popoverDom" :style="positionStyle" class="im-popover"> <div ref="arrowDom" :style="{left: `${arrowLeft}px`}" class="im-popover__arrow" ></div> <! -- Customizable content, default slot --> <slot></slot> </div> <! <slot name="trigger"></slot> </div> </template> <style lang=" SCSS ">. Im-popover {z-index: 1000; position: absolute; max-width: 660px; &__arrow { // ... } &__box {/* Here we need to set the inline element */ display: inline-block; } } </style>Copy the code
  • Write the corresponding script script

This.$el is this.$refs.popoverdom.

<script> export default { name: 'ImPopover', props: { // ... }, methods: { open() { if (! This.visible) {// Append the popover component to the body end this.appendContainer(); / /... }) } else { this.close(); } }, appendContainer() { document.body.appendChild(this.$refs.popoverDom); }, handleClick(event) {// Stop bubbling.stopPropagation (); this.open(); } / /... } } </script>Copy the code
  • Im-popover component call
<template> <im-popover placement="top"> <div class="popover-content"> <img class="image" src="https://static-web.likeevideo.com/as/indigo-static/test/diamond.png" alt=""> <p Class =" TXT "> Primitive script is used by humans to record specific things, simplified images into writing symbols. Writing was pictorial in its early stages of development, </p> </div> <div slot="trigger" class="app__button app__button--small"> custom popover contents </div> </im-popover> </template> <script> import ImPopover from './ImPopOver'; export default { name: 'App', components: { ImPopover } } </script> <style lang="scss"> .popover-content { width: 468px; padding: 12px; box-sizing: border-box; display: flex; align-items: center; flex-direction: column; .image { width: 148px; height: auto; vertical-align: top; } .txt { margin-top: 20px; font-size: 28px; color: #fff; text-align: center; } } </style>Copy the code
  • Results the preview

conclusion

In fact, the popover component involves many functions, such as the configuration of popover trigger mode (such as click, hover), whether the popover content supports scrolling and clicking, popover show and hide callbacks, popover animation and so on. This will wait until later when components need to be extended. If you read this article, hopefully it will help you understand how popover component development works.

Welcome everyone to leave a message to discuss, wish smooth work, happy life!

I’m bigO front. See you next time.