Episode 14: Implementing a set of ui component libraries (Popover pop-up box) of vue on pc from scratch

Episode 14: Implementing a set of ui component libraries (Popover pop-up box) of vue on pc from scratch

1. Location of this collection

Popover component is different from the hegemonic president of alert. It is more inclined to assist in displaying some incomplete content, toast component is more inclined to'prompt'and Popover is more inclined to'display', but it belongs to a kind of'Light display', after all, it will not have the effect of'mask'.
Despite its small size, there are many doors in it. The most important thing is its positioning problem. For example, it is set to appear above the element, but the element itself is at the top. At this time, it needs to show him a new orientation. The calculation method of this positioning can also be applied on other components, such as the next one. The'date component'of the collection and the disappearance time of this pop-up box are also recommended. I recommend that it be cleared as long as it scrolls. The performance of calculating its position is very high, because every time it triggers rearrangement and redrawing. Let's do this little thing together without more words.

Effect Show

2. Demand analysis

  1. Configurable trigger forms, such as'Click'and'hover'.
  2. Define where components appear,'top left','top middle','top right'and so on.
  3. This component may be used in large quantities, and performance optimization needs to be considered.
  4. Remove related events in time

3. Foundation construction

vue-cc-ui/src/components/Popover/index.js

export { default } from './main/index';

vue-cc-ui/src/components/Popover/main/popover.vue

<template>
// Old routine, paternity
  <div class="cc-popover" ref='popover'>
// Content area
      <div class="cc-popover__content" ref='content'>
      // There are two layers here to solve the problems we will encounter later.
        <div class="cc-popover__box">
          <slot name="content"> Please enter the content</slot>
        </div>
      </div>
     // This is the wrapped element.
     // We need to use our popover tag to be effective.
      <slot />
  </div>
