一個(gè)指令實(shí)現(xiàn)左右拖動(dòng)改變布局,附詳細(xì)代碼
作者:靈扁扁
原文:https://juejin.cn/post/7245936314851377210
一、前言
本文以實(shí)現(xiàn)“一個(gè)指令實(shí)現(xiàn)左右拖動(dòng)改變頁面布局”的需求為例,介紹了:
- 實(shí)現(xiàn)思路
- 總結(jié)關(guān)鍵技術(shù)點(diǎn)
- 完整 demo
二、實(shí)現(xiàn)思路
2.1 外層div布局
首先設(shè)置4個(gè)div元素,一個(gè)作為父容器,一個(gè)作為左邊的容器,一個(gè)在中間作為拖動(dòng)指令承載的元素,最后一個(gè)在作為右邊容器的元素。
<div>
<div class="left"></div>
<div class="resize" v-resize="{left: 300, resize: 10}"></div>
<div class="right"></div>
</div>
2.2 獲取指令元素的父元素和兄弟元素
首先,接收指令傳遞的各元素的寬,并進(jìn)行初始賦值和利用 calc 計(jì)算右邊元素寬度。
let leftWidth = binding.value?.left || 300
let resizeWidth = binding.value?.resize || 10
let rightWidth = `calc(100% - ${leftWidth + 10}px - ${resizeWidth}px)`
然后,接收指令傳遞下來的元素 el,并根據(jù)該元素 通過 Element.previousElementSibling 獲取當(dāng)前元素前一個(gè)兄弟元素,即是 class=" left" 所在的元素。 通過 Element.nextElementSibling 獲取當(dāng)前元素的后一個(gè)兄弟元素,即是 class="right" 所在的元素。 通過 Element.parentElement 獲取當(dāng)前元素的父元素。
bind: function (el, binding, vnode) {
let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement
}
2.3 利用浮動(dòng)定位,實(shí)現(xiàn)浮動(dòng)布局
接著,給各個(gè)容器元素設(shè)置浮動(dòng)定位 float = 'left'。當(dāng)然,其實(shí)其他方式也可以的,只要能達(dá)到類似“行內(nèi)塊”的布局即可。
可以提一下的是,設(shè)置 float = 'left' 可以創(chuàng)建一個(gè)獨(dú)立的 BFC 區(qū)域,具有“獨(dú)立隔離性”, 即 BFC 區(qū)域內(nèi)部元素的布局,不會“越界”影響外部元素的布局; 外部元素的布局也不會“穿透”,影響 BFC 區(qū)域的內(nèi)部布局。
let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement
box.style.float = 'left'
left.style.float = 'left'
resize.style.float = 'left'
right.style.float = 'left'
2.4 實(shí)現(xiàn)鼠標(biāo)按下時(shí),將特定元素指定為未來指針事件的捕獲目標(biāo)
通過 onpointerdown 監(jiān)聽,實(shí)現(xiàn)實(shí)現(xiàn)鼠標(biāo)按下時(shí),將特定元素指定為未來指針事件的捕獲目標(biāo),這個(gè)特定元素即 v-resize 指令所在的元素。
這樣,就可以通過獲取 v-resize 指令所在的元素的位置屬性,來計(jì)算出左右的元素,在拖動(dòng)時(shí)需要設(shè)置的寬和位置信息。
resize.onpointerdown = function (e) {
let startX = e.clientX
resize.left = resize.offsetLeft
resize.setPointerCapture(e.pointerId);
return false
}
2.5 實(shí)現(xiàn)鼠標(biāo)移動(dòng)時(shí),改變左右的寬度
通過 onpointermove 監(jiān)聽,實(shí)現(xiàn)在鼠標(biāo)指針移動(dòng)時(shí),獲取鼠標(biāo)事件的位置信息 clientX 等,并由此計(jì)算出合適的移動(dòng)距離 moveLen, resize 的左邊距離,left 元素的寬,以及 right 元素的寬。
由此,就實(shí)現(xiàn)了每移動(dòng)一步,就重新計(jì)算出新的布局位置信息,并進(jìn)行了賦值。
resize.onpointermove = function (e) {
let endX = e.clientX
let moveLen = resize.left + (endX - startX)
let maxT = box.clientWidth - resize.offsetWidth
const step = 100
if (moveLen < step) moveLen = step
if (moveLen > maxT - step) moveLen = maxT - step
resize.style.left = moveLen
resize.style.width = resizeWidth
left.style.width = moveLen + 'px'
right.style.width = (box.clientWidth - moveLen - resizeWidth - 2) + 'px'
}
2.6 鼠標(biāo)抬起時(shí),將鼠標(biāo)指針從先前捕獲的元素中釋放
通過監(jiān)聽 onpointerup,實(shí)現(xiàn)在鼠標(biāo)指針抬起時(shí),通過 releasePointerCapture 將鼠標(biāo)指針從先前捕獲的元素中釋放,還給鼠標(biāo)自由。并將 resize 元素的 onpointermove 事件設(shè)置為 null。這樣,當(dāng)鼠標(biāo)被抬起后,再操作就不會攜帶此前的綁定操作了。
resize.onpointerup = function (evt) {
resize.onpointermove = null;
resize.releasePointerCapture(evt.pointerId);
}
經(jīng)過上訴步驟,我們就實(shí)現(xiàn)了,從鼠標(biāo)按下,到移動(dòng)計(jì)算改變布局,然后鼠標(biāo)抬起釋放綁定,操作完成,改變布局的目標(biāo)達(dá)成。
三、總結(jié)關(guān)鍵技術(shù)點(diǎn)
實(shí)現(xiàn)本需求主要的關(guān)鍵技術(shù)點(diǎn)有:
3.1 setPointerCapture 和 releasePointerCapture
Element.setPointerCapture() 用于將特定元素指定為未來指針事件的捕獲目標(biāo)。 指針的后續(xù)事件將以捕獲元素為目標(biāo),直到捕獲被釋放(通過 Element.releasePointerCapture())。
Element.releasePointerCapture() 則用來將鼠標(biāo)從先前通過 Element.setPointerCapture() 綁定的元素身上釋放出來,還給鼠標(biāo)自由。
需要注意的是,類似的功能事件還有 setCapture() 和 releaseCapture,但它們已經(jīng)被標(biāo)記為棄用,且是非標(biāo)準(zhǔn)的,所以不建議使用。
3.2 onpointerdown,onpointermove 和 onpointerup
與上面配套的關(guān)鍵事件還有,onpointerdown,onpointermove 和 onpointerup。其中 onpointermove 是實(shí)現(xiàn)主要改變布局的邏輯的地方。
pointerdown:全局事件處理程序,當(dāng)鼠標(biāo)指針按下時(shí)觸發(fā)。返回 pointerdown 事件觸發(fā)對象的事件處理程序。
onpointermove:全局事件處理程序,當(dāng)鼠標(biāo)指針移動(dòng)時(shí)觸發(fā)。返回 targetElement 元素的 pointermove 事件處理函數(shù)。
onpointerup:全局事件處理程序,當(dāng)鼠標(biāo)指針抬起時(shí)觸發(fā)。返回 targetElement 元素的pointerup事件處理函數(shù)。
3.3 注意事項(xiàng)
① Vue.nextTick 的使用。在 vue 指令定義的 bind 中使用了 Vue.nextTick,是為了解決初次運(yùn)算時(shí),有些 dom 元素未完成渲染,設(shè)置元素屬性會報(bào)警告或錯(cuò)誤。
Vue.directive('resize', {
bind: function (el, binding, vnode) {
Vue.nextTick(() => {
handler(el, binding, vnode)
})
}
})
② position = 'relative' 的設(shè)置。給每個(gè)元素 left 和 right 元素設(shè)置 position = 'relative',是為了解決 z-index 可能會失效的問題,我們知道有時(shí)浮動(dòng)元素會導(dǎo)致這種情形發(fā)生。 當(dāng)然這并不影響本次需求的實(shí)現(xiàn),是為了其他設(shè)計(jì)考慮才這樣做的。
left.style.position = 'relative'
resize.style.position = 'relative'
right.style.position = 'relative'
③ cursor = 'col-resize' 的設(shè)置。為了獲得更友好的體驗(yàn),使得用戶一眼鑒別這個(gè)功能,我們使用了 cursor 的 col-resize 屬性。
resize.style.cursor = 'col-resize'
四、完整 demo
// 這是定義指令的完整代碼:directive.js
/**
* 自定義調(diào)整寬度指令:添加指令后,可以實(shí)現(xiàn)拖拽邊線改變頁面元素的寬度。
* 指令接收兩個(gè)參數(shù),left 左邊元素的寬度,中間 resize 元素的寬度。數(shù)據(jù)類型均為 number
* 使用示例:
* <div>
* <div></div>
* <div v-resize="{left: 300, resize: 10}" />
* <div></div>
* </div>
*
* 注意:由于是使用 float 布局,所以需要保證有4個(gè)元素作為浮動(dòng)元素的容器,即父容器 1 個(gè),子容器 3 個(gè)。
* 具體使用請參考 src/views/dataAsset/dataWarehouse/index.vue 或 src/views/dataModeling/themeDesign/index.vue
*/
import Vue from 'vue'
const resizeDirective = {}
const handler = (el, binding, vnode) => {
let leftWidth = binding.value?.left || 300
let resizeWidth = binding.value?.resize || 10
let rightWidth = `calc(100% - ${leftWidth + 10}px - ${resizeWidth}px)`
if (binding.value?.left && Object.prototype.toString.call(binding.value?.left) !== '[object Number]') {
console.error(`${binding.value.left} Must be Number`)
}
if (binding.value?.resize && Object.prototype.toString.call(binding.value?.resize) !== '[object Number]') {
console.error(`${binding.value.left} Must be Number`)
}
let resize = el
let left = resize.previousElementSibling
let right = resize.nextElementSibling
let box = resize.parentElement
box.style.float = 'left'
box.style.height = '100%'
box.style.width = '100%'
box.style.overflow = 'hidden'
left.style.float = 'left'
left.style.width = leftWidth + 'px'
left.style.position = 'relative'
resize.style.float = 'left'
resize.style.cursor = 'col-resize'
resize.style.width = resizeWidth + 'px'
resize.style.height = box.offsetHeight + 'px'
resize.style.position = 'relative'
right.style.float = 'left'
right.style.width = rightWidth
right.style.position = 'relative'
right.style.zIndex = 99
resize.onpointerdown = function (e) {
let startX = e.clientX
resize.left = resize.offsetLeft
resize.onpointermove = function (e) {
let endX = e.clientX
let moveLen = resize.left + (endX - startX)
let maxT = box.clientWidth - resize.offsetWidth
const step = 100
if (moveLen < step) moveLen = step
if (moveLen > maxT - step) moveLen = maxT - step
resize.style.left = moveLen
resize.style.width = resizeWidth
left.style.width = moveLen + 'px'
right.style.width = (box.clientWidth - moveLen - resizeWidth - 2) + 'px'
}
resize.onpointerup = function (evt) {
resize.onpointermove = null;
resize.releasePointerCapture(evt.pointerId);
}
resize.setPointerCapture(e.pointerId);
return false
}
}
resizeDirective.install = Vue => {
Vue.directive('resize', {
bind: function (el, binding, vnode) {
Vue.nextTick(() => {
handler(el, binding, vnode)
})
},
update: function (el, binding) {
handler(el, binding)
},
unbind: function (el, binding) {
el.instance && el.instance.$destroy()
}
})
}
export default resizeDirective
// 在 main.js 中使用
import resizeDirective from './directive'
Vue.use(resizeDirective)
// 在具體頁面中使用:ResizeWidth.vue
<template>
<div>
<div class="left">left</div>
<div class="resize" v-resize="{left: 300, resize: 10}"></div>
<div class="right">right</div>
</div>
</template>
<script>
export default {
name: 'ResizeWidth'
}
</script>
<style scoped>
.left {
background: #42b983;
height: 50vh;
}
.resize {
background: #EEEEEE;
height: 50vh;
}
.right {
background: #1e87f0;
height: 50vh;
}
</style>
最后
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我個(gè)小忙:
-
點(diǎn)個(gè)「喜歡」或「在看」,讓更多的人也能看到這篇內(nèi)容
-
我組建了個(gè)氛圍非常好的前端群,里面有很多前端小伙伴,歡迎加我微信「sherlocked_93」拉你加群,一起交流和學(xué)習(xí)
-
關(guān)注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。
