Drag,drag,drag!拽出嗶哩嗶哩側(cè)邊導(dǎo)航組件

image

一.前言


文章主要以宏觀的形式來(lái)聊嗶哩嗶哩側(cè)邊導(dǎo)航拖拽組件,非常適合正在漸進(jìn)式學(xué)習(xí)VUE的你,適當(dāng)?shù)哪7麻_發(fā)項(xiàng)目是前端學(xué)習(xí)必須要有的技能。大多數(shù)人都知道的是,面試需要有自己的作品,而作品最重要的不是切頁(yè)面,而是:<font face="黑體" color=red>創(chuàng)新+用戶體驗(yàn)+性能優(yōu)化+技術(shù)展示</font> 。作者也是一個(gè)前端小白,正在摸索階段,我今天講解的是模仿我覺得做的不錯(cuò)的側(cè)邊導(dǎo)航欄,希望大家有收獲。讓我們一起來(lái),淡黃的長(zhǎng)裙,蓬松的頭發(fā),拽拽拽!

組件展示

這是一個(gè)模仿老版嗶哩嗶哩的側(cè)邊導(dǎo)航欄組件,部分效果如下圖:

image

根據(jù)效果圖可以看出,組件擁有以下功能:

  1. 導(dǎo)航欄中的條目元素item可以進(jìn)行拖拽,并且頁(yè)面專題結(jié)構(gòu)同步改變。
  2. 點(diǎn)擊任意條目元素item,可以立即到其對(duì)應(yīng)的頁(yè)面位置。
  3. 當(dāng)瀏覽頁(yè)面時(shí),移動(dòng)的某個(gè)專題時(shí),旁邊的條目元素item也會(huì)與之對(duì)應(yīng)。

二.具體講解

  • 根據(jù)需求:本文將簡(jiǎn)述對(duì)h5和css進(jìn)行編寫,重點(diǎn)是如何實(shí)現(xiàn)實(shí)時(shí)滾動(dòng)導(dǎo)航和拖拽。

獲取專題名稱及其相關(guān)數(shù)據(jù)

1.首先我們要去vuex里面拿數(shù)據(jù),完成顯示專題名稱,拖拽等功能,需要sortValues、sortKeys以及sortIds,vuex通過去請(qǐng)求嗶哩嗶哩官方提供的api進(jìn)行拿取。具體過程暫且忽略,部分代碼如下(因?yàn)檫@個(gè)是一個(gè)全棧項(xiàng)目,而這個(gè)組件和其他組件的關(guān)聯(lián)程度最大,所以作者有點(diǎn)不好如何講解,還望多多諒解,文末將會(huì)附上guthub地址):

import { contentApi, contentrankApi } from '@/api'
import * as TYPE from '../actionType/contentType' //采用actionType便于開發(fā)與管理

const state = {
   // 默認(rèn)排序
   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: ['動(dòng)畫', '番劇', '音樂', '舞蹈', '游戲', '科技', '生活', '鬼畜', '時(shí)尚', '廣告', '娛樂', '電影', 'TV劇'],
   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) {

   },

   // 排行榜信息
   [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
}

2. 接下來(lái),我們要做的事情就是就是對(duì)數(shù)據(jù)進(jìn)行初始化。作者先上代碼再來(lái)解釋,代碼如下:

import { mapGetters } from "vuex";
export default {
  mixins: [scrollMixin],
  data() {
    return {
      current: 0, //當(dāng)前選中條目的序號(hào)
      data: [], //數(shù)據(jù)(name,element,offsetTop,height)
      time: 800, //動(dòng)畫時(shí)間
      height: 32, //單個(gè)元素的高度
      isSort: false, //排序模式
      scrollTop: 0, //距離頁(yè)面的頂部距離
      dragId: 0, //拖拽元素序號(hào)
      isDrag: false, //當(dāng)前是否在拖拽
      offsetX: 0, //鼠標(biāo)在要拖拽的元素上的X坐標(biāo)上的偏移
      offsetY: 0, //鼠標(biāo)在要拖拽的元素上的Y坐標(biāo)上的偏移
      x: 0, //被拖拽的元素在其相對(duì)的元素上的X坐標(biāo)上的偏移
      y: 0 //被拖拽的元素在其相對(duì)的元素上的Y坐標(biāo)上的偏移
    };
  },

首先我們將所有我們實(shí)現(xiàn)需求所需的數(shù)據(jù),全部簡(jiǎn)單初始化寫在data,如我們需要實(shí)現(xiàn)頁(yè)面滾動(dòng)時(shí)條目跟隨專題,就需要獲取這個(gè)條目的序號(hào),名字,元素以及距離頁(yè)面頂部的高度等等。要實(shí)現(xiàn)可以把條目進(jìn)行拖拽,就需要獲取是否參與拖拽狀態(tài),正在拖拽哪一個(gè)條目,所有需要獲取拖拽的條目序號(hào)以及鼠標(biāo)的一些數(shù)據(jù)。

僅僅向上面這樣初始化數(shù)據(jù)是遠(yuǎn)遠(yuǎn)不夠的,要實(shí)現(xiàn)需求就必須在兼容所有瀏覽器的情況下,獲取整個(gè)網(wǎng)頁(yè)的大小寬高數(shù)據(jù)以及對(duì)鼠標(biāo)的操作有著實(shí)時(shí)的監(jiān)聽。作者先上代碼:

methods: {
    /** 初始化 */
    init() {
      this.initData(); //初始化
      this.bindEvent();
      this._screenHeight = window.screen.availHeight; //返回當(dāng)前屏幕高度(空白空間) 
      this._left = this.$refs.list.getBoundingClientRect().left;//方法返回元素的大小及其相對(duì)于視口的位置。
      this._top = this.$refs.list.getBoundingClientRect().top;
    },
    /** 綁定事件 */
    bindEvent() {
      document.addEventListener("scroll", this.scroll, false);
      document.addEventListener("mousemove", this.dragMove, false);//當(dāng)指針設(shè)備( 通常指鼠標(biāo) )在元素上移動(dòng)時(shí), mousemove 事件被觸發(fā)。
      document.addEventListener("mouseup", this.dragEnd, false);//事件在指針設(shè)備按鈕抬起時(shí)觸發(fā)。
      document.addEventListener("mouseleave", this.dragEnd, false);//指點(diǎn)設(shè)備(通常是鼠標(biāo))的指針移出某個(gè)元素時(shí),會(huì)觸發(fā)mouseleave事件。
      //mouseleave  和 mouseout 是相似的,但是兩者的不同在于mouseleave 不會(huì)冒泡而mouseout 會(huì)冒泡。
      //這意味著當(dāng)指針離開元素及其所有后代時(shí),會(huì)觸發(fā)mouseleave,而當(dāng)指針離開元素或離開元素的后代(即使指針仍在元素內(nèi))時(shí),會(huì)觸發(fā)mouseout。
    },
    /** 初始化data */
    initData() {
      //將this.options.items轉(zhuǎn)化成新的數(shù)組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,//返回當(dāng)前元素相對(duì)于其 offsetParent 元素的頂部的距離。
          height: element.offsetHeight//它返回該元素的像素高度,高度包含該元素的垂直內(nèi)邊距和邊框,且是一個(gè)整數(shù)。
        };
      });
    },
    //獲取元素距離頂部的距離
    getOffsetTop(element) {
      let top,
        clientTop,
        clientLeft,
        scrollTop,
        scrollLeft,
        doc = document.documentElement,//返回元素
        body = document.body;
      if (typeof element.getBoundingClientRect !== "undefined") {
        top = element.getBoundingClientRect().top;
      } else {
        top = 0;
      }
      clientTop = doc.clientTop || body.clientTop || 0;//表示一個(gè)元素的上邊框的寬度.boder
      scrollTop = window.pageYOffset || doc.scrollTop;//返回當(dāng)前頁(yè)面相對(duì)于窗口顯示區(qū)左上角的 Y 位置。瀏覽器兼容
      return top + scrollTop - clientTop;
    },
   }
  • init():在瀏覽器中打開可能是全屏或者是小窗,此時(shí)頁(yè)面的大小高度都會(huì)改變,我們必須每次當(dāng)瀏覽器窗口大小變化時(shí),重新獲?。ǔ跏蓟?dāng)前屏幕的高度以及每個(gè)條目元素相對(duì)窗口的位置,只有這樣才可以在不同的情況下,也不出錯(cuò),實(shí)時(shí)變化。使用screen.availHeight.availHeight獲取屏幕高度,使用getBoundingClientRect()方法來(lái)獲取條目元素相對(duì)于視窗的位置,如下圖所示。