</template>
export default {
  name: "ccPopover",
  props: {
    // Event type user self-transmission, this time only supports two modes
    trigger: {
      type: String,
      default: "hover",
      // Here's how to extend it.
      // Only two cases can be optimized to default to hover as long as it is not click
      validator: value => ["click", "hover"].indexOf(value) > -1
    },
    placement: {
    // The range of our orientation is that there are three situations in each direction: start, middle and end.
      type: String,
      default: "right-middle",
      validator(value) {
        let dator = /^(top|bottom|left|right)(-start|-end|-middle)?$/g.test(
          value
        );
        return dator;
      }
    }
  },

Some operations for initializing projects
Adding listening events to dom through user input
In fact, the following on method draws lessons from element-ui.

  mounted() {
    this.$nextTick(() => {
    // Gets the current user-defined event type
      let trigger = this.trigger,
      // This selection operation dom
        popover = this.$refs.popover;
      if (trigger === "hover") {
        // hover, of course, listens for entries and departures.
        on(popover, "mouseenter", this.handleMouseEnter);
        on(popover, "mouseleave", this.handleMouseLeave);
      } else if (trigger === "click") {
        on(popover, "click", this.handlClick);
      }
    });
  },

Encapsulation of on method
element also determines whether or not the server environment and other operations, we only select the browser-side related code.

vue-cc-ui/src/assets/js/utils.js

// Adding events, element-ui determines whether the server environment is
export function on(element, event, handler) {
  if (element && event && handler) {
    element.addEventListener(event, handler, false);
  }
}
// Remove events
export function off(element, event, handler) {
  if (element && event) {
    element.removeEventListener(event, handler, false);
  }
}

4. Talking about Click Events

Assuming that the user's incoming event type is'click', the operation in mounted has bound the corresponding event'handlClick', the next task is:

  1. Let the prompt box appear or disappear.
  2. If so, calculate where to appear.
  3. If so, bind the event for document to hide the popover.

Overview of Thoughts

  1. this.init variable matches v-if to ensure that components will never be rendered without being used.
  2. When it comes to frequent clicks, the v-show is coming on. this.show controls the v-show, so the two commands can work together closely.
  3. Events should not be bound to the body. One possibility is that the user body does not fully wrap content, such as no height.
handlClick() {
      // Anyway, if triggered once, this value will always set v-if to true.
      this.init = true;
      // When he himself is hidden by the css attribute
      if (this.$refs.content && this.$refs.content.style.display === "none") {
        // This must be mandatory. 
        // Otherwise, with subsequent code, bug s can't disappear
        this.$refs.content.style.display = "block";
        this.show = true;
      } else {
      // Except for the first time, they only changed the'true'and'false' of this.show.
        this.show = !this.show;
      }
      // Don't listen to body, because height s may not be 100%;
      // This document can also be specified by the user.
      // Put in a function that makes popover disappear, so it's easy to remove events later.
      this.show && document.addEventListener("click", this.close);
    },

Click on the disappearance event

 close(e) {
    // It's important to determine whether the event source is our popover component or not.
      if (this.isPopover(e)) {
        this.show = false;
        // You can remove it after clicking, and bind it again next time.
        // Because if you bind too many events to document, it will be very card, very card
        document.removeEventListener("click", this.close);
      }
    },

isPopover

  1. This is responsible for determining whether the clicked element is a popover component or not.
  2. Clicking on the elements in the Popover pop-up layer is also a click on popover, because users may pass in some structures through slot s, which cannot be closed.
isPopover(e) {
      let dom = e.target,
        popover = this.$refs.popover,
        content = this.$refs.content;
        // 1: Click on the popover package element to close popover
        // 2: Click on the popover content area element without closing popover
      return !(popover.contains(dom) || content.contains(dom));
    },

It tells us the logic of appearance and disappearance, and then let's make him appear on the screen.

watch: {
   // We will monitor the v-if situation and do this only once when we first render it.
    init() {
      this.$nextTick(() => {
        let trigger = this.trigger,
          dom = this.$refs.content,
          content = this.$refs.content;
          // Someone here will wonder what the ghost writing is.
          // This is because the append operation belongs to clipping, so there are no two elements.
          // In fact, this element has been on the page since it appeared, unless the component is destroyed.
          // When components are destroyed, we will document.body.removeChild(content);
        document.body.appendChild(dom);
        if (trigger === "hover") {
          on(content, "mouseenter", this.handleMouseEnter);
          on(content, "mouseleave", this.handleMouseLeave);
        }
      });
    },
    // This is the method that triggers every hiding display.
    show() {
    // Judge that only when the prompt box is displayed will you go back to the calculation position.
      if (this.show) {
        this.$nextTick(() => {
          let { popover, content } = this.$refs,
            { left, top, options } = getPopoverposition(
              popover,
              content,
              this.placement
            );
          // With coordinates, we can positioning happily.
          this.left = left;
          this.top = top;
          // This configuration determines the location of the'small triangle'.
          this.options = options;
        });
      }
    }
  },

5. Key issues: Get the display location getPopoverposition

thinking

  1. First, whether the experiment can be displayed according to the user's incoming coordinates.
  2. If it is not possible to display according to the coordinates passed in by the user, cycle all display modes to see if there are available schemes.
  3. Getting dom coordinates causes rearrangement and redrawing, so we only do it once.

vue-cc-ui/src/assets/js/vue-popper.js

// Inspired by the vue instantiation part of the vue source code, the following is written.
// CONTANT constant: clearance distance between object and target, unit px;
function getPopoverPosition(popover, content, direction,CONTANT ) {
   // This show is not needed this time to prepare for future components
  let result = { show: true };
  // 1: Let this function initialize'All the parameters involved in the operation';
  // Pay the processed value to the result object
  getOptions(result, popover, content, direction,CONTANT );
  // 2: The offset to get the screen
  let { left, top } = getScrollOffset();
  // 3: The coordinates returned must be for the current visual region.
  result.left += left;
  result.top += top;
  return result;
}

Make all possible lists first. Some people may wonder why not generate list for loops. That's because for loops also need performance. This directly reduces the number of operations, so many unnecessary operations should not be written as much as possible.

const list = [
  'top-end',
  'left-end',
  'top-start',
  'right-end',
  'top-middle',
  'bottom-end',
  'left-start',
  'right-start',
  'left-middle',
  'right-middle',
  'bottom-start',
  'bottom-middle'
];

Parameters required to initialize getOptions

function getOptions(result, popover, content, direction,CONTANT = 10) {
 // 1: It may be called repeatedly, so make a deep copy.
  let myList = list.concat(),
    client = popover.getBoundingClientRect();// Get the visual area distance of popover
 // 2: Every time a pattern is used, it is removed from the list, so that until the array is empty, all possibilities have been tried.
  myList.splice(list.indexOf(direction), 1);
 // 3: Put the parameters in order and pass them to the processing function.
  getDirection(result, {
    myList,
    direction,
    CONTANT,
    top: client.top,
    left: client.left,
    popoverWidth: popover.offsetWidth,
    contentWidth: content.offsetWidth,
    popoverHeight: popover.offsetHeight,
    contentHeight: content.offsetHeight
  });
}

getDirection
There's a little bit of code, but the logic is simple. Let me talk about ideas.

  1. For example, the user passes in'top-end'and splits the top and end fields.
  2. That is, to appear above the target element, to the right.
  3. For end - > result. left = the left distance of the target element from the visual area + the width of the target element - the width of the pop-up box;
  4. For top - > result. top = distance above the target element from the visual area - pop-up height - distance between the two;
  5. There's no complicated logic, just simple arithmetic.
function getDirection(result, options) {
  let {
    top,
    left,
    CONTANT,
    direction,
    contentWidth,
    popoverWidth,
    contentHeight,
    popoverHeight
  } = options;
  result.options = options;
  let main = direction.split('-')[0],
    around = direction.split('-')[1];
  if (main === 'top' || main === 'bottom') {
    if (around === 'start') {
      result.left = left;
    } else if (around === 'end') {
      result.left = left + popoverWidth - contentWidth;
    } else if (around === 'middle') {
      result.left = left + popoverWidth / 2 - contentWidth / 2;
    }
    if (main === 'top') {
      result.top = top - contentHeight - CONTANT;
    } else {
      result.top = top + popoverHeight + CONTANT;
    }
  } else if (main === 'left' || main === 'right') {
    if (around === 'start') {
      result.top = top;
    } else if (around === 'end') {
      result.top = top + popoverHeight - contentHeight;
    } else if (around === 'middle') {
      result.top = top + popoverHeight / 2 - contentHeight / 2;
    }
    if (main === 'left') {
      result.left = left - contentWidth - CONTANT;
    } else {
      result.left = left + popoverWidth + CONTANT;
    }
  }

  testDirection(result, options);
}

TesDirection tests whether the calculated values can appear in the user's field of view
thinking

  1. Calculate the four corners of the pop-up box, whether they are all in the visual area, whether they are not fully displayed.
  2. For example, if the left is negative, there must be a place to hide it.
  3. If it does not meet the requirements, continue to cycle the types in the list and recalculate the location left and top.
  4. If there is no suitable loop to the end, then use the last one.

function testDirection(result, options) {
  let { left, top } = result,
    width = document.documentElement.clientWidth,
    height = document.documentElement.clientHeight;
  if (
    top < 0 ||
    left < 0 ||
    top + options.contentHeight > height ||
    left + options.contentWidth > width
  ) {
    // There are also recyclable ones.
    if (options.myList.length) {
      options.direction = options.myList.shift();
      getDirection(result, options);
    } else {
      // It really can't be on the father's side.
      result.left = options.left;
      result.right = options.right;
    }
  } else {
    result.show = true;
  }
}

dom should be structured with corresponding styles
click here must not be able to use stop modifier, it will interfere with the normal operation of users.
Here we add an animation to make it look a little more beautiful.

<div class="cc-popover"
       ref='popover'>
    
    <!-- Not available stop Prevents user actions -->
    <transition name='fade'>
      <div v-if="init"
           ref='content'
           v-show='show'
           class="cc-popover__content"
           :class="options.direction"
           :style="{ // Here is the key to control positioning.
               top:top+'px',
               left:left+'px'
           }">
        <div class="cc-popover__box">
          <slot name="content"> Please enter the content</slot>
        </div>
      </div>
    </transition>
    <slot />
  </div>

