Drag,drag,drag!拽出嗶哩嗶哩側邊導航組件


一.前言


文章主要以宏觀的形式來聊嗶哩嗶哩側邊導航拖拽組件,非常適合正在漸進式學習VUE的你,適當的模仿開發項目是前端學習必須要有的技能。大多數人都知道的是,面試需要有自己的作品,而作品最重要的不是切頁面,而是:創新+用戶體驗+性能優化+技術展示 。作者也是一個前端小白,正在摸索階段,我今天講解的是模仿我覺得做的不錯的側邊導航欄,希望大家有收穫。讓我們一起來,淡黃的長裙,蓬鬆的頭髮,拽拽拽!

組件展示

這是一個模仿老版嗶哩嗶哩的側邊導航欄組件,部分效果如下圖:

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

  1. 導航欄中的條目元素item可以進行拖拽,並且頁面專題結構同步改變。
  2. 點擊任意條目元素item,可以立即到其對應的頁面位置。
  3. 當瀏覽頁面時,移動的某個專題時,旁邊的條目元素item也會與之對應。

二.具體講解

  • 根據需求:本文將簡述對h5和css進行編寫,重點是如何實現實時滾動導航和拖拽。

獲取專題名稱及其相關數據

1.首先我們要去vuex裏面拿數據,完成顯示專題名稱,拖拽等功能,需要sortValuessortKeys以及sortIds,vuex通過去請求嗶哩嗶哩官方提供的api進行拿取。具體過程暫且忽略,部分代碼如下(因爲這個是一個全棧項目,而這個組件和其他組件的關聯程度最大,所以作者有點不好如何講解,還望多多諒解,文末將會附上guthub地址):

import { contentApi, contentrankApi } from '@/api'
import * as TYPE from '../actionType/contentType' //採用actionType便於開發與管理

const state = {
   // 默認排序
   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: ['動畫', '番劇', '音樂', '舞蹈', '遊戲', '科技', '生活', '鬼畜', '時尚', '廣告', '娛樂', '電影', '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. 接下來,我們要做的事情就是就是對數據進行初始化。作者先上代碼再來解釋,代碼如下:

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

首先我們將所有我們實現需求所需的數據,全部簡單初始化寫在data,如我們需要實現頁面滾動時條目跟隨專題,就需要獲取這個條目的序號,名字,元素以及距離頁面頂部的高度等等。要實現可以把條目進行拖拽,就需要獲取是否參與拖拽狀態,正在拖拽哪一個條目,所有需要獲取拖拽的條目序號以及鼠標的一些數據。

僅僅向上面這樣初始化數據是遠遠不夠的,要實現需求就必須在兼容所有瀏覽器的情況下,獲取整個網頁的大小寬高數據以及對鼠標的操作有着實時的監聽。作者先上代碼:

methods: {
    /** 初始化 */
    init() {
      this.initData(); //初始化
      this.bindEvent();
      this._screenHeight = window.screen.availHeight; //返回當前屏幕高度(空白空間) 
      this._left = this.$refs.list.getBoundingClientRect().left;//方法返回元素的大小及其相對於視口的位置。
      this._top = this.$refs.list.getBoundingClientRect().top;
    },
    /** 綁定事件 */
    bindEvent() {
      document.addEventListener("scroll", this.scroll, false);
      document.addEventListener("mousemove", this.dragMove, false);//當指針設備( 通常指鼠標 )在元素上移動時, mousemove 事件被觸發。
      document.addEventListener("mouseup", this.dragEnd, false);//事件在指針設備按鈕擡起時觸發。
      document.addEventListener("mouseleave", this.dragEnd, false);//指點設備(通常是鼠標)的指針移出某個元素時,會觸發mouseleave事件。
      //mouseleave  和 mouseout 是相似的,但是兩者的不同在於mouseleave 不會冒泡而mouseout 會冒泡。
      //這意味着當指針離開元素及其所有後代時,會觸發mouseleave,而當指針離開元素或離開元素的後代(即使指針仍在元素內)時,會觸發mouseout。
    },
    /** 初始化data */
    initData() {
      //將this.options.items轉化成新的數組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,//返回當前元素相對於其 offsetParent 元素的頂部的距離。
          height: element.offsetHeight//它返回該元素的像素高度,高度包含該元素的垂直內邊距和邊框,且是一個整數。
        };
      });
    },
    //獲取元素距離頂部的距離
    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;//表示一個元素的上邊框的寬度.boder
      scrollTop = window.pageYOffset || doc.scrollTop;//返回當前頁面相對於窗口顯示區左上角的 Y 位置。瀏覽器兼容
      return top + scrollTop - clientTop;
    },
   }
  • init():在瀏覽器中打開可能是全屏或者是小窗,此時頁面的大小高度都會改變,我們必須每次當瀏覽器窗口大小變化時,重新獲取(初始化),當前屏幕的高度以及每個條目元素相對窗口的位置,只有這樣纔可以在不同的情況下,也不出錯,實時變化。使用screen.availHeight.availHeight獲取屏幕高度,使用getBoundingClientRect()方法來獲取條目元素相對於視窗的位置,如下圖所示。

  • bindEvent():這個方法裏面寫了對鼠標操作以及滾動的行爲進行事件綁定,也可說監聽,這是實現實時變化的關鍵。這個方法裏面我要特別說一下的是我們使用mouseleave,而不使用mouseout,的原因是我們需要實現進行拖拽時,當條目元素脫出側邊欄,這個元素將不會顯示了(下面將放上展示動圖),因爲觸發了mouseleave,這個方法是當鼠標離開其父組件時觸發。不使用mouseout是因爲這個方法離開元素自己的位置就會觸發離開其父級元素的時候也會觸發,是冒泡觸發的。這裏我們使用一定要準確,如果你還是有點不理解,可以去試試MDN上的對比演示demo演示demo文檔

  • initData(): 將this.options.items轉化成新的數組this.data,返回名字、元素本身、元素相對於其 offsetParent 元素的頂部的距離以及該元素的像素高度,高度包含該元素的垂直內邊距和邊框。

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