image
  • bindEvent():這個(gè)方法里面寫了對(duì)鼠標(biāo)操作以及滾動(dòng)的行為進(jìn)行事件綁定,也可說監(jiān)聽,這是實(shí)現(xiàn)實(shí)時(shí)變化的關(guān)鍵。這個(gè)方法里面我要特別說一下的是我們使用mouseleave,而不使用mouseout,的原因是我們需要實(shí)現(xiàn)進(jìn)行拖拽時(shí),當(dāng)條目元素脫出側(cè)邊欄,這個(gè)元素將不會(huì)顯示了(下面將放上展示動(dòng)圖),因?yàn)橛|發(fā)了mouseleave,這個(gè)方法是當(dāng)鼠標(biāo)離開其父組件時(shí)觸發(fā)。不使用mouseout是因?yàn)檫@個(gè)方法離開元素自己的位置就會(huì)觸發(fā)離開其父級(jí)元素的時(shí)候也會(huì)觸發(fā),是冒泡觸發(fā)的。這里我們使用一定要準(zhǔn)確,如果你還是有點(diǎn)不理解,可以去試試MDN上的對(duì)比演示demo演示demo文檔
image
  • initData(): 將this.options.items轉(zhuǎn)化成新的數(shù)組this.data,返回名字、元素本身、元素相對(duì)于其 offsetParent 元素的頂部的距離以及該元素的像素高度,高度包含該元素的垂直內(nèi)邊距和邊框。

  • getOffsetTop():獲取條目元素距離頂部的距離,這里作者不過多講解推薦一篇文章JavaScript之scrollTop、scrollHeight、offsetTop、offsetHeight等屬性學(xué)習(xí)筆記。需要講解的是return top + scrollTop - clientTop;元素本身的高度加上滾動(dòng)增加的高度減去一個(gè)重復(fù)的上邊框高度才是實(shí)際的元素的高度。

3. 現(xiàn)在我們就要開始實(shí)現(xiàn)第一個(gè)功能,點(diǎn)擊條目元素,網(wǎng)頁(yè)移動(dòng)到對(duì)應(yīng)的位置,我們要實(shí)現(xiàn)這個(gè)功能很容易,只要獲取對(duì)應(yīng)條目元素的位置和index就可以實(shí)現(xiàn),但是要實(shí)現(xiàn)平滑的滾動(dòng)需要引入smooth-scroll.js代碼如下:

        <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(() => {});
    },

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
    }
}

關(guān)于smooth-scroll.js,作者推薦自己查一下資料,有比較多。

4. 實(shí)現(xiàn)頁(yè)面滾動(dòng)時(shí)條目元素跟隨對(duì)應(yīng),代碼如下:

     //  偏移值
    offset() {
      return this.options.offset || 100;
    },
     /** 滾動(dòng)事件 */
    scroll(e) {
      this.scrollTop =
        window.pageYOffset ||
        document.documentElement.scrollTop + document.body.scrollTop;//瀏覽器兼容,返回當(dāng)前頁(yè)面相對(duì)于窗口顯示區(qū)左上角的 Y 位置
      if (this.scrollTop >= 300) {
        this.$refs.navSide.style.top = "0px";
        this.init();
      } else {
        this.$refs.navSide.style.top = "240px";
        this.init();
      }
      // console.log("距離頂部" + this.scrollTop);
      //實(shí)時(shí)跟蹤頁(yè)面滾動(dòng)
      for (let i = 0; i < this.data.length; i++) {
        if (this.scrollTop >= this.data[i].offsetTop - this.offset) {
          this.current = i;
        }
      }
    },

這里我們可以看到,我們使用了初始化里面的數(shù)據(jù),然后滾動(dòng)的關(guān)鍵就是獲得元素到窗口的距離以及偏移值。需要注意的一個(gè)細(xì)節(jié)是滾動(dòng)時(shí)元素與窗口頂部的距離大于300px時(shí),整個(gè)組件將吸頂。

