一个指令实现左右拖动改变布局,附详细代码

前端下午茶

共 14118字,需浏览 29分钟

 · 2023-08-28

作者:灵扁扁

原文:https://juejin.cn/post/7245936314851377210

一、前言

本文以实现“一个指令实现左右拖动改变页面布局”的需求为例,介绍了:

  1. 实现思路
  2. 总结关键技术点
  3. 完整 demo

二、实现思路

2.1 外层div布局

首先设置4个div元素,一个作为父容器,一个作为左边的容器,一个在中间作为拖动指令承载的元素,最后一个在作为右边容器的元素。

      
      <div>
    <div class="left"></div>
    <div class="resize" v-resize="{left: 300, resize: 10}"></div>
    <div class="right"></div>
</div>

2.2 获取指令元素的父元素和兄弟元素

首先,接收指令传递的各元素的宽,并进行初始赋值和利用 calc 计算右边元素宽度。

      
      let leftWidth = binding.value?.left || 300
let resizeWidth = binding.value?.resize || 10
let rightWidth = `calc(100% - ${leftWidth + 10}px - ${resizeWidth}px)`

然后,接收指令传递下来的元素 el,并根据该元素 通过 Element.previousElementSibling 获取当前元素前一个兄弟元素,即是 class=" left" 所在的元素。 通过 Element.nextElementSibling 获取当前元素的后一个兄弟元素,即是 class="right" 所在的元素。 通过 Element.parentElement 获取当前元素的父元素。

      
      bind: function (el, binding, vnode{
    let resize = el
    let left = resize.previousElementSibling
    let right = resize.nextElementSibling
    let box = resize.parentElement
}

2.3 利用浮动定位,实现浮动布局

接着,给各个容器元素设置浮动定位 float = 'left'。当然,其实其他方式也可以的,只要能达到类似“行内块”的布局即可。

可以提一下的是,设置 float = 'left' 可以创建一个独立的 BFC 区域,具有“独立隔离性”, 即 BFC 区域内部元素的布局,不会“越界”影响外部元素的布局; 外部元素的布局也不会“穿透”,影响 BFC 区域的内部布局。

      
      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 实现鼠标按下时,将特定元素指定为未来指针事件的捕获目标

通过 onpointerdown 监听,实现实现鼠标按下时,将特定元素指定为未来指针事件的捕获目标,这个特定元素即 v-resize 指令所在的元素。

这样,就可以通过获取 v-resize 指令所在的元素的位置属性,来计算出左右的元素,在拖动时需要设置的宽和位置信息。

      
      resize.onpointerdown = function (e{
    let startX = e.clientX
    resize.left = resize.offsetLeft
    resize.setPointerCapture(e.pointerId);
    return false
}

2.5 实现鼠标移动时,改变左右的宽度

通过 onpointermove 监听,实现在鼠标指针移动时,获取鼠标事件的位置信息 clientX 等,并由此计算出合适的移动距离 moveLen, resize 的左边距离,left 元素的宽,以及 right 元素的宽。

由此,就实现了每移动一步,就重新计算出新的布局位置信息,并进行了赋值。

      
      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 鼠标抬起时,将鼠标指针从先前捕获的元素中释放

通过监听 onpointerup,实现在鼠标指针抬起时,通过 releasePointerCapture 将鼠标指针从先前捕获的元素中释放,还给鼠标自由。并将 resize 元素的 onpointermove 事件设置为 null。这样,当鼠标被抬起后,再操作就不会携带此前的绑定操作了。

      
      resize.onpointerup = function (evt{
    resize.onpointermove = null;
    resize.releasePointerCapture(evt.pointerId);
}

经过上诉步骤,我们就实现了,从鼠标按下,到移动计算改变布局,然后鼠标抬起释放绑定,操作完成,改变布局的目标达成。

三、总结关键技术点

实现本需求主要的关键技术点有:

3.1 setPointerCapture 和 releasePointerCapture

Element.setPointerCapture() 用于将特定元素指定为未来指针事件的捕获目标。 指针的后续事件将以捕获元素为目标,直到捕获被释放(通过 Element.releasePointerCapture())。

Element.releasePointerCapture() 则用来将鼠标从先前通过 Element.setPointerCapture() 绑定的元素身上释放出来,还给鼠标自由。

需要注意的是,类似的功能事件还有 setCapture() 和 releaseCapture,但它们已经被标记为弃用,且是非标准的,所以不建议使用。

3.2 onpointerdown,onpointermove 和 onpointerup

与上面配套的关键事件还有,onpointerdown,onpointermove 和 onpointerup。其中 onpointermove 是实现主要改变布局的逻辑的地方。

pointerdown:全局事件处理程序,当鼠标指针按下时触发。返回 pointerdown 事件触发对象的事件处理程序。

onpointermove:全局事件处理程序,当鼠标指针移动时触发。返回 targetElement 元素的 pointermove 事件处理函数。

onpointerup:全局事件处理程序,当鼠标指针抬起时触发。返回 targetElement 元素的pointerup事件处理函数。

3.3 注意事项

① Vue.nextTick 的使用。在 vue 指令定义的 bind 中使用了 Vue.nextTick,是为了解决初次运算时,有些 dom 元素未完成渲染,设置元素属性会报警告或错误。

      
      Vue.directive('resize', {
    bindfunction (el, binding, vnode{
        Vue.nextTick(() => {
            handler(el, binding, vnode)
        })
    }
})

② position = 'relative' 的设置。给每个元素 left 和 right 元素设置 position = 'relative',是为了解决 z-index 可能会失效的问题,我们知道有时浮动元素会导致这种情形发生。 当然这并不影响本次需求的实现,是为了其他设计考虑才这样做的。

      
      left.style.position = 'relative'
resize.style.position = 'relative'
right.style.position = 'relative'

③ cursor = 'col-resize' 的设置。为了获得更友好的体验,使得用户一眼鉴别这个功能,我们使用了 cursor 的 col-resize 属性。

      
        resize.style.cursor = 'col-resize'

四、完整 demo

// 这是定义指令的完整代码:directive.js

      
      /**
 * 自定义调整宽度指令:添加指令后,可以实现拖拽边线改变页面元素的宽度。
 * 指令接收两个参数,left 左边元素的宽度,中间 resize 元素的宽度。数据类型均为 number
 * 使用示例:
 * <div>
 *      <div></div>
 *      <div v-resize="{left: 300, resize: 10}" />
 *      <div></div>
 * </div>
 *
 * 注意:由于是使用 float 布局,所以需要保证有4个元素作为浮动元素的容器,即父容器 1 个,子容器 3 个。
 * 具体使用请参考 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', {
        bindfunction (el, binding, vnode{
            Vue.nextTick(() => {
                handler(el, binding, vnode)
            })
        },
        updatefunction (el, binding{
            handler(el, binding)
        },
        unbindfunction (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;
        height50vh;
    }

    .resize {
        background#EEEEEE;
        height50vh;
    }

    .right {
        background#1e87f0;
        height50vh;
    }
</style>

最后



如果你觉得这篇内容对你挺有启发,我想邀请你帮我个小忙:

  1. 点个「喜欢」或「在看」,让更多的人也能看到这篇内容

  2. 我组建了个氛围非常好的前端群,里面有很多前端小伙伴,欢迎加我微信「sherlocked_93」拉你加群,一起交流和学习

  3. 关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。



882931f6dba3e33fad8b5d431cfc3b8d.webp点个喜欢支持我吧,在看就更好了


浏览 38
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报