前言
本例是在React中實現,不過改一改通過原生js也很好實現,另外兼容性也做到了IE9。(IE8講道理也是可以的)。
首先看一下需要實現的需求:
要拖動圖中的白色橫條調整綠色和藍色區域的高度,要拖動白色豎條調整左邊區域和紅色區域的寬度。
一兩年前曾經遇到過這個需求,當時直接在網上搜了個解決方案貼上去了,不過那個解決方案很挫。
這次的項目又遇到這個需求,而且是三個塊的拖動。不僅需要左右拖動還需要上下拖動。
在這里特地記錄下解決方案,也希望可以得到一些反饋與優化。
方案的思路
橫條拖動和豎條拖動原理差不多,那就先來實現豎條左右拖動調整寬度。
水平方向的布局是通過以下方式實現:
.left{
width: 500px;
height: 100%;
float: left;
position: relative;
}
.v-resize{
height: 100%;
width: 4px;
position: absolute;
background: #fff;
left: 500px;
z-index: 2;
cursor: col-resize;
user-select: none;
}
.right{
margin-left: 504px;
background-color: lightsalmon;
height: 100%;
}
通過樣式我們可以了解到,只要同時改變left塊的寬度,白色豎條v-resize的left定位和right塊的定位(這個數相差不大,我們將這個數定義為vNum),即可實現水平拖動的效果。
通過鼠標按下白色豎條,開啟水平拖動,監控鼠標位置,計算vNum,在鼠標放開或者鼠標移出拖動區域時停止水平拖動。
計算vNum的本質實際上就是通過鼠標位置減去拖動區域離瀏覽器左側位置,從而得到vNum。
因為每塊區域都有最大和最小寬度的限制,這里僅僅加了個vNumLimit來進行限制,即豎條最少要離最左側和最右側的距離。
這里還要考慮到每次窗口resize時,各個參數可能都會有所調整,所以加了窗口resize的處理,當然也加了防抖。
本來想要分步講解,但是冬寒人乏,懶病發作。
方案的實現
jsx部分
import React, { Component } from 'react'
import _ from 'underscore'
import styles from './index.css'
// 可調整寬高的Div
export default class ResizeDiv extends Component {
state = {
isHResize: false,
isVResize: false,
hNum: 100,
vNum: 500,
hNumLimit: 30,
vNumLimit: 30
}
resizeOffsetInfo = {
clientTop: 0,
clientLeft: 0
}
leftHeight = 0
containerWidth = 0
componentDidMount() {
this.initResizeInfo()
const throttled = _.throttle(() => {
this.initResizeInfo()
}, 200)
window.onresize = throttled
}
componentWillUnmount() {
window.onresize = null
}
/**
* 初始化resize信息
*/
initResizeInfo = () => {
const hEle = document.getElementById('h_resize_container')
this.resizeOffsetInfo = this.getEleOffset(hEle)
this.leftHeight = hEle.offsetHeight
this.containerWidth = document.getElementById('v_resize_container').offsetWidth
}
/**
* 獲取元素的偏移信息
*/
getEleOffset(ele) {
var clientTop = ele.offsetTop
var clientLeft = ele.offsetLeft
let current = ele.offsetParent
while (current !== null) {
clientTop += current.offsetTop
clientLeft += current.offsetLeft
current = current.offsetParent
}
return {
clientTop,
clientLeft,
height: ele.offsetHeight,
width: ele.offsetWidth
}
}
/**
* 開始拖動水平調整塊
*/
hResizeDown = () => {
this.setState({
isHResize: true
})
}
/**
* 拖動水平調整塊
*/
hResizeOver = (e) => {
const { isHResize, hNum, hNumLimit } = this.state
if (isHResize && hNum >= hNumLimit && (this.resizeOffsetInfo.height - hNum >= hNumLimit)) {
let newValue = this.resizeOffsetInfo.clientTop + this.resizeOffsetInfo.height - e.clientY
if (newValue < hNumLimit) {
newValue = hNumLimit
}
if (newValue > this.resizeOffsetInfo.height - hNumLimit) {
newValue = this.resizeOffsetInfo.height - hNumLimit
}
this.setState({
hNum: newValue
})
}
}
/**
* 開始拖動垂直調整塊
*/
vResizeDown = () => {
this.setState({
isVResize: true
})
}
/**
* 拖動垂直調整塊
*/
vResizeOver = (e) => {
const { isVResize, vNum, vNumLimit } = this.state
if (isVResize && vNum >= vNumLimit && (this.containerWidth - vNum >= vNumLimit)) {
let newValue = e.clientX - this.resizeOffsetInfo.clientLeft
if (newValue < vNumLimit) {
newValue = vNumLimit
}
if (newValue > this.containerWidth - vNumLimit) {
newValue = this.containerWidth - vNumLimit
}
this.setState({
vNum: newValue
})
}
}
/**
* 只要鼠標松開或者離開區域,那么就停止resize
*/
stopResize = () => {
this.setState({
isHResize: false,
isVResize: false
})
}
render() {
const hCursor = this.state.isHResize ? 'row-resize' : 'default'
const hColor = this.state.isHResize ? '#ddd' : '#fff'
const vCursor = this.state.isVResize ? 'col-resize' : 'default'
const vColor = this.state.isVResize ? '#ddd' : '#fff'
return (
<div className={styles['container']} onMouseUp={this.stopResize} onMouseLeave={this.stopResize}>
<div id='v_resize_container' className={styles['content']} onMouseMove={this.vResizeOver}>
<div id='h_resize_container' style={{ width: this.state.vNum, cursor: vCursor }} className={styles['left']}
onMouseMove={this.hResizeOver}>
<div style={{ bottom: this.state.hNum, cursor: hCursor }} className={styles['left-top']}>aasd</div>
<div style={{ bottom: this.state.hNum, backgroundColor: hColor }} draggable={false} onMouseDown={this.hResizeDown} className={styles['h-resize']} />
<div style={{ height: this.state.hNum + 4, cursor: hCursor }} className={styles['left-bottom']}>asd</div>
</div>
<div style={{ left: this.state.vNum, backgroundColor: vColor }} draggable={false} onMouseDown={this.vResizeDown} className={styles['v-resize']} />
<div style={{ marginLeft: this.state.vNum + 4, cursor: vCursor }} className={styles['right']}>
asdas
</div>
</div>
</div>
)
}
}
css部分
.container{
margin: 30px;
overflow: hidden;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.content{
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
min-height: 300px;
}
.left{
width: 500px;
height: 100%;
float: left;
position: relative;
}
.left-top{
position: absolute;
top: 0;
bottom: 104px;
width: 100%;
background-color: lightblue;
}
.h-resize{
height: 4px;
width: 100%;
background: #fff;
position: absolute;
bottom: 100px;
z-index: 1;
cursor: row-resize;
user-select: none;
}
.left-bottom{
position: absolute;
bottom: 0;
width: 100%;
height: 100px;
background-color: lightgreen;
}
.v-resize{
height: 100%;
width: 4px;
position: absolute;
background: #fff;
left: 500px;
z-index: 2;
cursor: col-resize;
user-select: none;
}
.right{
margin-left: 504px;
background-color: lightsalmon;
height: 100%;
}
總結
技術上其實還是比較簡單的,不過絲般潤滑的左右移動還是挺有成就感的。
如果有更好的玩法還望不吝賜教。
這是這個demo的地址:demo地址