从零开始打造一个支持多个滑块的滑动组件

从零开始打造一个支持多个滑块的滑动组件

背景

<input>有type=range,许多组件库也有slider,并且可以支持范围选择。但是很少有支持多个滑块的滑动条。所以我决定从零开始新建一个(Vue)。

滑动组件核心

当鼠标按下的时候,开始监听鼠标move,同时同步更改滑块位置

vue 复制代码
<script lang="ts" setup>
function onPointerMove(e: PointerEvent) {
  // change position
}

function onPointerUp(e: PointerEvent) {
  window.removeEventListener('pointermove', onPointerMove)
  window.removeEventListener('pointerup', onPointerUp)
  // move done
}

async function onPointerDown(e: PointerEvent) {
  e.preventDefault()
  e.stopPropagation()
  window.addEventListener('pointermove', onPointerMove, { passive: false })
  window.addEventListener('pointerup', onPointerUp)
}
</script>

<template>
  <div @pointerdown="onPointerDown" />
</template>

只要知道鼠标和滑动轨道的位置就能获得现在的所在的比例

ts 复制代码
const trackRect = trackRef.value.getBoundingClientRect()
const offset = props.vertical ? e.clientY - trackRect.top : e.clientX - trackRect.left
const percent = offset / (props.vertical ? trackRect.height : trackRect.width) * 100

我认为多个滑块比较困难的地方其实也是很多范围选择器比较关键的地方,就是你要保证modelValue中的数组的值是递增的,但是值对应的dom元素最好不是一直随着modelValue排序而变化的。理想的状态就是保证modelValue有序,但是dom不排序,不改变除了位置的样式之外的任何东西。

当然也有一些组件库的实现方式是。当你拖动左边的滑块到右边滑块的更右边的时候,你其实已经在拖动更右边的滑块了,左边的滑块到了原来右边滑块的位置。这种方法其实有点投机取巧,尤其是当需要给用户自定滑块渲染内容的时候,就会出不少问题。

所以我的一个想法就是建立一个map,对应modelValue中数值和dom。这样我们就知道一个modelValue的值是现在哪个dom。

有了indexMap之后,增删改就不会出现某个数据对应的滑块组件重新渲染了

然后我们在组件滑动之后,如果发生了滑块越过另一个滑块,除了要替换modelValue的值,也要一并替换modelValue的值,那你拖动的永远都是这项数据对应的dom,并且modelValue的顺序永远保持递增

核心update实现:

ts 复制代码
const indexMap = ref<Record<number, number>>({})
const indexMapReversed = computed(() => Object.fromEntries(Object.entries(indexMap.value).map(([k, v]) => [v, Number.parseInt(k)])))

function onUpdate(percentage: number) {
  setCurrentPercentage(percentage)
  const value = getValue(percentage)
  const modelValue = model.value
  const values = modelValue.map(i => i.value)
  if (props.deduplicate && values.includes(value))
    return
  let index = indexMap.value[current.value]
  const oldValue = values[index]
  if (oldValue - value > 0 && index > 0) {
    for (let i = index; i > 0; i--) {
      const prev = values[i - 1]
      if (value < prev) {
        swap(modelValue, i, i - 1)
        swap(indexMap.value, indexMapReversed.value[i], indexMapReversed.value[i - 1])
        index -= 1
      }
    }
  }
  if (oldValue - value < 0 && index < modelValue.length - 1) {
    for (let i = index; i < modelValue.length - 1; i++) {
      const next = values[i + 1]
      if (value > next) {
        swap(modelValue, i, i + 1)
        swap(indexMap.value, indexMapReversed.value[i], indexMapReversed.value[i + 1])
        index += 1
      }
    }
  }
  modelValue[index].value = value
  model.value = modelValue
}

核心功能完成后,我们只需要实现其他特性即可,目前组件有的特性:

  • ✨ 支持一个或多个滑块。
  • 🔄 自动检测模型类型并显示相应的滑块。
  • 🔀 自动对模型值进行排序,而不是对DOM进行排序。
  • ➕ 能够动态地添加或删除滑块。
  • 🚫 避免重复的滑块。
  • 🍡 平滑移动滑块,或者严格限制在每一个stop。
  • 🎨 可定制的样式和主题。
  • 🌓 支持黑暗模式。
  • 📍 在滑块上方或下方渲染内容(render函数 / 插槽)。
  • 🏷 支持在滑轨下方显示标记

项目

Demo体验

Github repo

npm
文章博客地址:wiidede.space/create-rang...

转载请标明出处

相关推荐
清秋20 分钟前
全网最全 ECMAScript 攻略( 更新至 ES2026)
前端·javascript·编程语言
Cao_Ron37 分钟前
用 SVG image symbol 画 ECharts graph 圆形节点
前端
hxy060142 分钟前
React-Call让你的React组件像异步函数一样调用
前端·react.js
Hilaku43 分钟前
为什么 AI 写前端时,总是优先选择 React,而不是 Vue?
前端·javascript·程序员
京东云开发者1 小时前
GPT-5.6、ChatGPT Work 与 Codex 更新食用指南
前端
Cao_Ron1 小时前
用 ECharts graph 画出卡片式矩形节点
前端
skiyee1 小时前
换个底座的 Wot Starter 居然让 AI 更懂项目!
前端·ai编程
anyup1 小时前
这一套绝了!AI 大模型写故事,星云 SDK 驱动 3D 数字人实时演绎
前端·人工智能·aigc
耀耀切克闹灬1 小时前
短链跳转
前端
孟陬2 小时前
高质量全方位的 AI 工具 sse-stuntman — Mock SSE 接口
前端·node.js·openai