阅读 377

基于vue3实现的可视化元素放大缩小

github传送门: github.com/johnsonhoo0…

实现效果

最近迷上前端可视化的开发,加上最近vue3的学习,突然脑子发热想试试用vue3的方式封装一个简单的容器放大缩小功能的组件。先看看效果:

想法

类似于在一张纸上,我们称它为container;在这个container里面,用一个东西包住需要放大缩小的元素,让它可以自由放大缩小,正如上述动图所示。按照vue的语法,我想实现的样子大概如下:

<div class="container" ref="parentRef">
  <resize-able :parentRef="parentRef">
      <button>button</button>
  </resize-able>
</div>
复制代码

中间部分的resize-able就是我们需要封装的组件。外层的container可以换成我们需要的容器,例如可以是活页编辑器的容器等。

代码部分

前戏

在封装组件之前,先得实现一个小demo,让当前的元素可以放大缩小。

样式

可以从图上得出,样式主要是点击目标元素后,生成蓝色的边框以及边角的圆圈,拖动这些圆圈可以让目标元素往不同的角度进行放大缩小。很容易可以想到,这些圆圈一直都在四个边角和边中间,可以考虑用定位的方式解决。(重点小球的位置粗略用了49%来表示) 代码如下:

<div class="box">
    <div class="box-ball" 
        v-for="item in 8" 
        :data-i="item"
        :key="item"
        @mousedown="handleResize"></div>
</div>
<style scoped>
.draggable-container {
    width: 500px;
    height: 500px;
    border: 1px solid #aaa;
    position: relative;
    box-sizing: border-box;
}
.target-box {
    display: block;
    width: 300px;
    height: 100px;
    border: 1px solid #aaa;
    background: #eee;
    box-sizing: border-box;
    position: absolute;
    left: 100px;
    top: 20px;
} 
.target-can-move {
    cursor: all-scroll;
} 
.box-click {
    border: 1px solid blue;
} 
.box-ball {
    width: 8px;
    height: 8px;
    border: 1px solid blue;
    box-sizing: border-box;
    border-radius: 50%;
    position: absolute;
}
.box-ball:nth-child(1) {
    top: -8px;
    left: -8px;
    cursor: nw-resize;
}
.box-ball:nth-child(2) {
    top: -8px;
    left: calc(50% - 4px);
    cursor: n-resize;
}
.box-ball:nth-child(3) {
    top: -8px;
    right: -8px;
    cursor: nesw-resize;
}
.box-ball:nth-child(4) {
    top: calc(50% - 4px);
    left: -8px;
    cursor: w-resize;
}
.box-ball:nth-child(5) {
    top: calc(50% - 4px);
    right: -8px;
    cursor: w-resize;
}
.box-ball:nth-child(6) {
    left: -8px;
    bottom: -8px;
    cursor: nesw-resize;
}
.box-ball:nth-child(7) {
    left: calc(50% - 4px);
    bottom: -8px;
    cursor: n-resize;
}
.box-ball:nth-child(8) {
    right: -8px;
    bottom: -8px;
    cursor: nw-resize;
}
</style>
复制代码
鼠标事件

接下来就是鼠标事件了。onclickonmousedownonmousemoveonmouseup

  1. 当点击的时候激活目标元素(显示蓝色边框和小圈圈);
  2. 当鼠标按下的时候并且是在激活状态下,点击的事件target是小球还是元素本身,若是小球,则根据不同的小球按照鼠标移动的方向进行放大或者缩小;如果是元素本身,则表示拖动目标元素;
  3. 当鼠标松开的时候,代表不在移动或者放大缩小元素;
  4. 当点击最外层container空白的地方,则变成未激活状态。
<template>
    <div class="draggable-container" 
        @click="boxActive($event)"
        @mousedown="handleDown"
        @mousemove="handleMove"
        @mouseup="handleUp">
        <div ref="targetBox" 
            class="target-box"
            data-aim="canMove"
            :class="boxSelected ? 'box-click target-can-move' : ''"
            >
            <slot></slot>
            <div :class="boxSelected ? 'box-ball' : ''" 
                v-for="item in 8" 
                :data-i="item"
                :key="item"></div>
        </div>
    </div>
