文章主要以宏觀的形式來聊嗶哩嗶哩側邊導航拖拽組件,很是適合正在漸進式學習VUE的你,適當的模仿開發項目是前端學習必需要有的技能。大多數人都知道的是,面試須要有本身的做品,而做品最重要的不是切頁面,而是:創新+用戶體驗+性能優化+技術展現 。做者也是一個前端小白,正在摸索階段,我今天講解的是模仿我以爲作的不錯的側邊導航欄,但願你們有收穫。讓咱們一塊兒來,淡黃的長裙,蓬鬆的頭髮,拽拽拽!css
這是一個模仿老版嗶哩嗶哩的側邊導航欄組件,部分效果以下圖:html
根據效果圖能夠看出,組件擁有如下功能:前端
item
能夠進行拖拽,而且頁面專題結構同步改變。item
,能夠當即到其對應的頁面位置。item
也會與之對應。sortValues
、sortKeys
以及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
}
複製代碼
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
,如咱們須要實現頁面滾動時條目跟隨專題,就須要獲取這個條目的序號,名字,元素以及距離頁面頂部的高度等等。要實現能夠把條目進行拖拽,就須要獲取是否參與拖拽狀態,正在拖拽哪個條目,全部須要獲取拖拽的條目序號以及鼠標的一些數據。vue
僅僅向上面這樣初始化數據是遠遠不夠的,要實現需求就必須在兼容全部瀏覽器的狀況下,獲取整個網頁的大小寬高數據以及對鼠標的操做有着實時的監聽。做者先上代碼:git
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;
},
}
複製代碼
screen.availHeight.availHeight
獲取屏幕高度,使用getBoundingClientRect()
方法來獲取條目元素相對於視窗的位置,以下圖所示。mouseleave
,而不使用mouseout
,的緣由是咱們須要實現進行拖拽時,當條目元素脫出側邊欄,這個元素將不會顯示了(下面將放上展現動圖),由於觸發了mouseleave
,這個方法是當鼠標離開其父組件時觸發。不使用mouseout
是由於這個方法離開元素本身的位置就會觸發離開其父級元素的時候也會觸發,是冒泡觸發的。這裏咱們使用必定要準確,若是你仍是有點不理解,能夠去試試MDN上的對比演示demo演示demo文檔。initData(): 將this.options.items
轉化成新的數組this.data
,返回名字、元素自己、元素相對於其 offsetParent 元素的頂部的距離以及該元素的像素高度,高度包含該元素的垂直內邊距和邊框。github
getOffsetTop():獲取條目元素距離頂部的距離,這裏做者不過多講解推薦一篇文章JavaScript之scrollTop、scrollHeight、offsetTop、offsetHeight等屬性學習筆記。須要講解的是return top + scrollTop - clientTop;
元素自己的高度加上滾動增長的高度減去一個重複的上邊框高度纔是實際的元素的高度。web
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
,做者推薦本身查一下資料,有比較多。vuex
// 偏移值
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時,整個組件將吸頂。api
- 進入排序模式
<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。
- 拖拽完成
/** 拖拽結束 */
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)
複製代碼
效果圖:
文章看到如今也結束啦,若是有錯誤的話就麻煩你們給我指出來吧!若是以爲不錯的話別忘了點個贊👍再走噢!但願在看文章的你-P,加油,堅持比努力更可怕,讓咱們一塊兒仗劍走天涯。