分享一个不侵入全自动容器元素伸缩分隔条组件

159 阅读4分钟

本插件为纯js代码,非react、vue组件。(不过可以自己稍做改动实现)

公司有几个老系统,界面有左右布局的,有上下布局,还有组合布局的。为了提升用户体验,提出了一个需求,就是操作区域可伸缩。

网上找了一些组件,大部分需要改现有布局代码或者库的方向不精准用起来别扭。想想这种场景还挺多的,索性自己实现了一个。

目标:

  1. 不动现有html容器元素代码,不改布局。
  2. 用法简单,全自动。
  3. 支持样式配置。

先看用法:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>example</title>
        <script src="./flexible-bar.js"></script>
    </head>
    <body style="margin:0;padding:0;">
        <div style="width:100%;height:100vh;display: flex;flex-direction: column;">
            <div style="height:100px;background-color:#eee;">
                Top Area
            </div>
            <flexible-bar></flexible-bar>
            <div style="flex:1;display: flex;">

                <!-- 左侧栏 -->
                <div style="width:100px;background-color:#ffe;"> div 1 </div>
                <flexible-bar size="10px" lineColor="#409eff" handleColor="white" hoverShadow="0 0 3px #333"></flexible-bar>
                <!-- 右侧栏 -->
                <div style="width:400px;background-color:#ffe;overflow: hidden;">

                    <!-- 右上 -->
                    <div style="width:100%;height:50%;background:#fef;"> div 2 </div>
                    <flexible-bar handleColor="#409eff"></flexible-bar>
                    <!-- 右下 -->
                    <div style="width:100%;height:50%;background:#eff;"> div 3 </div>

                </div>
            </div>
        </div>
    </body>
</html>

只需要在两个容器元素之间添加一行代码: <flexible-bar></flexible-bar>

效果:

flexible-bar-example.png

这样就可以用鼠标拖拽对各区域进行尺寸控制了。

可配置属性:

lineColor:分隔条的颜色,默认#ddd
size: 分隔条的粗细值,默认3px
handle:是否显示手柄,默认yes
handleColor:手柄的颜色,默认white
hoverShadow:鼠标移动到分隔条时显示的阴影

用法示例:

<flexible-bar size="10px" lineColor="#409eff" handleColor="white" hoverShadow="0 0 3px #333"></flexible-bar>

组件源代码


/**
 * flexible-bar.js 1.0 容器元素伸缩条
 * thejava@163.com
 * 
 * 用法示例:
 * <flexible-bar size="10px" lineColor="#409eff" handleColor="white" hoverShadow="0 0 3px #333"></flexible-bar>
 * 
 * 可配置属性:
 * lineColor:分隔条的颜色,默认#ddd
 * size: 分隔条的粗细值,默认3px
 * handle:是否显示手柄,默认yes
 * handleColor:手柄的颜色,默认white
 * hoverShadow:鼠标移动到分隔条时显示的阴影
 */