</template>
<script>
import { ref } from 'vue'
export default {
	setup() {
    	  // 目标元素宽度高度以及外容器的宽度高度
          const boxElement = reactive({
                  containerWidth: 500,
                  containerHeigth: 500,
                  width: 300,
                  height: 100,
                  left: 100,
                  top: 20
              })
    	   const boxSelected = ref(false)
           // 激活目标元素
           const boxActive = function(e) {
              if (e.target.className !== 'draggable-container') {
                  boxSelected.value = true
              } else {
                  boxSelected.value = false
              }
          }
          // 鼠标按下事件
          const handleDown = function(e) {
            if (boxSelected.value) {
                ballIdx.value = e.target.dataset.i
                // 判断鼠标按下的是小球
                if (ballIdx.value && ballIdx.value <= 8) {
                    mouseResizeDown.value = true
                    return
                }
                // 判断鼠标按下的是目标元素本身
                if (e.target.dataset.aim) {
                    mouseMove.value = true
                    return
                }
            }
        }
        // 松开鼠标事件
        const handleUp = function(e) {
            mouseResizeDown.value = false
            mouseMove.value = false
        }
        // 鼠标移动事件
        const handleMove = function(e) {
            if (mouseResizeDown.value) {
                // 对照的容器
                const contrast = e.target.className
                // 需要屏蔽小球的宽高bug
                if (e.offsetX <= 8 || e.offsetY <= 8) {
                    return
                }
                if (ballIdx.value) {
                    handleResize(contrast, ballIdx.value, e.offsetX, e.offsetY) // 下文阐述
                }
            } else if (mouseMove.value) {
                move(e.movementX, e.movementY) // 下文阐述
            }
        }
    }
}
</script>
复制代码
元素的放大缩小和移动

这里主要阐述下如何实现目标元素的放大缩小和元素的移动。即上述的handleResizemove方法。移动最主要是不要让元素移出容器外,这个比较简单。在移动的时候鼠标光标永远在目标元素上,因此鼠标光标移动的movementXmovementY则是其移动的距离,我们修改此时的lefttop值就可以改变当前元素位置了。
而对于目标元素的放大和缩小,需要注意: 以右下角的小圆圈为例: 鼠标在拖动元素放大缩小的时候,在光标移动的一瞬间,可能存在这三种不同的情况,需要分情况进行判断鼠标移动的方向:
a. 如果鼠标光标在目标元素内如1,则元素缩小,宽度是offsetX,高度是offsetY
b. 如果光标在小圈圈内如2,则不进行移动,因为小圈圈的偏移量非常小,可以忽略不计;
c. 如果光标在容器上如3,则元素放大,宽度是offsetX - left,高度是offsetY - top

// 移动目标元素
function move(movementX, movementY) {
	// 不让目标元素移动到容器外
    if (boxElement.width + boxElement.left + movementX > boxElement.containerWidth) {
        movementX = 0
    }
    if (boxElement.height + boxElement.top + movementY > boxElement.containerHeigth) {
        movementY = 0 
    }
    boxElement.left += movementX
    boxElement.top += movementY
    boxElement.left = boxElement.left < 0 ? 0 : boxElement.left 
    boxElement.top = boxElement.top < 0 ? 0 : boxElement.top
    // 更新目标元素位置
    updateStyle()
}