6. hover status

The above is also reflected in the watch. The difference from click is that the events bound are different.
The 200 millisecond delay disappeared here because the user left the target element, possibly to move into the popover pop-up box.

 // Move in
    handleMouseEnter() {
      clearTimeout(this.time);
      this.init = true;
      this.show = true;
    },
    // Move out
    handleMouseLeave() {
      clearTimeout(this.time);
      this.time = setTimeout(() => {
        this.show = false;
      }, 200);
    }

7. Definition of Clearance Directives and Ending Work

vue-cc-ui/src/components/Popover/main/index.js
thinking

  1. Mount the $clearPopover command to hide all popover prompts on the screen
  2. Previous work encountered such a situation, there are two forms, positioned together, one in the top below, the results of the switch, the last form of popover in the second above, so I need a global cleaning method.
  3. Listen for window s scroll events and hide all popover s every time you scroll.
  4. The custom instruction v-scroll-clear-popover, placed on an element, listens for the element's scrolling events, thus hiding the Popover pop-up box.
  5. Of course, all of these methods of monitoring scroll have been throttled and triggered once in 400 milliseconds.
import Popover from './popover.vue';
import prevent from '@/assets/js/prevent';
Popover.install = function(Vue) {
  Vue.component(Popover.name, Popover);
  Vue.prototype.$clearPopover = function() {
    let ary = document.getElementsByClassName('cc-popover__content');
    for (let i = 0; i < ary.length; i++) {
        ary[i].style.display = 'none';
    }
  };
  // Monitoring instructions
  window.addEventListener('scroll',()=>{
    prevent(1,() => {
      Vue.prototype.$clearPopover()
    },400);
  },false)
   
  Vue.directive('scroll-clear-popover', {
    bind: el => {
      el.addEventListener('scroll', ()=>{
        prevent(1,() => {
          Vue.prototype.$clearPopover()
        },400);
      }, false);
    }
  });
};

export default Popover;

Don't underestimate this. If it hadn't been for this closing job, maybe the memory would have exploded.
Remove all events, delete dom elements

  beforeDestroy() {
    let { popover, content } = this.$refs;
    off(content, "mouseleave", this.handleMouseLeave);
    off(popover, "mouseleave", this.handleMouseLeave);
    off(content, "mouseenter", this.handleMouseEnter);
    off(popover, "mouseenter", this.handleMouseEnter);
    off(document, "click", this.close);
    document.body.removeChild(content);
  }

Show me the final result.

end

Everyone can communicate with each other, learn together, make progress together and realize self-worth as soon as possible!!
Next episode talks about Calendar Components

Project github address: github
Personal Technology Blog (Component's Official Website): Technology Blog

Keywords: Javascript Vue Attribute github

Added by Dragoon1 on Mon, 26 Aug 2019 16:18:57 +0300