Drag,drag,drag! Pull out the Bili Bili side navigation component

Posted by brauchi on Mon, 06 Apr 2020 10:11:53 +0200

I. Preface

This article mainly talks about the side navigation and drag components of bleblebleblebler in a macro form, which is very suitable for you who are learning VUE gradually. A proper imitation development project is a necessary skill for front-end learning. Most people know that the interview needs to have their own works, and the most important thing is not to cut the page, but: Innovation + user experience + performance optimization + technology display. The author is also a front-end Xiaobai, in the exploratory stage. What I'm going to talk about today is to imitate the side navigation bar that I think is good. I hope you can get something. Let's come together, light yellow dress, fluffy hair, drag!

Component display

This is a side navigation bar component that imitates the old Bili Bili. Some effects are as follows:

According to the renderings, the components have the following functions:

  1. The item element in the navigation bar can be dragged and the page theme structure can be changed synchronously.
  2. Click any item element to immediately go to its corresponding page location.
  3. When you browse the page and move a topic, the item element next to it also corresponds to it.

2, Specific explanation

  • According to the requirements: This paper will briefly describe the compilation of h5 and css, focusing on how to realize real-time scrolling navigation and dragging.

Get topic name and related data

1. First of all, we need to go to vuex to get the data, complete the display of topic name, drag and other functions. We need sortValues, sortKeys and sortids. Vuex can get the data by requesting the api provided by the official of bilibilibili. The specific process is ignored for the moment. Some codes are as follows (because this is a full stack project, and this component is most related to other components, so the author is a little difficult to explain, and I hope to understand more. The guthub address will be attached at the end of the article):

import { contentApi, contentrankApi } from '@/api'
import * as TYPE from '../actionType/contentType' //Easy to develop and manage with actionType

const state = {
   // Default sort
   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: ['animation', 'Folk Opera', 'Music', 'Dance', 'Game', 'science and technology', 'Life', 'Ghost animal', 'fashion', 'Advertisement', 'entertainment', 'Film', 'TV drama'],
   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) {

   },

   // Leaderboard 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 code

2. Next, we need to initialize the data. The code is as follows:

import { mapGetters } from "vuex";
export default {
  mixins: [scrollMixin],
  data() {
    return {
      current: 0, //Serial number of the currently selected item
      data: [], //Name, element, offsettop, height
      time: 800, //Animation time
      height: 32, //Height of individual elements
      isSort: false, //Ranking mode
      scrollTop: 0, //Distance from top of page
      dragId: 0, //Drag element number
      isDrag: false, //Drag or not
      offsetX: 0, //The offset of the mouse on the X coordinate of the element to be dragged
      offsetY: 0, //Offset of the mouse on the Y coordinate of the element to be dragged
      x: 0, //The X-coordinate offset of the dragged element on its relative element
      y: 0 //The offset of the dragged element on its relative Y coordinate
    };
  },

Copy code

First of all, we write all the data we need to implement the requirements, all the simple initialization in data. For example, when we need to implement the page scrolling, we need to obtain the item number, name, element and the height from the top of the page. In order to drag items, you need to obtain whether to participate in the dragging state, which item is being dragged, all the item serial numbers to be dragged and some data of the mouse.

It is not enough to initialize the data just like above. In order to realize the requirements, we must obtain the size, width and height data of the whole web page and monitor the operation of the mouse in real time when all browsers are compatible. Author first code:

methods: {
    /** Initialization */
    init() {
      this.initData(); //Initialization
      this.bindEvent();
      this._screenHeight = window.screen.availHeight; //Return to current screen height (blank space) 
      this._left = this.$refs.list.getBoundingClientRect().left;//Method returns the size of the element and its position relative to the viewport.
      this._top = this.$refs.list.getBoundingClientRect().top;
    },
    /** Binding events */
    bindEvent() {
      document.addEventListener("scroll", this.scroll, false);
      document.addEventListener("mousemove", this.dragMove, false);//The mousemove event is triggered when a pointing device (usually a mouse) moves over an element.
      document.addEventListener("mouseup", this.dragEnd, false);//Event triggered when the pointing device button is raised.
      document.addEventListener("mouseleave", this.dragEnd, false);//The mouseleave event is triggered when the pointer of a pointing device (usually a mouse) moves out of an element.
      //Mouseleave and mouseout are similar, but they are different in that mouseleave does not bubble and 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 leaves its descendants (even if the pointer is still inside the element).
    },
    /** Initialize 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 from the top of its offsetParent element.
          height: element.offsetHeight//It returns the pixel height of the element, which contains the vertical inner margin and border of the element, and is an integer.
        };
      });
    },
    //Get the distance from the element to the top
    getOffsetTop(element) {
      let top,
        clientTop,
        clientLeft,
        scrollTop,
        scrollLeft,
        doc = document.documentElement,//Return element
        body = document.body;
      if (typeof element.getBoundingClientRect !== "undefined") {
        top = element.getBoundingClientRect().top;
      } else {
        top = 0;
      }
      clientTop = doc.clientTop || body.clientTop || 0;//Represents the width of the top border of an element.border
      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 compatible
      return top + scrollTop - clientTop;
    },
   }
Copy code
  • init(): it may be a full screen or a small window when it is opened in the browser. At this time, the size and height of the page will change. We must retrieve (initialize) the height of the current screen and the position of each entry element relative to the window every time the size of the browser window changes. Only in this way, we can make real-time changes without errors under different circumstances. Use screen.availHeight.availHeight to get the screen height, and getBoundingClientRect() to get the position of the entry element relative to the window, as shown in the following figure.

  • bindEvent(): this method writes event binding for mouse operation and scrolling behavior, or listening, which is the key to real-time change. In this method, I'd like to say that we use mouseleave instead of mouseout. The reason is that when we need to implement drag, when the item element is out of the sidebar, this element will not be displayed (the display diagram will be placed below), because mouseleave is triggered. This method is triggered when the mouse leaves its parent component. Mouseout is not used because this method will trigger when leaving the element's own position. It will also trigger when leaving its parent element. It is triggered by bubbling. Here we must use it accurately. If you still don't understand it, you can try the demo on MDN demo document.

  • initData(): convert this.options.items into a new array this.data, and return the name, the element itself, the distance between the element and the top of its offsetParent element, and the pixel height of the element. The height includes the vertical inner margin and border of the element.

  • getOffsetTop(): get the distance between the item element and the top. Here, the author does not recommend more than one article Learning notes on properties of JavaScript, such as scrollTop, scrollHeight, offsetTop, offsetHeight . It needs to be explained that return top + scrollTop - clientTop; the height of the element itself plus the height of the scrolling increase minus the height of a repeated top border is the actual height of the element.

3. Now we are going to start to realize the first function. Click the item element, and the web page moves to the corresponding position. We want to realize this function easily, as long as we get the position and index of the corresponding item element, we can achieve it. But to achieve smooth rolling, we need to introduce the code of smooth-scroll.js 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 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 code

With regard to smooth-scroll.js, the author recommends checking the data by himself. There are many.

4. When the page scrolls, the entry element follows the corresponding code as follows:

     //  Offset value
    offset() {
      return this.options.offset || 100;
    },
     /** Rolling events */
    scroll(e) {
      this.scrollTop =
        window.pageYOffset ||
        document.documentElement.scrollTop + document.body.scrollTop;//Browser compatible, return 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);
      //Real time tracking page scrolling
      for (let i = 0; i < this.data.length; i++) {
        if (this.scrollTop >= this.data[i].offsetTop - this.offset) {
          this.current = i;
        }
      }
    },

Copy code

As we can see here, we used the initialization data, and the key to scrolling is to get the distance and offset value from the element to the window. One detail to note is that when the distance between the element and the top of the window is greater than 300px during scrolling, the whole component will top up.

5. Drag

  1. Enter sorting mode
  <div class="nav-side" :class="{customizing: isSort}" ref="navSide">  <!--No sorting by default-->
    <transition name="fade">
      <div v-if="isSort">
        <div class="tip"></div>
        <div class="custom-bg"></div>
      </div>
    </transition>
 </div>
 //Enter sorting mode
    sort() {
      this.isSort = !this.isSort;
      this.$emit("change");
    },
    
    .fade-enter-actice, .fade-leave-active {
    transition: opacity 0.3s;
  }
  
  .fade-enter, .fade-leave-active {
    .tip {
      top: 50px;
      opacity: 0;
    }

    .custom-bg {
      top: 150px;
      left: -70px;
      height: 100px;
      width: 100px;
      opacity: 0;
    }
  }
}
Copy code

It can be seen from the above code that the code to enter the sorting mode is relatively simple, which is mainly implemented by css animation.

2. Start dragging

/** Get mouse position */
    getPos(e) {
      this.x = e.clientX - this._left - this.offsetX;
      this.y = e.clientY - this._top - this.offsetY;
    },
/** Drag and drop start */
    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 code

When you start dragging, you need to judge whether you have entered the sorting. Only when you have entered can you drag. At this time, you can get the location of the selected mouse, the location of the element, and the corresponding id.

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 will become absolute, and dragstyles is used to set its position, which will be called when the mouse moves, so as to follow the mouse movement
    dragStyles() {
      return {
        left: `${this.x}px`,
        top: `${this.y}px`
      };
    },
    //When the dragged element moves to the position of other elements, the replaceItem will send changes
    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 and drop */
    dragMove(e) {
      if (this.isDrag) {
        this.getPos(e);
      }
      e.preventDefault();//This method tells the Web browser not to perform the default action associated with the event, if one exists
    },
Copy code

When entering the drag, the first thing is to determine whether the mouse position of the element to be dragged is obtained. If it is not obtained, it will not be able to drag, then use e.preventDefault() to inform the browser not to drag. Then use dragStyles() to get the real-time location of the element drag. Finally, when the element is dragged, the position of other elements will be changed, and the corresponding id will be changed. We use replaceItem() to achieve this. In this method, we magically divide the real-time height of the element and the height of the element itself to obtain the dynamic id.

  1. Drag and drop to complete
    /** Drag end */
    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 code

This code is ingenious. First, judge whether the drag is still in progress. If there is one, this.isDrag = false. Stop the drag. Then the core part cleverly uses the splice. If this.replaceItem! = = this.dragid, add this.options.items.splice(this.dragId, 1)[0], that is, the initial id of the drag element, It means that the drag is not successful, return to the original position, otherwise the drag is successful. Now I'll demonstrate it with a moving picture.

Finally, Tomb Sweeping Day is also novel coronavirus pneumonia, and we are deeply concerned about the days of the martyrs and the dead compatriots who died at the expense of the new crown pneumonia.

Just add the following css in the global. The code is as follows. Refer to the article 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=\'0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0\'/></filter></svg>#grayscale")
    filter progid:DXImageTransform.Microsoft.BasicImage(grayscale=1)
    -webkit-filter: grayscale(1)
Copy code

Design sketch:

End

The article is over now. If there is any mistake, please point it out to me! Don't forget to like it if you think it's good A kind of Go again! I hope that you-P reading the article, come on, persistence is more terrible than hard work, let's fight together to go to the end of the world.

Finally, attach Github address

Personal blog address

expect

  • The author's Junior is looking for spring recruitment practice, looking forward to the big guy's favor~

Topics: Vue Javascript xml github