3. 現在我們就要開始實現第一個功能,點擊條目元素,網頁移動到對應的位置,我們要實現這個功能很容易,只要獲取對應條目元素的位置和index就可以實現,但是要實現平滑的滾動需要引入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
	}
}

關於smooth-scroll.js,作者推薦自己查一下資料,有比較多。

4. 實現頁面滾動時條目元素跟隨對應,代碼如下:

     //  偏移值
    offset() {
      return this.options.offset || 100;
    },
     /** 滾動事件 */
    scroll(e) {
      this.scrollTop =
        window.pageYOffset ||
        document.documentElement.scrollTop + document.body.scrollTop;//瀏覽器兼容,返回當前頁面相對於窗口顯示區左上角的 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);
      //實時跟蹤頁面滾動
      for (let i = 0; i < this.data.length; i++) {
        if (this.scrollTop >= this.data[i].offsetTop - this.offset) {
          this.current = i;
        }
      }
    },

這裏我們可以看到,我們使用了初始化裏面的數據,然後滾動的關鍵就是獲得元素到窗口的距離以及偏移值。需要注意的一個細節是滾動時元素與窗口頂部的距離大於300px時,整個組件將吸頂。

5. 實現拖拽

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

通過上面的代碼可知,進入排序模式的代碼比較簡單,主要是由css的動畫來實現。

2.開始拖拽

/** 得到鼠標位置 */
    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);
    },

開始拖拽時,需要判斷是否進入了排序,進入了才允許可以進行拖拽,此時獲得鼠標選中的位置,元素的位置以及對應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會變爲absolute,dragStyles用來設置其位置,鼠標運動時會調用,從而實現跟隨鼠標運動
    dragStyles() {
      return {
        left: `${this.x}px`,
        top: `${this.y}px`
      };
    },
    //當被拖拽的元素運動到其他元素的位置時,會使得replaceItem發送變化
    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 瀏覽器不要執行與事件關聯的默認動作(如果存在這樣的動作)
    },

進入拖拽時,首要的是判斷是否獲取了要拖拽元素的鼠標位置,如果沒有獲取到,將無法進行拖拽,則使用e.preventDefault()通知瀏覽器不進行拖拽。然後使用dragStyles()獲取元素拖拽的實時位置。最後元素拖拽時會改變其他元素的位置,位置改變了,其對應的id就會發生變化,我們通過replaceItem()來實現,在這個方法裏面,我們奇妙的利用元素的實時高度與元素本身的高度相除獲得動態的id

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

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


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

在全局中加上如下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)

效果圖:


結束

文章看到現在也結束啦,如果有錯誤的話就麻煩大家給我指出來吧!如果覺得不錯的話別忘了點個贊👍再走噢!

最後附上Github地址

個人博客地址

期待

  • 作者大三正在尋找春招實習中,期待大佬的青睞~
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章