A preface.


This article mainly in macro form to talk about bilibilii side navigation drag components, very suitable for you are gradually learning VUE, appropriate imitation development projects are front-end learning must have skills. Most people know that the interview needs to have their own work, and the most important thing of work is not to cut the page, but: innovation + user experience + performance optimization + technical display. The author is also a front-end white, is groping stage, I explained today is to imitate I think do a good “side navigation bar”, I hope you have harvest. Let’s do it together, light yellow dress, fluffy hair, ye Ye ye!

Components display

This is a side navigation component that mimics the original Bilibili. Part of the effect is shown below:

As can be seen from the renderings, the component has the following functions:

  1. The entry element in the navigation baritemYou can drag and drop, and the thematic structure of the page changes synchronously.
  2. Click on any item elementitem, you can immediately go to the corresponding page location.
  3. When browsing the page, move the item element next to a topicitemIt will also correspond to it.

2. Specific explanation

  • According to requirements: This article will briefly write h5 and CSS, focusing on how to achieve real-time scrolling navigation and drag.

Gets the topic name and its associated data

1. First of all, we need to get data from VUEX to display the topic name, drag and drop and other functionssortValues,sortKeysAs well assortIds,vuex requests the API provided by Bilibili. The specific process is ignored for the moment, part of the code is as follows (because this is a full stack project, and this component is the most associated with other components, so the author is a little difficult to explain, but I hope you understand, the guthub address will be attached at the end of the article) :

import { contentApi, contentrankApi } from '@/api'
import * as TYPE from '.. /actionType/contentType' // Use actionType to facilitate development and management

