前言
手動封裝一個類似Iview中的Split組件,可將一片區(qū)域,分割為可以拖拽調整寬度或高度的兩部分區(qū)域,最終效果如下:
開始
基礎布局
在vue工程中創(chuàng)建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組件由三部分組成:區(qū)域1,區(qū)域2,以及滑動器。添加這三個元素,并分別添加class名,注意.pane為區(qū)域1和區(qū)域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布局,區(qū)域2的flex屬性設為1,則區(qū)域2會根據(jù)區(qū)域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>
可以看到設置區(qū)域1的寬度變化就是實現(xiàn)該組件的核心點。
除了橫向還要支持縱向布局,所以給組件添加一個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> export default { props: { direction: { type: String, default: "row" } }, data() { return {} } } </script>
在橫向布局中,區(qū)域1設置width:50%,滑動器設置width:10px,而變?yōu)榭v向布局后這兩個width應該變?yōu)閔eight。所以刪除style中這兩個width設置,添加一個lengthType計算屬性,根據(jù)不同的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" } }
同時在橫向布局中,區(qū)域1,區(qū)域2,滑動器的height都為100%,在縱向布局下都應該改為width: 100%。所以刪除原本的height設置,將direction綁定為容器的一個class,根據(jù)該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 { height: 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",可以看到已經(jīng)變?yōu)榭v向
<template> <div class="page"> <SplitPane direction="column" /> </div> </template>
數(shù)據(jù)綁定
當前區(qū)域1的寬(高)度和滑動器的寬(高)度都是在樣式中寫死的,需要變?yōu)樵趈s中綁定才能進行操作,首先將能用于計算的數(shù)字放在data中
data() { return { paneLengthPercent: 50, // 區(qū)域1寬度 (%) triggerLength: 10 // 滑動器寬度 (px) } }
然后通過computed返回兩個樣式中需要的字符串,同時為了保證滑動器在區(qū)域1和區(qū)域2的正中間,區(qū)域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>
按下鼠標后開始滑動,應該監(jiān)聽mousemove事件,但注意不是在滑動器上,而是在整個文檔上監(jiān)聽,因為鼠標有可能滑動到頁面任何位置。當用戶松開鼠標時,應該取消對整個文檔mousemove的監(jiān)聽,所以在鼠標按下的那一刻,應該對document添加兩個事件:鼠標移動和鼠標松開
methods: { // 按下滑動器 handleMouseDown(e) { document.addEventListener("mousemove", this.handleMouseMove) document.addEventListener("mouseup", this.handleMouseUp) }, // 按下滑動器后移動鼠標 handleMouseMove(e) { console.log("拖動中") }, // 松開滑動器 handleMouseUp() { document.removeEventListener("mousemove", this.handleMouseMove) } }
我們實際要控制的是區(qū)域1的寬度,讓區(qū)域1的寬度等于當前鼠標距容器左邊的距離,也就是如果鼠標移動到下圖的圓圈位置,讓區(qū)域1的寬度等于中間的長度:
這個長度可以根據(jù)當前鼠標距頁面最左邊的距離減去容器距頁面最左邊的距離算出,也就是綠色長度等于紅色減藍色:
給容器添加ref為了獲取容器的dom信息
... <div ref="splitPane" class="split-pane" :class="direction" :style="{ flexDirection: direction }"> ...
如果打印ref的getBoundingClientRect()可以看到如下信息:
console.log(this.$refs.splitPane.getBoundingClientRect())
其中l(wèi)eft代表容器距離頁面左側的距離,width代表容器的寬度。
通過鼠標事件對象event的pageX可以獲得當前鼠標距頁面左側的距離,則我們要求的鼠標距容器左側距離就可以算出來了。
最后用這個距離除以容器寬度乘上100,就得到了這個距離的百分比數(shù)值,賦值給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 },
優(yōu)化
此時看上去需求已經(jīng)完成,但作為一個通用組件還有幾個要優(yōu)化的地方。
優(yōu)化一 抖動問題
把滑動器寬度設置大一些后可以發(fā)現(xiàn)一個抖動問題如下:
在滑動器兩側按下后輕輕移動就會出現(xiàn)大幅偏移,因為現(xiàn)在的計算邏輯始終認為鼠標在滑動器的正中間,沒有把滑動器寬度考慮進去。
在dota中定義一個當前鼠標距滑動器左(頂)側偏移量
data() { return { paneLengthPercent: 50, // 區(qū)域1寬度 (%) triggerLength: 100, // 滑動器寬度 (px) triggerLeftOffset: 0 // 鼠標距滑動器左(頂)側偏移量 } }
這個值等于鼠標距頁面左側距離減去滑動器距頁面左側距離(通過e.srcElement.getBoundingClientRect()),在每次滑動器被按下時進行賦值,也要區(qū)分橫向/縱向布局:紅 - 藍 = 綠
// 按下滑動器 handleMouseDown(e) { document.addEventListener("mousemove", this.handleMouseMove) document.addEventListener("mouseup", this.handleMouseUp) if (this.direction === "row") { this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left } else { this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top } },
有了這個triggerLeftOffset,設置區(qū)域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 },
此時不再有抖動問題
優(yōu)化二 鼠標樣式
鼠標在滑動器上經(jīng)過時應該改變樣式告訴用戶可以拖動,分別在橫向布局與縱向布局的滑動器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; // 這里 } } .pane-one { background: palevioletred; } .pane-trigger { background: palegoldenrod; } .pane-two { flex: 1; background: turquoise; } } </style>
優(yōu)化三 滑動限制
作為一個通用組件,應該向外部提供設置滑動最小與最大距離的限制功能,接收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 }
優(yōu)化四 面板默認寬度和滑動器寬度
還是作為一個通用組件,面板初始化比例與滑動器寬度應該也由外部使用者決定。
將data中的paneLengthPercent 和 triggerLength轉移到props中,從外部接收。
props: { direction: { type: String, default: "row" }, min: { type: Number, default: 10 }, max: { type: Number, default: 90 }, paneLengthPercent: { type: Number, default: 50 }, triggerLength: { type: Number, default: 10 } }, data() { return { triggerLeftOffset: 0 // 鼠標距滑動器左(頂)側偏移量 } },
在頁面中則需傳入paneLengthPercent,注意paneLengthPercent必須是一個定義在data中的數(shù)據(jù),并且要加上.sync修飾符,因為這個值要動態(tài)修改。
// page.vue <template> <div class="page"> <SplitPane direction="row" :paneLengthPercent.sync="paneLengthPercent" /> </div> </template> ... data() { return { paneLengthPercent: 30 } } ...
然后在組件中handleMouseMove中通過this.$emit觸發(fā)事件的方式修改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控制了。
優(yōu)化五 插槽
作為一個容器組件不能添加內容不是等于白費,分別給兩個區(qū)域添加兩個具名插槽。
<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>
優(yōu)化六 禁止選中
在拖動過程中,如果區(qū)域中有文字內容可能會出現(xiàn)選中文字的情況,給滑動器添加禁止選中效果。
... .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, default: "row" }, min: { type: Number, default: 10 }, max: { type: Number, default: 90 }, paneLengthPercent: { type: Number, default: 50 }, triggerLength: { type: Number, default: 10 } }, data() { return { triggerLeftOffset: 0 // 鼠標距滑動器左(頂)側偏移量 } }, computed: { lengthType() { return this.direction === "row" ? "width" : "height" }, paneLengthValue() { return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + "px"})` }, triggerLengthValue() { return this.triggerLength + "px" } }, methods: { // 按下滑動器 handleMouseDown(e) { document.addEventListener("mousemove", this.handleMouseMove) document.addEventListener("mouseup", this.handleMouseUp) if (this.direction === "row") { this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left } else { this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top } }, // 按下滑動器后移動鼠標 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) }, // 松開滑動器 handleMouseUp() { document.removeEventListener("mousemove", 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> 區(qū)域一 </div> </template> <template v-slot:two> <div> 區(qū)域二 </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 拖拽滑動分隔面板 內容請搜索服務器之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://juejin.cn/post/6941785036173606926