🐳

Vue.jsでお手製RangeSlider

2024/12/31に公開

完成形

それっぽいものを作ってみました。
range_slider.gif

なぜ実装しようと思ったか

私は普段はReactを使っています。ReactでもComponent外のマウス操作で画面を更新したいときがあります。そんなときはbodyなどに対してaddEventListenerによるイベントを設定し、refを用いてDOMからleftやwidthを取得してstateを更新することがあります。

Vue.jsではどんな感じかなーということでやってみました。

実装

<template>
  <div class="range_slider" :style="{width: width + 'px'}">
    <div class="range_wrapper" ref="range" @mousedown="mousedown">
      <div class="range" :style="{'background-color': color}"></div>
    </div>
    <div class="circle" :style="circleStyle" @mousedown="mousedown"></div>
  </div>
</template>

<script>
  export default {
    name: 'RangeSlider',
    props: {
      color: {type: String, default: '#eee'},
      width: {type: Number, default: 200},
      value: {type: Number, default: 0},
      max: {type: Number, default: 100},
      min: {type: Number, default: 0}
    },
    data () {
      return {
        isMoving: false
      }
    },
    methods: {
      moveCircle (e) {
        const width = this.$refs.range.getBoundingClientRect().width
        const rangeLeft = this.$refs.range.getBoundingClientRect().left
        const eventLeft = e.clientX
        const left = eventLeft - rangeLeft

        if (left <= 0) {
          this.$emit('update:value', this.min)
        }
        if (width <= left) {
          this.$emit('update:value', this.max)
        }
        if (left >= 0 && left <= width) {
          const ratio = left / width
          const value = (this.max - this.min) * ratio + this.min
          this.$emit('update:value', Math.round(value))
        }
      },
      mousedown (e) {
        this.isMoving = true
        this.moveCircle(e)
        // mousedownしたタイミングでマウス操作に対するイベントを設定
        document.body.addEventListener('mousemove', this.mousemove)
        document.body.addEventListener('mouseup', this.mouseup)
      },
      mousemove (e) {
        if (this.isMoving) {
          this.moveCircle(e)
        }
      },
      mouseup () {
        this.isMoving = false
        // ドラッグ操作が終わったタイミングでイベントを削除
        document.body.removeEventListener('mousemove', this.mousemove)
        document.body.removeEventListener('mouseup', this.mouseup)
      }
    },
    computed: {
      circleStyle () {
        const ratio = (this.value - this.min) / (this.max - this.min)
        const left = this.width * ratio
        return {
          left: `${left - 10}px`
        }
      }
    }
  }
</script>

<style scoped>
  .range_slider {
    height: 20px;
    margin: 0 auto;
    position: relative;
    display: inline-block;
  }

  .range_wrapper {
    position: relative;
    height: 100%;
    width: 100%;
  }

  .range {
    height: 4px;
    width: 100%;
    background-color: #eee;
    position: absolute;
    top: calc(50% - 2px);
    border-radius: 3px;
  }

  .circle {
    width: 0;
    height: 0;
    top: 0;
    border: 10px solid #DDD;
    border-radius: 50%;
    position: absolute;
  }
</style>

補足

Reactと違う点はthis.$refsを使用する点でしょうか。Reactでもthis.refsとして取得する方法があったと思いますが、Reactでは非推奨になっているはずですね。
ここまで実装して思ったのが、HTML5のinputのrangeを使用しても実装できるかもしれないという点です。inputのrangeの円よりも大きい円を上に被せるような実装をすればマウス操作を気にする必要はないかもしれません。

Discussion