5. 實(shí)現(xiàn)拖拽

  1. 進(jìn)入排序模式
  <div class="nav-side" :class="{customizing: isSort}" ref="navSide">  <!--默認(rèn)不進(jìn)行排序-->
    <transition name="fade">
      <div v-if="isSort">
        <div class="tip"></div>
        <div class="custom-bg"></div>
      </div>
    </transition>
 </div>
 //進(jìn)入排序模式
    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;
    }
  }
}

通過上面的代碼可知,進(jìn)入排序模式的代碼比較簡(jiǎn)單,主要是由css的動(dòng)畫來(lái)實(shí)現(xiàn)。

2.開始拖拽

/** 得到鼠標(biāo)位置 */
    getPos(e) {
      this.x = e.clientX - this._left - this.offsetX;
      this.y = e.clientY - this._top - this.offsetY;
    },
/** 拖拽開始 */
    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);
    },

開始拖拽時(shí),需要判斷是否進(jìn)入了排序,進(jìn)入了才允許可以進(jìn)行拖拽,此時(shí)獲得鼠標(biāo)選中的位置,元素的位置以及對(duì)應(yīng)id。

3.拖拽中

<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>
      
      
    // 拖拽的元素的position會(huì)變?yōu)閍bsolute,dragStyles用來(lái)設(shè)置其位置,鼠標(biāo)運(yùn)動(dòng)時(shí)會(huì)調(diào)用,從而實(shí)現(xiàn)跟隨鼠標(biāo)運(yùn)動(dòng)
    dragStyles() {
      return {
        left: `${this.x}px`,
        top: `${this.y}px`
      };
    },
    //當(dāng)被拖拽的元素運(yùn)動(dòng)到其他元素的位置時(shí),會(huì)使得replaceItem發(fā)送變化
    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;
    }
    
     /** 拖拽中 */
    dragMove(e) {
      if (this.isDrag) {
        this.getPos(e);
      }
      e.preventDefault();//該方法將通知 Web 瀏覽器不要執(zhí)行與事件關(guān)聯(lián)的默認(rèn)動(dòng)作(如果存在這樣的動(dòng)作)
    },

進(jìn)入拖拽時(shí),首要的是判斷是否獲取了要拖拽元素的鼠標(biāo)位置,如果沒有獲取到,將無(wú)法進(jìn)行拖拽,則使用e.preventDefault()通知瀏覽器不進(jìn)行拖拽。然后使用dragStyles()獲取元素拖拽的實(shí)時(shí)位置。最后元素拖拽時(shí)會(huì)改變其他元素的位置,位置改變了,其對(duì)應(yīng)的id就會(huì)發(fā)生變化,我們通過replaceItem()來(lái)實(shí)現(xiàn),在這個(gè)方法里面,我們奇妙的利用元素的實(shí)時(shí)高度與元素本身的高度相除獲得動(dòng)態(tài)的id。

  1. 拖拽完成
    /** 拖拽結(jié)束 */
    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);
        }

這段代碼巧妙的是,首先判斷是否還在進(jìn)行拖拽如果有,則this.isDrag = false;停止拖拽,接著就是核心部分巧妙利用splice,如果this.replaceItem !== this.dragId,則在this.replaceItem后面添加this.options.items.splice(this.dragId, 1)[0],即這個(gè)拖拽元素初始id,相當(dāng)于拖拽不成功,回到原來(lái)的位置,否則拖拽成功。下面我用動(dòng)圖來(lái)演示一下。

image

最后今天是清明節(jié),也是我們深切悼念新冠肺炎疫情犧牲的烈士和逝世同胞的日子,把網(wǎng)站變灰。

在全局中加上如下css就好,代碼如下,參考文章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)

效果圖:

image

結(jié)束

文章看到現(xiàn)在也結(jié)束啦,如果有錯(cuò)誤的話就麻煩大家給我指出來(lái)吧!如果覺得不錯(cuò)的話別忘了點(diǎn)個(gè)贊??再走噢!

最后附上Github地址

個(gè)人博客地址

期待

  • 作者大三正在尋找春招實(shí)習(xí)中,期待大佬的青睞~
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容