const state = {
   // Sort by default
 sortKeys: ['douga'.'bangumi'.'music'.'dance'.'game'.'technology'.'life'.'kichiku'.'fashion'.'ad'.'ent'.'movie'.'teleplay']. sortIds: [1.13.3.129.4.36.160.119.155.165.5.23.11]. sortValues: ['animated'.'were'.'music'.'dance'.'games'.'technology'.'life'.'ghost animals'.'fashion'.'advertising'.'entertainment'.'movie'.'TV show']. rows: []. ranks: []. rank: {} }  const getters = {  rows: state= > state.rows,  sortKeys: state= > state.sortKeys,  sortIds: state= > state.sortIds,  ranks: state= > state.ranks,  rank: state= > state.rank,  sortValues: state= > state.sortValues }  const actions = {  getContentRows({commit, state, rootState}) {  rootState.requesting = true  commit(TYPE.CONTENT_REQUEST)  contentApi.content().then((response) = > {  rootState.requesting = false  commit(TYPE.CONTENT_SUCCESS, response)  }, (error) => {  rootState.requesting = false  commit(TYPE.CONTENT_FAILURE)  })  },  getContentRank({commit, state, rootState}, categoryId) {  console.log(categoryId)  rootState.requesting = true  commit(TYPE.CONTENT_RANK_REQUEST)  let param = {  categoryId: categoryId  }  contentrankApi.contentrank(param).then((response) = > {  rootState.requesting = false  if (categoryId === 1) {  console.log(response)  }  commit(TYPE.CONTENT_RANK_SUCCESS, response)  }, (error) => {  rootState.requesting = false  commit(TYPE.CONTENT_RANK_FAILURE)  })  } } const mutations = {  [TYPE.CONTENT_REQUEST] (state) {   },  [TYPE.CONTENT_SUCCESS] (state, response) {  for (let i = 0; i < state.sortKeys.length; i++) {  let category = state.sortKeys[i]  let rowItem = {  category: category,  categoryId: state.sortIds[i],  name: state.sortValues[i],  b_id: `b_${category}`. item: Object.values(response[category])  }  state.rows.push(rowItem)  }  },  [TYPE.CONTENT_FAILURE] (state) {   },   // List information  [TYPE.CONTENT_RANK_REQUEST] (state) {   },  [TYPE.CONTENT_RANK_SUCCESS] (state, response) {  state.ranks.push(response)  state.rank = response  },  [TYPE.CONTENT_RANK_FAILURE] (state) {   } }  export default {  state,  getters,  actions,  mutations }  Copy the code

2. The next thing we need to do is initialize the data. The author first on the code to explain, the code is as follows:

import { mapGetters } from "vuex";
export default {
  mixins: [scrollMixin],
  data() {
    return {
 current: 0.// The sequence number of the currently selected item  data: [], / / data (name, element, offsetTop, height)  time: 800.// Animation time  height: 32.// The height of a single element  isSort: false.// Sort mode  scrollTop: 0.// Distance from the top of the page  dragId: 0.// Drag and drop the element number  isDrag: false.// Whether you are currently dragging  offsetX: 0.// The offset of the mouse pointer on the x-coordinate of the element to be dragged  offsetY: 0.// The offset of the mouse on the Y coordinate of the element to be dragged  x: 0.// The offset of the dragged element in the x-coordinate of its relative element  y: 0 // The offset of the dragged element in the Y coordinate of its relative element  };  },  Copy the code

First of all, we write all the data we need to realize the requirements in data, all the simple initialization. For example, we need to realize the page scrolling items follow the topic, we need to obtain the item’s serial number, name, element and height from the top of the page, etc. To realize the dragging of items, it is necessary to obtain whether to participate in the dragging state, which item is being dragged, all need to obtain the dragged item serial number and some data of the mouse.

It is not enough just to initialize the data above, to achieve the requirements must be compatible with all browsers, the size of the entire web page, width and height data and real-time monitoring of the mouse operation. Author first code:

methods: {
    /** Initialize */
    init() {
      this.initData(); / / initialization
      this.bindEvent();
 this._screenHeight = window.screen.availHeight; // Return the current screen height (blank space)  this._left = this.$refs.list.getBoundingClientRect().left;The // method returns the size of the element and its position relative to the viewport.  this._top = this.$refs.list.getBoundingClientRect().top;  },  /** Bind event */  bindEvent() {  document.addEventListener("scroll".this.scroll, false);  document.addEventListener("mousemove".this.dragMove, false);// The Mousemove event is triggered when the pointer device (usually the mouse) moves over the element.  document.addEventListener("mouseup".this.dragEnd, false);// The event is emitted when the pointer device button is lifted.  document.addEventListener("mouseleave".this.dragEnd, false);// The mouseleave event is raised when the pointer of a pointing device (usually a mouse) moves out of an element.  // Mouseleave and Mouseout are similar, but the difference is that mouseleave does not bubble while Mouseout does.  // This means that mouseleave is triggered when the pointer leaves the element and all its descendants, and mouseout is triggered when the pointer leaves the element or its descendants (even if the pointer is still inside the element).  },  /** Initializes data */  initData() {  // Convert this.options.items to a new array this.data  this.data = Array.from(this.options.items, item => {  let element = document.getElementById(item.b_id);  if(! element) { console.error(`can not find element of name is ${item.b_id}`);  return;  }  let offsetTop = this.getOffsetTop(element);  return {  name: item.name,  element: element,  offsetTop: offsetTop,// Returns the distance of the current element relative to the top of its offsetParent element.  height: element.offsetHeight// It returns the pixel height of the element. The height contains the element's vertical inner margin and border, and is an integer.  };  });  },  // Gets the distance from the top of the element  getOffsetTop(element) {  let top,  clientTop,  clientLeft,  scrollTop,  scrollLeft,  doc = document.documentElement,// Return the element  body = document.body;  if (typeofelement.getBoundingClientRect ! = ="undefined") {  top = element.getBoundingClientRect().top;  } else {  top = 0;  }  clientTop = doc.clientTop || body.clientTop || 0;// Indicates the width of the top border of an element  scrollTop = window.pageYOffset || doc.scrollTop;// Returns the Y position of the current page relative to the upper left corner of the window display area. Browser compatibility  return top + scrollTop - clientTop;  },  } Copy the code
  • Init () : open in a browser may be full screen or small window, at this time the size of the page height will change, we must each time when the browser window size changes, to obtain (initialization), the height of the current screen and the position of the each item element relative to the window, only such ability can be in different circumstances, don’t make a mistake, changes in real time. usescreen.availHeight.availHeightTo get the screen height, usegetBoundingClientRect()Method to get the position of the item element relative to the window, as shown in the figure below.

  • BindEvent () : This method is used to bind events, or listen, for mouse actions and scrolling. This is the key to real-time change. One of the methods that I want to highlight is that we usemouseleave, instead of usingmouseoutWhen the item element falls off the sidebar, it will not be displayed (display GIF below) because it is triggeredmouseleaveThis method is triggered when the mouse moves away from its parent component. Do not usemouseoutBecause this method fires when it leaves its element and it fires when it leaves its parent element, it’s bubbling. Here we must use accurate, if you still a little confused, you can try the comparison demo on MDNDemo Documents.

  • InitData (): converts this.options.items to a new array this.data that returns the name, the element itself, the distance of the element from the top of its offsetParent element, and the element’s pixel height, which includes the element’s vertical inner margin and border.

  • GetOffsetTop () : Retrieves the distance between the element and the top. Return top + scrolltop-clientTop; The height of the element itself plus the height increased by scrolling minus a repeated top border height is the actual height of the element.

3. Now we are going to start to implement the first function, click on the item element, the page moves to the corresponding position, we want to implement this function is easy, just get the position and corresponding item elementindexYou can do that, but for smooth scrolling you need to introducesmooth-scroll.jsThe code is as follows:

        <div
          class="n-i sotrable"
          :class="[{'on': current===index && !isSort}, {'drag': isDrag && current === index}]"
          @click="setEnable(index)"
 @mousedown="dragStart($event, index)"  :style="dragStyles"  :key="index"  >  <div class="name">{{item.name}}</div>  </div>   <div class="btn_gotop" @click="scrollToTop(time)"></div>    setEnable(index) {  if (index === this.current) {  return false;  }  this.current = index;  let target = this.data[index].element;  this.scrollToElem(target, this.time, this.offset || 0).then(() => {});  }, Copy the code

smooth-scroll.js

window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame

const Quad_easeIn = (t, b, c, d) = > c * ((t = t / d - 1) * t * t + 1) + b

const scrollTo = (end, time = 800) = > {
 let scrollTop = window.pageYOffset || document.documentElement.scrollTop  let b = scrollTop  let c = end - b  let d = time  let start = null   return new Promise((resolve, reject) = > {  function step(timeStamp) {  if (start === null) start = timeStamp  let progress = timeStamp - start  if (progress < time) {  let st = Quad_easeIn(progress, b, c, d)  document.body.scrollTop = st  document.documentElement.scrollTop = st  window.requestAnimationFrame(step)  }  else {  document.body.scrollTop = end  document.documentElement.scrollTop = end  resolve(end)  }  }  window.requestAnimationFrame(step)  }) }  const scrollToTop = (time) = > {  time = typeof time === 'number' ? time : 800  return scrollTo(0, time) }  const scrollToElem = (elem, time, offset) = > {  let top = elem.getBoundingClientRect().top + ( window.pageYOffset || document.documentElement.scrollTop ) - ( document.documentElement.clientTop || 0 )  return scrollTo(top - (offset || 0), time) }  export default {  methods: {  scrollToTop,  scrollToElem,  scrollTo  } }  Copy the code

As for smooth-scroll.js, the author recommends that you check it out.

4. The following code is used to implement the following correspondence of item elements when the page is scrolling:

     / / offset value
    offset() {
      return this.options.offset || 100;
    },
     /** scroll events */
 scroll(e) {  this.scrollTop =  window.pageYOffset ||  document.documentElement.scrollTop + document.body.scrollTop;// Browser compatible, returns the Y position of the current page relative to the upper left corner of the window display area  if (this.scrollTop >= 300) {  this.$refs.navSide.style.top = "0px";  this.init();  } else {  this.$refs.navSide.style.top = "240px";  this.init();  }  // console.log(" distance from top "+ this.scrollTop);  // Track page scrolling in real time  for (let i = 0; i < this.data.length; i++) {  if (this.scrollTop >= this.data[i].offsetTop - this.offset) {  this.current = i;  }  }  },  Copy the code

Here we can see that we used the data inside the initialization, and then the key to scrolling is to get the distance and offset of the element from the window. One detail to note is that when “scrolling elements are more than 300px away from the top of the window”, the entire component will be pulled to the top.

5. Implement drag and drop

  1. Go into sort mode

  <div class="nav-side" :class="{customizing: isSort}" ref="navSide">  <! -- Default not to sort -->
    <transition name="fade">
      <div v-if="isSort">
        <div class="tip"></div>
        <div class="custom-bg"></div>
 </div>  </transition>  </div> // Enter sort mode sort() { this.isSort = ! this.isSort; this.$emit("change");  },   .fade-enter-actice, .fade-leave-active { The transition: opacity 0.3 s; }   .fade-enter, .fade-leave-active {  .tip {  top: 50px;  opacity: 0;  }   .custom-bg {  top: 150px;  left: -70px;  height: 100px;  width: 100px;  opacity: 0;  }  } } Copy the code

As you can see from the above code, entering the sorting mode of the code is relatively simple, mainly by THE CSS animation to achieve.

2. Start dragging

/** get the mouse position */
    getPos(e) {
      this.x = e.clientX - this._left - this.offsetX;
      this.y = e.clientY - this._top - this.offsetY;
    },
/** Drag starts */  dragStart(e, i) {  if (!this.isSort) return false;  this.current = i;  this.isDrag = true;  this.dragId = i;  this.offsetX = e.offsetX;  this.offsetY = e.offsetY;  this.getPos(e);  }, Copy the code

At the beginning of dragging, it is necessary to determine whether the sorting is entered, and then the dragging can be carried out. At this time, the selected position of the mouse, the position of the element and the corresponding ID are obtained.

3. Drag and drop

<template v-for="(item, index) in data" >
        <div
          v-if="isDrag && index === replaceItem && replaceItem <= dragId"
          class="n-i sotrable"
 :key="item.name"  >  <div class="name"></div>  </div>  <div  class="n-i sotrable"  :class="[{'on': current===index && !isSort}, {'drag': isDrag && current === index}]"  @click="setEnable(index)"  @mousedown="dragStart($event, index)"  :style="dragStyles"  :key="index"  >  <div class="name">{{item.name}}</div>  </div>  <div  v-if="isDrag && index === replaceItem && replaceItem > dragId"  class="n-i sotrable"  :key="item.name"  >  <div class="name"></div>  </div> </template>   // The position of the dragged element becomes absolute, and dragStyles is used to set its position, which is called when the mouse moves dragStyles() {  return {  left: `${this.x}px`,  top: `${this.y}px`  };  }, // This causes the replaceItem to send changes when the dragged element moves to the position of another element replaceItem() {  let id = Math.floor(this.y / this.height);  if (id > this.data.length - 1) id = this.data.length;  if (id < 0) id = 0;  return id;  }  /** drag */ dragMove(e) {  if (this.isDrag) {  this.getPos(e);  }  e.preventDefault(a); // This method will notifyWebBrowsers do not perform default actions associated with events (if such actions exist) }, Copy the code

When entering drag, the first thing is to determine whether the mouse position of the element to be dragged is obtained. If not, drag cannot be carried out, so use e.preventDefault() to inform the browser not to carry out drag. Then use dragStyles() to get the real-time location of the element drag. Finally, when an element is dragged, it changes the position of the other element. When the position changes, its corresponding ID changes. We do this by replaceItem().

  1. Drag and drop to complete

    /** Drag ends */
    dragEnd(e) {
      if (this.isDrag) {
        this.isDrag = false;
        if (this.replaceItem ! = =this.dragId) {
 this.options.items.splice(  this.replaceItem,  0. this.options.items.splice(this.dragId, 1) [0]  );  } else {  this.setEnable(this.dragId, true);  } Copy the code

The neat thing about this code is that it first checks if it’s still dragging if it is, this.isDrag = false; Stop dragging and dropping, and then the core makes clever use of splice, if this.replaceItem! = = this. DragId, in this. Add replaceItem behind this. Options. The items. The splice (enclosing dragId, 1) [0], namely the initial id, drag and drop elements is equal to the drag and drop is not successful, return to its original position, or drag the success. Let me use a GIF to demonstrate.



Finally, today is Tomb-sweeping Day, a day when we deeply remember the martyrs and compatriots who lost their lives due to COVID-19.

Add the following CSS to the global, as shown in tuitui

  #app 
    filter grayscale(100%).    -webkit-filter grayscale(100%).    -moz-filter grayscale(100%).    -ms-filter grayscale(100%). -o-filter grayscale(100%). filter url("data:image/svg+xml;utf8, <svg xmlns= \ 'http: / /www.w3.org/ 2000 /svg\ '> <filter id= \ 'grayscale\ '> <feColorMatrix type= \ 'matrix\ 'values= \ '03333. 03333. 03333. 0 0 03333. 03333. 03333. 0 0 03333. 03333. 03333.0 0 0 0 1 0\'/></filter></svg>#grayscale")  filter progid:DXImageTransform.Microsoft.BasicImage(grayscale= 1) -webkit-filter: grayscale(1) Copy the code

Effect:



The end of the

The article sees now also end, if there is a mistake, troublesome everybody points out to me! If you feel good, don’t forget to click 👍 to go oh! I hope you are reading the article -P, refueling, persistence is more terrible than hard work, let us go together with the sword.

Finally, attach the Github address

  • Source code address: Bilibili

Personal Blog Address

  • Blog address: Xiao Yang’s blog

Looking forward to

  • The author is looking for spring recruitment internship in the third year, looking forward to the big guy’s favor ~