vue使用Split封裝通用拖拽滑動分隔面板元件
前言
手動封裝一個類似Iview中的Split元件,可將一片區域,分割為可以拖拽調整寬度或高度的兩部分割槽域,最終效果如下:
開始
基礎佈局
在vue工程中建立SplitPane元件,引入頁面使用。
<template> <div class="page"> <SplitPane /> </div> </template> <script> import SplitPane from './components/split-pane' export default { components: { SplitPane },data() { return {} } } </script> <style scoped lang="scss"> .page { height: 100%; padding: 10px; background: #000; } </style>
// split-pane.vue <template> <div class="split-pane"> split </div> </template> <script> export default { data() { return {} } } </script> <style scoped lang="scss"> .split-pane { background: palegreen; height: 100%; } </style>
SplitPane元件由三部分組成:區域1,區域2,以及滑動器。新增這三個元素,並分別新增class名,注意.pane為區域1和區域2共用。
<template> <div class="split-pane"> <div class="pane pane-one"></div> <div class="pane-trigger"></div> <div class="pane pane-two"></div> </div> </template>
將容器設定為flex佈局,區域2的flex屬性設為1,則區域2會根據區域1的寬度變化自適應。
<style scoped lang="scss"> .split-pane { background: palegreen; height: 100%; display: flex; .pane-one { width: 50%; background: palevioletred; } .pane-trigger { width: 10px; height: 100%; background: palegoldenrod; } .pane-two { flex: 1; background: turquoise; } } </style>
可以看到設定區域1的寬度變化就是實現該元件的核心點。
除了橫向還要支援縱向佈局,所以給元件新增一個direction屬性,該屬性由外部傳入,值為row 或 column,與父元素的flex-direction屬性繫結。
<template> <div class="split-pane" :style="{ flexDirection: direction }"> <div class="pane pane-one"></div> <div class="pane-trigger"></div> <div class="pane pane-two"></div> </div> </template> <script> e程式設計客棧xport default { props: { direction: { type: String,default: 'row' } },data() { return {} } } </script>
在橫向佈局中,區域1設定width:50%,滑動器設定width:10px,而變為縱向佈局後這兩個width應該變為height。所以刪除style中這兩個width設定,新增一個lengthType計算屬性,根據不同的direction在行內樣式中給這兩個元素分別設定寬高。
<template> <div class="split-pane" :style="{ flexDirection: direction }"> <div class="pane pane-one" :style="lengthType + ':50%'"></div> <div class="pane-trigger" :style="lengthType + ':10px'"></div> <div class="pane pane-two"></div> </div> </template> computed: { lengthType() { return this.direction === 'row' ? 'width' : 'height' } }
同時在橫向佈局中,區域1,區域2,滑動器的height都為100%,在縱向佈局下都應該改為width: 100%。所以刪除原本的height設定,將direction繫結為容器的一個class,根據該class設定三個子元素兩種情況下100%的屬性。
<template>
<div class="split-pane" :class="direction" :style="{ flexDirection: direction }">
<div class="pane pane-one" :style="lengthType + ':50%'"></div>
<div class="pane-trigger" :style="lengthType + ':10px'"></div>
<div class="pane pane-two"></div>
</div>
</template>
<script>
export default {
props: {
direction: {
type: String,default: 'row'
}
},data() {
return {}
},computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'
}
}
}
</script>
<style scoped lang="scss">
.split-pane {
background: palegreen;
height: 100%;
display: flex;
&.row {
.pane {
heiDErQlHWnght: 100%;
}
.pane-trigger {
height: 100%;
}
}
&.column {
.pane {
width: 100%;
}
.pane-trigger {
width: 100%;
}
}
.pane-one {
background: palevioletred;
}
.pane-trigger {
background: palegoldenrod;
}
.pane-two {
flex: 1;
background: turquoise;
}
}
</style>
此時如果在頁面中給元件傳入direction="column",可以看到已經變為縱向
<template> <div class="page"> <SplitPane direction="column" /> </div> </template>
資料繫結
當前區域1的寬(高)度和滑動器的寬(高)度都是在樣式中寫死的,需要變為在js中繫結才能進行操作,首先將能用於計算的數字放在data中
data() { return { paneLengthPercent: 50,// 區域1寬度 (%) triggerLength: 10 // 滑動器寬度 (px) } }
然後通過computed返回兩個樣式中需要的字串,同時為了保證滑動器在區域1和區域2的正中間,區域1的寬度應該減去滑動器寬度的一半。
computed: { lengthType() { return this.direction === 'row' ? 'width' : 'height' },paneLengthValue() { return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})` },triggerLengthValue() { return this.triggerLength + 'px' } }
最後繫結在模板中
<template> <div class="split-pane" :class="direction" :style="{ flexDirection: direction }"> <div class="pane pane-one" :style="lengthType + ':' + paneLengthValue"></div> <div class="pane-trigger" :style="lengthType + ':' + triggerLengthValue"></div> <div class="pane pane-two"></div> </div> </template>
事件繫結
想象一下拖拽滑動器的過程,第一步是在滑動器上按下滑鼠,給滑動器新增mousedown事件
<div class="pane-trigger" :style="lengthType + ':' + triggerLengthValue" @mousedown="handleMouseDown"></div>
按下滑鼠後開始滑動,應該監聽mousemove事件,但注意不是在滑動器上,而是在整個文件上監聽,因為滑鼠有可能滑動到頁面任何位置。當用戶鬆開滑鼠時,應該取消對整個文件mousemove的監聽,所以在滑鼠按下的那一刻,應該對document新增兩個事件:滑鼠移動和滑鼠鬆開
methods: { // 按下滑動器 handleMouseDown(e) { document.addEventListener('mousemove',this.handleMouseMove) document.addEventListener('mouseup',this.handleMouseUp) },// 按下滑動器後移動滑鼠 handleMouseMove(e) { console.log('拖動中') },// 鬆開滑動器 handleMouseUp() { document.removeEventListener('mousemove',this.handleMouseMove) } }
我們實際要控制的是區域1的寬度,讓區域1的寬度等於當前滑鼠距容器左邊的距離,也就是如果滑鼠移動到下圖的圓圈位置,讓區域1的寬度等於中間的長度:
這個長度可以根據當前滑鼠距頁面最左邊的距離減去容器距頁面最左邊的距離算出,也就是綠色長度等於紅色減藍色:
給容器新增ref為了獲取容器的dom資訊
... <div ref="splitPane" class="split-pane" :class="direction" :style="{ flexDirection: direction }"> ...
如果列印ref的getBoundingClientRect()可以看到如下資訊:
console.log(this.$refs.splitPane.getBoundingClientRect())
其中left代表容器距離頁面左側的距離,width代表容器的寬度。
通過滑鼠事件物件event的pageX可以獲得當前滑鼠距頁面左側的距離,則我們要求的滑鼠距容器左側距離就可以算出來了。
最後用這個距離除以容器寬度乘上100,就得到了這個距離的百分比數值,賦值給paneLengthPercent。
// 按下滑動器後移動滑鼠 handleMouseMove(e) { const clientRect = this.$refs.splitPane.getBoundingClientRect() const offset = e.pageX - clientRect.left const paneLengthPercent = (offset / clientRect.width) * 100 this.paneLengthPercent = paneLengthPercent },
相容縱向佈局。
// 按下滑動器後移動滑鼠 handleMouseMove(e) { const clientRect = this.$refs.splitPane.getBoundingClientRect() let paneLengthPercent = 0 if (this.direction === 'row') { const offset = e.pageX - clientRect.left paneLengthPercent = (offset / clientRect.width) * 100 } else { const offset = e.pageY - clientRect.top paneLengthPercent = (offset / clientRect.height) * 100 } this.paneLengthPercent = paneLengthPercent },
優化
此時看上去需求已經完成,但作為一個通用元件還有幾個要優化的地方。
優化一 抖動問題
把滑動器寬度設定大一些後可以發現一個抖動問題如下:
在滑動器兩側按下後輕輕移動就會出現大幅偏移,因為現在的計算邏輯始終認為滑鼠在滑動器的正中間,沒有把滑動器寬度考慮進去。
在dota中定義一個當前滑鼠距滑動器左(頂)側偏移量
data() { return { paneLengthPercent: 50,// 區域1寬度 (%) triggerLength: 100,// 滑動器寬度 (px) triggerLeftOffset: 0 // 滑鼠距滑動器左(頂)側偏移量 } }
這個值等於滑鼠距頁面左側距離減去滑動器距頁面左側距離(通過e.srcElement.getBoundingClientRect()),在每次滑動器被按下時進行賦值,也要區分橫向/縱向佈局:紅 - 藍 = 綠
// 按下滑動器 handleMouhttp://www.cppcns.comseDown(e) { document.addEventListener('mousemove',this.handleMouseUp) if (this.direction === 'row') { this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left } else { this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top } },
有了這個triggerLeftOffset,設定區域1的寬度時就應該變成:滑鼠距容器左側距離 減去 滑鼠距滑動器左側的距離(triggerLeftOffset) 再加上滑動器寬度的一半。
這樣就相當於把滑鼠又定位回了滑動器正中間。
// 按下滑動器後移動滑鼠 handleMouseMove(e) { const clientRect = this.$refs.splitPane.getBoundingClientRect() let paneLengthPercent = 0 if (this.direction === 'row') { const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2 paneLengthPercent = (offset / clientRect.width) * 100 } else { const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2 paneLengthPercent = (offset / clientRect.height) * 100 } this.paneLengthPercent = paneLengthPercent },
此時不再有抖動問題
優化二 滑鼠樣式
滑鼠在滑動器上經過時應該改變樣式告訴使用者可以拖動,分別在橫向佈局與縱向佈局的滑動器css中新增滑鼠樣式變化。
<style scoped lang="scss">
.split-pane {
background: palegreen;
height: 100%;
display: flex;
&.row {
.pane {
height: 100%;
}
.pane-trigger {
height: 100%;
cursor: col-resize; // 這裡
}
}
&.column {
.pane {
width: 100%;
}
.pane-trigger {
width: 100%;
cursor: row-resize; // 這裡
}
DErQlHWn }
.pane-one {
background: palevioletred;
}
.pane-trigger {
background: palegoldenrod;
}
.pane-two {
flex: 1;
background: turquoise;
}
}
</style>
優化三 滑動限制
作為一個通用元件,應該向外部提供設定滑動最小與最大距離的限制功能,接收min與max兩個props。
props: { direction: { type: String,default: 'row' },min: { type: Number,default: 10 },max: { type: Number,default: 90 } },
在handleMouseMove加入判斷:
// 按下滑動器後移動滑鼠 handleMouseMove(e) { const clientRect = this.$refs.splitPane.getBoundingClientRect() let paneLengthPercent = 0 if (this.direction === 'row') { const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2 paneLengthPercent = (offset / clientRect.width) * 100 } else { const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2 paneLengthPercent = (offset / clientRect.height) * 100 } if (paneLengthPercent < this.min) { paneLengthPercent = this.min } if (paneLengthPercent > this.max) { paneLengthPercent = this.max } this.paneLengthPercent = paneLengthPercent }
優化四 面板預設寬度和滑動器寬度
還是作為一個通用元件,面板初始化比例與滑動器寬度應該也由外部使用者決定。
將data中的paneLengthPercent 和 triggerLength轉移到props中,從外部接收。
props: { direction: { type: String,default: 90 },paneLengthPercent: { type: Number,default: 50 },triggerLength: { type: Number,default: 10 } },data() { return { triggerLeftOffset: 0 // 滑鼠距滑動器左(頂)側偏移量 } },
在頁面中則需傳入paneLengthPercent,注意paneLengthPercent必須是一個定義在data中的資料,並且要加上.sync修飾符,因為這個值要動態修改。
// page.vue <template> <div class="page"> <SplitPane direction="row" :paneLengthPercent.sync="paneLengthPercent" /> </div> </template> ... data() { return { paneLengthPercent: 30 } } ...
然後在元件中handleMouseMove中通過this.$emit觸發事件的方式修改paneLengthPercent值。
// 按下滑動器後移動滑鼠 handleMouseMove(e) { const clientRect = this.$refs.splitPane.getBoundingClientRect() let paneLengthPercent = 0 if (this.direction === 'row') { const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2 paneLengthPercent = (offset / clientRect.width) * 100 } else { const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2 paneLengthPercent = (offset / clientRect.height) * 100 } if (paneLengthPercent < this.min) { paneLengthPercent = this.min } if (paneLengthPercent > this.max) { paneLengthPercent = this.max } this.$emit('update:paneLengthPercent',paneLengthPercent) // 這裡 },
此時元件的要素資訊都可以通過外部的props控制了。
優化五 插槽
作為一個容器元件不能新增內容不是等於白費,分別給兩個區域新增兩個具名插槽。
<template> <div ref="splitPane" class="split-pane" :class="direction" :style="{ flexDirection: direction }"> <div class="pane pane-one" :style="lengthType + ':' + paneLengthValue"> <slot name="one"></slot> </div> <div class="pane-trigger" :style="lengthType + ':' + triggerLengthValue" @mousedown="handleMouseDown"> </div> <div class="pane pane-two"> <slot name="two"></slot> </div> </div> </template>
優化六 禁止選中
在拖動過程中,如果區域中有文字內容可能會出現選中文字的情況,給滑動器新增禁止選中效果。
... .pane-trigger { user-select: none; background: palegoldenrod; } ...
結束
元件完整程式碼
保留各背景色僅為了文章展示需要,實際使用中刪除
<template>
<div ref="splitPane" class="split-pane" :class="direction" :style="{ flexDirection: direction }">
<div class="pane pane-one" :style="lengthType + ':' + paneLengthValue">
<slot name="one"></slot>
</div>
<div
class="pane-trigger"
:style="lengthType + ':' + triggerLengthValue"
@mousedown="handleMouseDown"
></div>
<div class="pane pane-two">
<slot name="two"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
direction: {
type: String,computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'
},paneLengthValue() {
return `calc(${this.paneLengthPercent}%www.cppcns.com - ${this.triggerLength / 2 + 'px'})`
},triggerLengthValue() {
return this.triggerLength + 'px'
}
},methods: {
// 按下滑動器
handleMouseDown(e) {
document.addEventListener('mousemove',// 按下滑動器後移動滑鼠
handleMouseMove(e) {
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPercent = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPercent = (offset / clientRect.width) * 100
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPercent = (offset / clientRect.height) * 100
}
if (paneLengthPercent < this.min) {
paneLengthPercent = this.min
}
if (paneLengthPercent > this.max) {
paneLengthPercent = this.max
}
this.$emit('update:paneLengthPercent',paneLengthPercent)
},this.handleMouseMove)
}
}
}
</script>
<style scoped lang="scss">
.split-pane {
background: palegreen;
height: 100%;
display: flex;
&.row {
.pane {
height: 100%;
}
.pane-trigger {
height: 100%;
cursor: col-resize;
}
}
&.column {
.pane {
width: 100%;
}
.pane-trigger {
width: 100%;
cursor: row-resize;
}
}
.pane-one {
background: palevioletred;
}
.pane-trigger {
user-select: none;
background: palegoldenrod;
}
.pane-two {
flex: 1;
background: turquoise;
}
}
</style>
元件使用示例
保留各背景色僅為了文章展示需要,實際使用中刪除
<template> <div class="page"> <SplitPane direction="column" :min="20" :max="80" :triggerLength="20" :paneLengthPercent.sync="paneLengthPercent" > <template v-slot:one> <div> 區域一 </div> </template> <template v-slot:two> <div> 區域二 </div> </template> </SplitPane> </div> </template> <script> import SplitPane from './components/split-pane' export default { components: { SplitPane },data() { return { paneLengthPercent: 30 } } } </script> <style scoped lang="scss"> .page { height: 100%; padding: 10px; background: #000; } </style>
到此這篇關於vue使用Split封裝通用拖拽滑動分隔面板元件 的文章就介紹到這了,更多相關vue 拖拽滑動分隔面板 內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!