1. 程式人生 > >實現可調整寬高的DIV(左右拖動和上下拖動)

實現可調整寬高的DIV(左右拖動和上下拖動)

前言

本例是在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在github上的地址:demo地址