数值输入框实现鼠标左右移动修改数值

我不知道具体的专业术语叫什么,所以标题只能这样写。其实从标题也可以看出来到底是什么效果。现在很多设计软件、设计类网站都具备这样的功能,就是鼠标按下,指针变成左右拖动的模样,按住鼠标不松开然后左右拖动就可以修改数值,如果是按下直接松开,就是平常的准备输入。

通过以上的描述可以大概知道如何实现,无非就是监听 input 的 mouedown/pointerdown 事件,然后监听 document 的 mousemove/pointermove 和 mouseup/pointerup 事件,move 事件里根据移动距离修改数值,up 事件里取消绑定。

const isDrag = ref(false)
const events = computed(() => {
  if (typeIsNumber(props.type)) {
    return {
      touchmove: (e: TouchEvent) => {
        if (isDrag.value) {
          e.preventDefault()
        }
      },
      pointerdown: (e: PointerEvent) => {
        e.stopPropagation()
        isDrag.value = true
        document.addEventListener('pointermove', dragChange)
        const oldCursor = document.body.style.cursor
        document.body.style.cursor = 'e-resize'
        const remove = () => {
          document.removeEventListener('pointermove', dragChange)
          document.removeEventListener('pointerup', remove)
          isDrag.value = false
          document.body.style.cursor = oldCursor
        }
        document.addEventListener('pointerup', remove)
      },
    }
  }
  return {}
})
// 拖动改变数值
let moveDelta = 0
const dragChange = (e: PointerEvent) => {
  e.preventDefault()
  if (!typeIsNumber(props.type)) return
  if (!e.movementX) return
  window.getSelection
    ? window.getSelection().removeAllRanges()
    : document.selection.empty()
  moveDelta += e.movementX
  const delta = moveDelta / 10
  if (Math.abs(delta) >= 1) {
    value.value += delta
    moveDelta = 0
  }
}

这里是 vue3 版本的实现代码,我这里监听的是 pointer 事件,因为这个事件可以兼顾鼠标和触摸事件。在 pointerdown 事件里设置一下鼠标样式,绑定 move 和 up 事件。move 事件的回调 dragChange 里根据移动的距离修改数值。

但有一些细节需要注意。

input 框的指针样式需要修改

<q-input
  v-model="value"
  v-bind="config.field"
  :type="type"
  :min="min"
  :max="max"
  v-on="events"
  :disable="disable"
  :input-style="
    isDrag
      ? {
          'user-select': 'none',
          cursor: 'e-resize',
        }
      : {}
  "
/>

    input 框的文本需要禁止选中。如上代码使用了 user-select,但这对 input 是无效的。网上有一种解决方案是调用 input 的 blur 方法,我试了,反正我这里无效。

    之后我又尝试了 readonly。但 readonly 不阻止文本选择。

    然后我又试了 disable,这个有效,但是指针样式会变成 not-allowed。

    于是我又想到了另一种方案:实时取消文本选择。

      window.getSelection
        ? window.getSelection().removeAllRanges()
        : document.selection.empty()

    这就是这段代码的作用。这种方法有效,但体验不好,因为文本会一直闪烁。但在有更好的方法之前,我也只能如此。

    这段代码上面还有一句 if (!e.movementX) return。这里主要是为了可以双击选择文本。因为点击/双击也会触发 pointermove 事件。

    再解释一下为什么我一定要取消文本选择,从功能上来说,选择文本不影响数值的改变,但是选择文本之后,再次按下鼠标准备拖动改变数值的时候,edge 浏览器会认为你是想搜索文本,从而直接打开新窗口。并且 e.preventDefault 无法阻止这种行为。

    说到 e.preventDefault,代码里还有一个需要解释的地方:touchmove 事件。为什么我还要额外监听 touchmove 呢?因为在移动端左右拖动的时候,浏览器也是有默认行为的,并且这个默认行为仅仅通过 pointermove 事件里的 preventDefault 阻止是不够的。

    细节上花的时间比主要功能多多了~~~

    最后,我这里的功能比较简单,很多细节和市面上的没法比,比如 figma 貌似可以一直拖,拖出窗口后自动从另一侧继续拖。不过无所谓了,我也不追求极致体验,能用就行。毕竟精力有限,还有很多其他重要功能需要实现。