龙空技术网

什么,你还不会 vue 拉伸指令?

爱摸鱼的程序员 1228

前言:

目前朋友们对“图片拉伸css”大致比较关注,兄弟们都需要了解一些“图片拉伸css”的相关资讯。那么小编同时在网摘上搜集了一些关于“图片拉伸css””的相关知识,希望咱们能喜欢,兄弟们快快来学习一下吧!

前言

在我们项目开发中,经常会有布局拉伸的需求,接下来 让我们一步步用 vue指令 实现这个需求

动手开发在线体验

codesandbox.io/s/dawn-cdn-…

常规使用解决拉伸触发时机

既然我们使用了指令的方式,也就是牺牲了组件方式的绑定事件的便捷性.

那么我们如何来解决这个触发时机的问题呢?

在参考了 Element UI 的表格拉伸功能后,笔者受到了启发

有点抽象,这个红色区域可不是真实节点,只是笔者模拟的一个 boder-right 多说无益 直接上代码吧

const pointermove = e => {    const { right } = el.getBoundingClientRect() // 获取到节点的右边界    const { clientX } = e // 此时鼠标位置    if (right - clientX < 8) // 表明在右边界的 8px 的过渡区 可以拉伸}

实现一个简单的右拉伸

下面让我们来实现一个简单的右拉伸功能, 既然用指令 肯定是越简单越好

笔者决定用 vue提供的 修饰符, v-resize.right

实现 v-resize.right1.首先我们创建两个相邻 div

<template>  <div class="container">    <div      v-resize.right      class="left"    />    <div class="right" />  </div></template><style lang="scss" scoped>.container {  width: 400px;  height: 100px;  > div {    float: left;    height: 100%;    width: 50%;  }  .left {    background-color: lightcoral;  }  .right {    background-color: lightblue;  }}</style>

2.实现 resize 触发时机

export const resize = {  inserted: function(el, binding) {    el.addEventListener('pointermove', (e) => {      const { right } = el.getBoundingClientRect()      if (right - e.clientX < 8) {        // 此时表明可以拉伸,时机成熟        el.style.cursor = 'col-resize'      } else {        el.style.cursor = ''      }    })  }}

3.实现右拉伸功能

el.addEventListener('pointerdown', (e) => {  const rightDom = el.nextElementSibling // 获取右节点  const startX = e.clientX // 获取当前点击坐标  const { width } = el.getBoundingClientRect() // 获取当前节点宽度  const { width: nextWidth } = rightDom.getBoundingClientRect() // 获取右节点宽度  el.setPointerCapture(e.pointerId) // HTML5 的 API 自行百度~  const onDocumentMouseMove = (e) => {    const offsetX = e.clientX - startX // 此时的 x 坐标偏差    // 更新左右节点宽度    el.style.width = width + offsetX + 'px'    rightDom.style.width = nextWidth - offsetX + 'px'  }  // 因为此时我们要在整个屏幕移动 所以我们要在 document 挂上 mousemove  document.addEventListener('mousemove',onDocumentMouseMove)

让我们看看此时的效果

会发现有两个问题

左边宽度会>左右之和,右边宽度会 < 0 当我们鼠标弹起的时候 还能继续拉伸

让我们首先解决第一个问题

方案一:限制最小宽度

const MIN_WIDTH = 10document.addEventListener('mousemove', (e) => {    const offsetX = e.clientX - startX // 此时的 x 坐标偏差    +if (width + offsetX < MIN_WIDTH || nextWidth - offsetX < MIN_WIDTH) return    // 更新左右节点宽度    el.style.width = width + offsetX + 'px'    rightDom.style.width = nextWidth - offsetX + 'px'})

第二个问题,其实非常简单

方案二:鼠标弹起后释放对应拉伸事件

el.addEventListener('pointerup', (e) => {    el.releasePointerCapture(e.pointerId)    document.removeEventListener('mousemove', onDocumentMouseMove)})

此时最最最基础的 v-resize 左右拉伸版本 我们已经实现了,来看下效果吧

看下此时的完整代码

export const resize = {  inserted: function (el, binding) {    el.addEventListener('pointermove', (e) => {      const { right } = el.getBoundingClientRect()      if (right - e.clientX < 8) {        // 此时表明可以拉伸        el.style.cursor = 'col-resize'      } else {        el.style.cursor = ''      }    })    const MIN_WIDTH = 10    el.addEventListener('pointerdown', (e) => {      const rightDom = el.nextElementSibling // 获取右节点      const startX = e.clientX // 获取当前点击坐标      const { width } = el.getBoundingClientRect() // 获取当前节点宽度      const { width: nextWidth } = rightDom.getBoundingClientRect() // 获取右节点宽度      el.setPointerCapture(e.pointerId) // HTML5 的 API 自行百度~      const onDocumentMouseMove = (e) => {        const offsetX = e.clientX - startX // 此时的 x 坐标偏差        if (width + offsetX < MIN_WIDTH || nextWidth - offsetX < MIN_WIDTH) {          return        }        // 更新左右节点宽度        el.style.width = width + offsetX + 'px'        rightDom.style.width = nextWidth - offsetX + 'px'      }      // 因为此时我们要在整个屏幕移动 所以我们要在 document 挂上 mousemove      document.addEventListener('mousemove', onDocumentMouseMove)      el.addEventListener('pointerup', (e) => {        el.releasePointerCapture(e.pointerId)        document.removeEventListener('mousemove', onDocumentMouseMove)      })    })  },}

作为 一名 优秀的前端性能优化专家,红盾大大已经开始吐槽,这么多 EventListener 都不移除吗? 还有'top' 'left'这些硬编码,能维护吗?

是的,后续代码我们将会移除这些被绑定的事件,以及处理硬编码(此处非重点 不做赘叙)

实现完 右拉伸 想必你对 左,上,下拉伸 也已经有了自己的思路

开始进阶

接下来让我们看下这种场景

我们 想实现一个 两边能同时拉伸的功能, 也就是 v-resize.left.right

实现左右拉伸功能

这种场景比较复杂,就需要我们维护一个拉伸方向上的变量 position

实现 v-resize.left.right

export const resize = {  inserted: function (el, binding) {    let position = '',      resizing = false    el.addEventListener('pointermove', (e) => {      if (resizing) return      const { left, right } = el.getBoundingClientRect()      const { clientX } = e      if (right - clientX < 8) {        position = 'right' // 此时表明右拉伸        el.style.cursor = 'col-resize'      } else if (clientX - left < 8) {        position = 'left' // 此时表明左拉伸        el.style.cursor = 'col-resize'      } else {        position = ''        el.style.cursor = ''      }    })    const MIN_WIDTH = 10    el.addEventListener('pointerdown', (e) => {      if (position === '') return      const sibling = position === 'right' ? el.nextElementSibling : el.previousElementSibling // 获取相邻节点      const startX = e.clientX // 获取当前点击坐标      const { width } = el.getBoundingClientRect() // 获取当前节点宽度      const { width: siblingWidth } = sibling.getBoundingClientRect() // 获取右节点宽度      el.setPointerCapture(e.pointerId) // HTML5 的 API 自行百度~      const onDocumentMouseMove = (e) => {        resizing = true        if (position === '') return        const offsetX = e.clientX - startX        const _elWidth = position === 'right' ? width + offsetX : width - offsetX //判断左右拉伸 所影响的当前节点宽度        const _siblingWidth = position === 'right' ? siblingWidth - offsetX : siblingWidth + offsetX //判断左右拉伸 所影响的相邻节点宽度        if (_elWidth <= MIN_WIDTH || _siblingWidth <= MIN_WIDTH) return        // 更新左右节点宽度        el.style.width = _elWidth + 'px'        sibling.style.width = _siblingWidth + 'px'      }      document.addEventListener('mousemove', onDocumentMouseMove)      el.addEventListener('pointerup', (e) => {        position = ''        resizing = false        el.releasePointerCapture(e.pointerId)        document.removeEventListener('mousemove', onDocumentMouseMove)      })    })  },}

看下此时的效果

非常丝滑, 当然 我们还需要考虑 传递 最小宽度 最大宽度 过渡区 等多种业务属性,但是这对于各位彦祖来说都是鸡毛蒜皮的小事. 自行改造就行了

完整代码Js版本

const elEventsWeakMap = new WeakMap()const MIN_WIDTH = 50const MIN_HEIGHT = 50const TRIGGER_SIZE = 8const TOP = 'top'const BOTTOM = 'bottom'const LEFT = 'left'const RIGHT = 'right'const COL_RESIZE = 'col-resize'const ROW_RESIZE = 'row-resize'function getElStyleAttr(element, attr) {  const styles = window.getComputedStyle(element)  return styles[attr]}function getSiblingByPosition(el, position) {  const siblingMap = {    left: el.previousElementSibling,    right: el.nextElementSibling,    bottom: el.nextElementSibling,    top: el.previousElementSibling  }  return siblingMap[position]}function getSiblingsSize(el, attr) {  const siblings = el.parentNode.childNodes  return [...siblings].reduce((prev, next) => (next.getBoundingClientRect()[attr] + prev), 0)}function updateSize({  el,  sibling,  formatter = 'px',  elSize,  siblingSize,  attr = 'width'}) {  let totalSize = elSize + siblingSize  if (formatter === 'px') {    el.style[attr] = elSize + formatter    sibling.style[attr] = siblingSize + formatter  } else if (formatter === 'flex') {    totalSize = getSiblingsSize(el, attr)    el.style.flex = elSize / totalSize * 10 // 修复 flex-grow <1    sibling.style.flex = siblingSize / totalSize * 10  }}const initResize = ({  el,  positions,  minWidth = MIN_WIDTH,  minHeight = MIN_HEIGHT,  triggerSize = TRIGGER_SIZE,  formatter = 'px'}) => {  if (!el) return  const resizeState = {}  const defaultCursor = getElStyleAttr(el, 'cursor')  const elStyle = el.style  const canLeftResize = positions.includes(LEFT)  const canRightResize = positions.includes(RIGHT)  const canTopResize = positions.includes(TOP)  const canBottomResize = positions.includes(BOTTOM)  if (!canLeftResize && !canRightResize && !canTopResize && !canBottomResize) { return } // 未指定方向  const pointermove = (e) => {    if (resizeState.resizing) return    e.preventDefault()    const { left, right, top, bottom } = el.getBoundingClientRect()    const { clientX, clientY } = e    // 左右拉伸    if (canLeftResize || canRightResize) {      if (clientX - left < triggerSize) resizeState.position = LEFT      else if (right - clientX < triggerSize) resizeState.position = RIGHT      else resizeState.position = ''      if (resizeState.position === '') {        elStyle.cursor = defaultCursor      } else {        if (getSiblingByPosition(el, resizeState.position)) { elStyle.cursor = COL_RESIZE }        e.stopPropagation()      }    } else if (canTopResize || canBottomResize) {      // 上下拉伸      if (clientY - top < triggerSize) resizeState.position = TOP      else if (bottom - clientY < triggerSize) resizeState.position = BOTTOM      else resizeState.position = ''      if (resizeState.position === '') {        elStyle.cursor = defaultCursor      } else {        if (getSiblingByPosition(el, resizeState.position)) { elStyle.cursor = ROW_RESIZE }        e.stopPropagation()      }    }  }  const pointerleave = (e) => {    e.stopPropagation()    resizeState.position = ''    elStyle.cursor = defaultCursor    el.releasePointerCapture(e.pointerId)  }  const pointerdown = (e) => {    const { resizing, position } = resizeState    if (resizing || !position) return    if (position) e.stopPropagation() // 如果当前节点存在拉伸方向 需要阻止冒泡(用于嵌套拉伸)    el.setPointerCapture(e.pointerId)    const isFlex = getElStyleAttr(el.parentNode, 'display') === 'flex'    if (isFlex) formatter = 'flex'    resizeState.resizing = true    resizeState.startPointerX = e.clientX    resizeState.startPointerY = e.clientY    const { width, height } = el.getBoundingClientRect()    const sibling = getSiblingByPosition(el, position)    if (!sibling) {      console.error('未找到兄弟节点', position)      return    }    const rectSibling = sibling.getBoundingClientRect()    const { startPointerX, startPointerY } = resizeState    const onDocumentMouseMove = (e) => {      if (!resizeState.resizing) return      elStyle.cursor =        canLeftResize || canRightResize ? COL_RESIZE : ROW_RESIZE      const { clientX, clientY } = e      if (position === LEFT || position === RIGHT) {        const offsetX = clientX - startPointerX        const elSize = position === RIGHT ? width + offsetX : width - offsetX        const siblingSize =          position === RIGHT            ? rectSibling.width - offsetX            : rectSibling.width + offsetX        if (elSize <= minWidth || siblingSize <= minWidth) return        updateSize({ el, sibling, elSize, siblingSize, formatter })      } else if (position === TOP || position === BOTTOM) {        const offsetY = clientY - startPointerY        const elSize =          position === BOTTOM ? height + offsetY : height - offsetY        const siblingSize =          position === BOTTOM            ? rectSibling.height - offsetY            : rectSibling.height + offsetY        if (elSize <= minHeight || siblingSize <= minHeight) return        updateSize({ el, sibling, elSize, siblingSize, formatter })      }    }    const onDocumentMouseUp = (e) => {      document.removeEventListener('mousemove', onDocumentMouseMove)      document.removeEventListener('mouseup', onDocumentMouseUp)      resizeState.resizing = false      elStyle.cursor = defaultCursor    }    document.addEventListener('mousemove', onDocumentMouseMove)    document.addEventListener('mouseup', onDocumentMouseUp)  }  const bindElEvents = () => {    el.addEventListener('pointermove', pointermove)    el.addEventListener('pointerleave', pointerleave)    el.addEventListener('pointerup', pointerleave)    el.addEventListener('pointerdown', pointerdown)  }  const unBindElEvents = () => {    el.removeEventListener('pointermove', pointermove)    el.removeEventListener('pointerleave', pointerleave)    el.removeEventListener('pointerup', pointerleave)    el.removeEventListener('pointerdown', pointerdown)  }  bindElEvents()  // 设置解绑事件  elEventsWeakMap.set(el, unBindElEvents)}export const resize = {  inserted: function(el, binding) {    const { modifiers, value } = binding    const positions = Object.keys(modifiers)    initResize({ el, positions, ...value })  },  unbind: function(el) {    const unBindElEvents = elEventsWeakMap.get(el)    unBindElEvents()  }}

Ts版本

import type { DirectiveBinding } from 'vue'const elEventsWeakMap = new WeakMap()const MIN_WIDTH = 50const MIN_HEIGHT = 50const TRIGGER_SIZE = 8enum RESIZE_CURSOR {  COL_RESIZE = 'col-resize',  ROW_RESIZE = 'row-resize',}enum POSITION {  TOP = 'top',  BOTTOM = 'bottom',  LEFT = 'left',  RIGHT = 'right',}type Positions = [POSITION.TOP, POSITION.BOTTOM, POSITION.LEFT, POSITION.RIGHT]interface ResizeState {  resizing: boolean  position?: POSITION  startPointerX?: number  startPointerY?: number}type WidthHeight = 'width' | 'height'type ElAttr = WidthHeight | 'cursor' | 'display' // 后面补充type ResizeFormatter = 'px' | 'flex'interface ResizeInfo {  el: HTMLElement  positions: Positions  minWidth: number  minHeight: number  triggerSize: number  formatter: ResizeFormatter}function getElStyleAttr(element: HTMLElement, attr: ElAttr) {  const styles = window.getComputedStyle(element)  return styles[attr]}function getSiblingByPosition(el: HTMLElement, position: POSITION) {  const siblingMap = {    left: el.previousElementSibling,    right: el.nextElementSibling,    bottom: el.nextElementSibling,    top: el.previousElementSibling,  }  return siblingMap[position]}function getSiblingsSize(el: HTMLElement, attr: WidthHeight) {  const siblings = (el.parentNode && el.parentNode.children) || []  return [...siblings].reduce(    (prev, next) => next.getBoundingClientRect()[attr] + prev,    0,  )}function updateSize({  el,  sibling,  formatter = 'px',  elSize,  siblingSize,  attr = 'width',}: {  el: HTMLElement  sibling: HTMLElement  formatter: ResizeFormatter  elSize: number  siblingSize: number  attr?: WidthHeight}) {  let totalSize = elSize + siblingSize  if (formatter === 'px') {    el.style[attr] = elSize + formatter    sibling.style[attr] = siblingSize + formatter  } else if (formatter === 'flex') {    totalSize = getSiblingsSize(el as HTMLElement, attr)    el.style.flex = `${(elSize / totalSize) * 10}` // 修复 flex-grow <1    sibling.style.flex = `${(siblingSize / totalSize) * 10}`  }}const initResize = ({  el,  positions,  minWidth = MIN_WIDTH,  minHeight = MIN_HEIGHT,  triggerSize = TRIGGER_SIZE,  formatter = 'px',}: ResizeInfo) => {  if (!el || !(el instanceof HTMLElement)) return  const resizeState: ResizeState = {    resizing: false,  }  const defaultCursor = getElStyleAttr(el, 'cursor')  const elStyle = el.style  const canLeftResize = positions.includes(POSITION.LEFT)  const canRightResize = positions.includes(POSITION.RIGHT)  const canTopResize = positions.includes(POSITION.TOP)  const canBottomResize = positions.includes(POSITION.BOTTOM)  if (!canLeftResize && !canRightResize && !canTopResize && !canBottomResize) {    return  } // 未指定方向  const pointermove = (e: PointerEvent) => {    if (resizeState.resizing) return    e.preventDefault()    const { left, right, top, bottom } = el.getBoundingClientRect()    const { clientX, clientY } = e    // 左右拉伸    if (canLeftResize || canRightResize) {      if (clientX - left < triggerSize) resizeState.position = POSITION.LEFT      else if (right - clientX < triggerSize)        resizeState.position = POSITION.RIGHT      else resizeState.position = undefined      if (resizeState.position === undefined) {        elStyle.cursor = defaultCursor      } else {        if (getSiblingByPosition(el, resizeState.position)) {          elStyle.cursor = RESIZE_CURSOR.COL_RESIZE        }        e.stopPropagation()      }    } else if (canTopResize || canBottomResize) {      // 上下拉伸      if (clientY - top < triggerSize) resizeState.position = POSITION.TOP      else if (bottom - clientY < triggerSize)        resizeState.position = POSITION.BOTTOM      else resizeState.position = undefined      if (resizeState.position === undefined) {        elStyle.cursor = defaultCursor      } else {        if (getSiblingByPosition(el, resizeState.position)) {          elStyle.cursor = RESIZE_CURSOR.ROW_RESIZE        }        e.stopPropagation()      }    }  }  const pointerleave = (e: PointerEvent) => {    e.stopPropagation()    resizeState.position = undefined    elStyle.cursor = defaultCursor    el.releasePointerCapture(e.pointerId)  }  const pointerdown = (e: PointerEvent) => {    const { resizing, position } = resizeState    if (resizing || !position) return    if (position) e.stopPropagation() // 如果当前节点存在拉伸方向 需要阻止冒泡(用于嵌套拉伸)    el.setPointerCapture(e.pointerId)    if (el.parentElement) {      const isFlex = getElStyleAttr(el.parentElement, 'display') === 'flex'      if (isFlex) formatter = 'flex'    }    resizeState.resizing = true    resizeState.startPointerX = e.clientX    resizeState.startPointerY = e.clientY    const { width, height } = el.getBoundingClientRect()    const sibling: HTMLElement = getSiblingByPosition(      el,      position,    ) as HTMLElement    if (!sibling || !(sibling instanceof HTMLElement)) {      console.error('未找到兄弟节点', position)      return    }    const rectSibling = sibling.getBoundingClientRect()    const { startPointerX, startPointerY } = resizeState    const onDocumentMouseMove = (e: MouseEvent) => {      if (!resizeState.resizing) return      elStyle.cursor =        canLeftResize || canRightResize          ? RESIZE_CURSOR.COL_RESIZE          : RESIZE_CURSOR.ROW_RESIZE      const { clientX, clientY } = e      if (position === POSITION.LEFT || position === POSITION.RIGHT) {        const offsetX = clientX - startPointerX        const elSize =          position === POSITION.RIGHT ? width + offsetX : width - offsetX        const siblingSize =          position === POSITION.RIGHT            ? rectSibling.width - offsetX            : rectSibling.width + offsetX        if (elSize <= minWidth || siblingSize <= minWidth) return        updateSize({ el, sibling, elSize, siblingSize, formatter })      } else if (position === POSITION.TOP || position === POSITION.BOTTOM) {        const offsetY = clientY - startPointerY        const elSize =          position === POSITION.BOTTOM ? height + offsetY : height - offsetY        const siblingSize =          position === POSITION.BOTTOM            ? rectSibling.height - offsetY            : rectSibling.height + offsetY        if (elSize <= minHeight || siblingSize <= minHeight) return        updateSize({          el,          sibling,          elSize,          siblingSize,          formatter,          attr: 'height',        })      }    }    const onDocumentMouseUp = () => {      document.removeEventListener('mousemove', onDocumentMouseMove)      document.removeEventListener('mouseup', onDocumentMouseUp)      resizeState.resizing = false      elStyle.cursor = defaultCursor    }    document.addEventListener('mousemove', onDocumentMouseMove)    document.addEventListener('mouseup', onDocumentMouseUp)  }  const bindElEvents = () => {    el.addEventListener('pointermove', pointermove)    el.addEventListener('pointerleave', pointerleave)    el.addEventListener('pointerup', pointerleave)    el.addEventListener('pointerdown', pointerdown)  }  const unBindElEvents = () => {    el.removeEventListener('pointermove', pointermove)    el.removeEventListener('pointerleave', pointerleave)    el.removeEventListener('pointerup', pointerleave)    el.removeEventListener('pointerdown', pointerdown)  }  bindElEvents()  // 设置解绑事件  elEventsWeakMap.set(el, unBindElEvents)}export const resize = {  mounted: function (el: HTMLElement, binding: DirectiveBinding) {    const { modifiers, value } = binding    const positions = Object.keys(modifiers)    initResize({ el, positions, ...value })  },  beforeUnmount: function (el: HTMLElement) {    const unBindElEvents = elEventsWeakMap.get(el)    unBindElEvents()  },}

作者:前端手术刀

链接:

标签: #图片拉伸css