(() => {
    function flexibleBar(fb) {
        const prev = fb.previousElementSibling // 上一个兄弟节点
        const next = fb.nextElementSibling // 下一个兄弟节点
        console.log('prev', prev)
        console.log('next', next)
        if (!prev || !next) {
            throw new Error('<flexible-bar>必须放到两个元素之间')
        }

        /**
         * 判断横向还是纵向,
         * 若上一个的bottom大于等于下一个的top,则是纵向布局的,
         * 若上一个的right大于等于下一个的left,则是左右布局的,
         * 若两个情况同时满足,那可能是以下这种情况:
         * ---------
         *   口
         *      口
         * ---------
         * 这种情况无法支持,抛出错误。
         * 
         * 原理:
         * 通过控件所放位置的兄弟节点来生成伸缩条,点住条子拖动时动态改变兄弟节点的宽度或高度实现伸缩效果。
         * 点击伸缩条时在伸缩条dom里生成一个position:absolute的div节点,高宽度足够大,用来接收鼠标移动事件。全透明,无边框。鼠标按键松开时取消div样式。
         * 可以在伸缩条上拖拽,也可以在中间手柄上拖拽,手柄也是一个position:absolute的div节点,做了鼠标事件转移,在上面按住鼠标时会mousedown事件转给伸缩条。
         * 
         * 可配置属性:
         * lineColor:分隔条的颜色,默认#ddd
         * size: 分隔条的粗细值,默认3px
         * handle:是否显示手柄,默认yes
         * handleColor:手柄的颜色,默认white
         * hoverShadow:鼠标移动到分隔条时显示的阴影
         */

        let prevRect = prev.getBoundingClientRect()
        let nextRect = next.getBoundingClientRect()

        let direction = ''
        if (prevRect.bottom <= nextRect.top) {
            direction += 'column'
        }
        if (prevRect.right <= nextRect.left) {
            direction += 'row'
        }

        if (direction.includes('column') && direction.includes('row')) {
            throw new Error('布局不支持伸缩条')
        }

        let separatorBackground = fb.getAttribute('lineColor') || '#ddd'
        let size = fb.getAttribute('size') || '3px'
        let showHandle = fb.getAttribute('handle') || 'yes'
        let handleBackground = fb.getAttribute('handleColor') || 'white'
        let hoverShadow = fb.getAttribute('hoverShadow') || ''


        
        let separatorCssText = `display:flex;justify-content: center;align-items: center;background: ${separatorBackground};position:relative;transition:box-shadow .5s ease;`
        // pointer-events:none;
        let handleCssText = `position:absolute;background:${handleBackground};border-radius:15px;box-shadow: 0 0 2px #000;display:flex;justify-content: center;align-items: center;`
        let handleSymbolCssText = ``
        

        // 分隔栏
        const separator = document.createElement('div')
        separator.style.position = 'absolute'
        separator.style.width = '100%'
        separator.style.height = '100%'
        // separator.style.background = '#0008'
        

        if (direction === 'column') {
            // 取两个块最大的那个块的边作为伸缩柄的尺寸
            const width = prevRect.width > nextRect.width ? prevRect.width : nextRect.width
            separatorCssText += `width:${width}px;height:${size};cursor: ns-resize;`
            handleCssText += `width:50px;height:10px;flex-direction: column;`
            handleSymbolCssText = `width:60%;height:1px;background:#aaa;margin:1px;`
        }
        
        if (direction === 'row') {
            // 取两个块最大的那个块的边作为伸缩柄的尺寸
            const height = prevRect.height > nextRect.height ? prevRect.height : nextRect.height
            separatorCssText += `width:${size};height:${height}px;cursor: ew-resize;`
            handleCssText += `width:10px;height:50px;flex-direction: row;`
            handleSymbolCssText = `width:1px;height:60%;background:#aaa;margin:1px;`
        }


        let mouseDownPageX = 0
        let mouseDownPageY = 0

        let mouseStatus = 'up'
        let handleMouseDowned = false
        separator.onmousedown = (e) => {
            mouseStatus = 'down'
            if (!handleMouseDowned) {
                mouseDownPageX = e.pageX
                mouseDownPageY = e.pageY
            }
            // console.log('mouseDownPageX: ', mouseDownPageX, 'mouseDownPageY: ', mouseDownPageY)

            prevRect = prev.getBoundingClientRect()
            nextRect = next.getBoundingClientRect()
            separator.style.zIndex = 65535
            if (direction === 'column') {
                separator.style.width = '100%'
                separator.style.height = '500px'
            } else if (direction === 'row') {
                separator.style.height = '100%'
                separator.style.width = '500px'
            }
            e.preventDefault()
        }

        let cha = 0
        let newPrevHeight = 0
        let newNextHeight = 0
        let newPrevWidth = 0
        let newNextWidth = 0
        separator.onmousemove = (e) => {
            handleMouseDowned = false
            // console.log(e.pageX, e.pageY)
            if (mouseStatus !== 'down') {
                return
            }
            if (direction === 'column') {
                cha = e.pageY - mouseDownPageY
                newPrevHeight = prevRect.height + cha
                newNextHeight = nextRect.height - cha
                if (newPrevHeight < 2 || newNextHeight < 2) {
                    return
                }
                prev.style.height = newPrevHeight + 'px'
                next.style.height = newNextHeight + 'px'
            } else if (direction === 'row') {
                cha = e.pageX - mouseDownPageX
                newPrevWidth = prevRect.width + cha
                newNextWidth = nextRect.width - cha
                if (newPrevWidth < 2 || newNextWidth < 2) {
                    return
                }
                prev.style.width = newPrevWidth + 'px'
                next.style.width = newNextWidth + 'px'
            }
        }

        const resetHandle = () => {
            separator.style.width = '100%'
            separator.style.height = '100%'
            separator.style.zIndex = 'unset'
        }

        separator.onmouseup = (e) => {
            mouseStatus = 'up'
            handleMouseDowned = false
            resetHandle()
            e.preventDefault()
        }
        
        separator.onmouseenter = (e) => {
            // console.log('separator.onmouseenter')
            fb.style.boxShadow = hoverShadow
        }

        separator.onmouseleave = (e) => {
            // console.log('separator.onmouseleave')
            fb.style.boxShadow = 'unset'
            mouseStatus = 'leave'
            resetHandle()
            e.preventDefault()
        }

        fb.style.cssText = separatorCssText
        fb.appendChild(separator)

        if (showHandle === 'yes') {
            
            // 拖拽手柄
            const handle = document.createElement('div')
            const line1 = document.createElement('div')
            const line2 = document.createElement('div')
            line1.style.cssText = handleSymbolCssText
            line2.style.cssText = handleSymbolCssText
            
            handle.appendChild(line1)
            handle.appendChild(line2)
            handle.style.cssText = handleCssText
    
            handle.onmousedown = (e) => {
                e.preventDefault()
                handleMouseDowned = true
                mouseDownPageX = e.pageX
                mouseDownPageY = e.pageY
                var event = new MouseEvent('mousedown', {
                    bubbles: true, // 是否允许事件冒泡
                    cancelable: true, // 是否可以取消事件默认行为
                    view: window
                });
                separator.dispatchEvent(event);
            }
            handle.onmouseenter = (e) => {
                fb.style.boxShadow = hoverShadow
            }
            handle.onmouseleave = (e) => {
                fb.style.boxShadow = 'unset'
            }
            fb.appendChild(handle)
        }

    }


    // 初始化
    window.addEventListener('load', () => {
        const flexibles = document.querySelectorAll('flexible-bar')
        flexibles.forEach(fb => {
            flexibleBar(fb)
        })
    })

    // 若发生窗体变动则删除已生成的dom,重新初始化
    window.addEventListener('resize', () => {
        const flexibles = document.querySelectorAll('flexible-bar')
        flexibles.forEach(fb => {
            fb.childNodes.forEach(child => {
                fb.removeChild(child)
            })
            flexibleBar(fb)
        })
    })

})()

这是一个快速实现的版本,代码不漂亮,能用吧,看不惯请自行优化。

最后:
大龄程序员求成都工作,目前base北京,全栈能力,质量高,要求低。