// 根据不同的球移动对容器拉伸收缩,currentBall为当前小圈圈,从左至右从上至下依次为1~8
function handleResize(contrast, currentBall, offsetX, offsetY) {
    if (currentBall === '8') {
        if (contrast === 'draggable-container') {
            boxElement.width = offsetX - boxElement.left
            boxElement.height = offsetY - boxElement.top 
        } else {
            boxElement.width = offsetX
            boxElement.height = offsetY
        }
    } else if (currentBall === '7') {
        if (contrast === 'draggable-container') {
            boxElement.height = offsetY - boxElement.top 
        } else {
            boxElement.height = offsetY
        }
    } else if (currentBall === '6') {
        if (contrast === 'draggable-container') {
            boxElement.height = offsetY - boxElement.top 
            const gapX = boxElement.left - offsetX
            boxElement.width = gapX + boxElement.width
            boxElement.left = boxElement.left - gapX
        } else {
            boxElement.width = boxElement.width - offsetX
            boxElement.left = boxElement.left + offsetX
        }
    } else if (currentBall === '5') {
        if (contrast === 'draggable-container') {
            boxElement.width = offsetX - boxElement.left 
        } else {
            boxElement.width = offsetX
        }
    } else if (currentBall === '4') {
        if (contrast === 'draggable-container') {
            const gapX = boxElement.left - offsetX
            boxElement.width = gapX + boxElement.width
            boxElement.left = boxElement.left - gapX
        } else {
            boxElement.width = boxElement.width - offsetX
            boxElement.left = boxElement.left + offsetX
        }
    } else if (currentBall === '3') {
        if (contrast === 'draggable-container') {
            boxElement.width = offsetX - boxElement.left
            const gapY = boxElement.top - offsetY
            boxElement.height = gapY + boxElement.height 
            boxElement.top = boxElement.top - gapY
        } else {
            boxElement.width = offsetX
            boxElement.height = boxElement.height - offsetY
            boxElement.top = boxElement.top + offsetY
        }
    } else if (currentBall === '2') {
        if (contrast === 'draggable-container') {
            boxElement.height = boxElement.top - offsetY + boxElement.height
            boxElement.top = offsetY
        } else {
            boxElement.height = boxElement.height - offsetY
            boxElement.top = boxElement.top + offsetY
        }
    } else if (currentBall === '1') {
        if (contrast === 'draggable-container') {
            const gapX = boxElement.left - offsetX
            boxElement.width = gapX + boxElement.width
            boxElement.left = boxElement.left - gapX
            boxElement.height = boxElement.height + boxElement.top - offsetY
            boxElement.top = offsetY
        } else {
            boxElement.width = boxElement.width - offsetX
            boxElement.left = boxElement.left + offsetX
            boxElement.height = boxElement.height - offsetY
            boxElement.top = boxElement.top + offsetY
        }
    }
    // 更新目标元素位置
    updateStyle()
}
复制代码

最终的效果为:

封装成组件

在上面的基础下,要实现

<div class="container" ref="parentRef">
  <resize-able :parentRef="parentRef">
      <button>button</button>
  </resize-able>
</div>
复制代码

思路:需要把事件自动綁定到外层的元素,要怎么做呢?核心是两点:

<div class="draggable-container" 
        @click="boxActive($event)"
        @mousedown="handleDown"
        @mousemove="handleMove"
        @mouseup="handleUp">
复制代码
  1. 我们可以利用vue的ref来获取dom节点,在获取到容器节点后把事件都绑定到外层container上;
  2. 可以通过window.getComputedStyle(dom, null)的方法来获取目标元素的高度宽度以及位置,从而存到初始化数据中。
<template>
  <slot></slot>
  // 利用定位的方式把蓝色的框包裹slot传进来的元素
  <div
    ref="targetBox"
    class="target-box"
    data-aim="canMove"
    :class="boxSelected ? `box-click target-can-move` : ``"
  >
    <div
      :class="boxSelected ? 'box-ball' : ''"
      v-for="item in 8"
      :data-i="item"
      :key="item"
    ></div>
    <div v-if="boxSelected" class="box-details">
      <div>x: {{ boxElement.left }}px</div>
      <div>y: {{ boxElement.top }}px</div>
      <div>width: {{ boxElement.width }}px</div>
      <div>height: {{ boxElement.height }}px</div>
    </div>
  </div>
</template>
<script>
import { ref, reactive, onMounted, getCurrentInstance, toRaw } from "vue";
export default {
  const slot = ref(null)
  ...
  const slotStyle = window.getComputedStyle(slot.value, null)
  boxElement.width = parseInt(slotStyle.width)
  boxElement.height = parseInt(slotStyle.height)
  boxElement.left = parseInt(slotStyle.left)
  boxElement.top = parseInt(slotStyle.top)
  ...
  // 给外层容器添加鼠标事件
  function bindMouseEvent(container) {
    // 支持现代浏览器
    containerSize.containerWidth = parseInt(window.getComputedStyle(container, null).width)
    containerSize.containerHeigth = parseInt(window.getComputedStyle(container, null).height)
    container.addEventListener('click', boxActive)
    container.addEventListener('mousedown', handleDown)
    container.addEventListener('mousemove', handleMove)
    container.addEventListener('mouseup', handleUp)
  }
复制代码

这就大功告成啦!

文章分类
前端
文章标签