よくあるUIですが、掴んで、動かすの基本が詰まっていて実用的なので作り方をお伝えできればと思います。 ドラッグする機能などを自力で実装する際にもしかしたら参考になるかもしれません。
初の技術ブログなので色々分かりづらいところ多いかと思います。修正などは気づいたベースで行っていきたいと思っておりますので、よろしくおねがいします。
- 0.手っ取り早くjsfiddleのURL貼れって方はこちらからどうぞ
- 1.見た目をさっさと作る
- 2.ざっくりと仕様を決める
- 3. propsを決める
- 4.dataの解説
- 5.computedで初期値等の下ごしらえ
- 6.「掴む」を実装する
- 7「掴んだものを動かす」を実装する
- 8.「掴んで動かしたものを離す」を実装する
- 9.おわり
0.手っ取り早くjsfiddleのURL貼れって方はこちらからどうぞ
https://jsfiddle.net/takuto_ex/3rpmL5by/
1.見た目をさっさと作る
構成しているHTMLの要素は以下の4つです。
①コンポーネント自体のラッパー
②スライダー本体
③スライダーフィル(埋まっていく箇所
④掴む部分
クラスをつけたり、処理をつけたりする再に便利なので通称もつけておくと楽です。
仮に①slider、②bar、③fill、④catchとします。
マークアップ部分は以下のとおりです。
<template> <div class="slider"> <div class="bar"> <div class="fill"></div> <div class="catch"></div> </div> </div> </template>
.slider{ .bar{ width: 100%; height: 6px; background-color: #EEE; border-radius: 6px 6px; position: relative; .fill{ width: 0px; background-color: #47b1e1; height: 6px; border-radius: 6px 6px; } .catch{ width: 14px; height: 14px; border-radius: 14px 14px; background-color: #FFF; border: 1px solid #FAFAFA; position: absolute; top: 50%; margin-top: -7px; margin-left: -7px; box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.1); cursor: pointer; } } }
2.ざっくりと仕様を決める
・④catchを掴んで、②barの範囲内で左右に動かせる事
・動かした箇所を割合として、最小値(min)と最大値(max)の間で数値が返ってくる事
・最小値(min)と最大値(max)を設定出来る事
最低限の機能ですが、任意の単位をつけて返すかどうか・・なども選べるとコンポーネントとして親切かもしれません。
では実際にscriptの部分を書いていきます。
3. propsを決める
仕様を満たす最低限ですが、コンポーネントのwidth、最大値、最小値、現在の値をpropsとして設定できるようにします。
props: { width: { type: Number, default: 300, }, max: { type: Number, default: 2000, }, min: { type: Number, default: 100, }, value: { type: Number, default: 950, }, },
4.dataの解説
詳しい説明は処理の中でそれぞれ解説しているのでこのブロックでは割愛させていただきます。
data: { draggable: false, startDragPoint: 0, slideValue: 0, fixedSlideValue: 0, },
5.computedで初期値等の下ごしらえ
computed: { componentId(){ return 'slider-' + this._uid; }, distance(){ return this.max - this.min; }, ratio(){ return this.value / this.distance; }, initialSlideValue(){ return this.width * this.ratio; } },
componentIdはそのままの意味です。今回はthis._uid(vue側が自動的にに割り当てているコンポーネントのインデックス)を使っていますが、ユーザー側でユニークIDを生成する関数などを作っておいたほうが良いと思います。
initialSlideValueは初期の①catchの位置を決定する数値です。distanceで実際に操作できる量を算出し、ratioではpropsのvalueを比率に変換しています。
コンポーネントのwidthに対してratioをかける事で、コンポーネント上での数値に変換しています。
6.「掴む」を実装する
まずは④catchを掴んで動かせる状態を検知出来るようにします。
これにはmousedownとmousemove、mouseup、3つのイベントを利用します。
今回では複数のポイントでイベントリスナーを制御する必要はありませんが、使い回ししやすいようにまとめておきます。
addEventListenerSet(){ window.addEventListener('mousemove',this.handleMouseMove); window.addEventListener('mouseup',this.handleEndDrag); }, removeEventListenerSet(){ window.removeEventListener('mousemove',this.handleMouseMove); window.removeEventListener('mouseup',this.handleEndDrag); },
mousedownでやる事はシンプルです。
handleStartDrag(e){ this.draggable = true; this.startDragPoint = e.clientX; this.addEventListenerSet(); },
①dataのdraggableをtrueにする事。
②クリックしたポイントの座標をstartDragPointにセットする。
②必要なイベントを登録する(前述でまとめてあるイベントリスナーを呼び出し
7「掴んだものを動かす」を実装する
mousemoveでは実際に③fillと④catchを動かす処理を書きます。
なんだか複雑で大変そう・・・とイメージするかもしれませんが、やっていることは単純です。ざっくりと表現するなら「動いた量を初期値に足す」これだけです。
まずは動いた量を割り出しましょう。
移動量は移動中のx座標(e.clientX)から掴んだ地点のx座標(this.startDragPoint)を引いた数で求められます。
handleMouseMove(e){ const moveQuantity = e.clientX - this.startDragPoint; const slideValue = this.fixedSlideValue + moveQuantity; },
ここにdata更新を追加していきますが、mousemoveが閾値を超えた場合の挙動を追加します。
mouse位置がsliderの左端を超えた場合0を代入し、右端を超えた場合はsliderのwidthを代入します。最後にドラッグ中以外に値が更新されるのを防ぐため、真偽値が入っているthis.draggableのifで囲んでおきましょう。
handleMouseMove(e){ if(this.draggable){ const moveQuantity = e.clientX - this.startDragPoint; const slideValue = this.fixedSlideValue + moveQuantity; if(slideValue < 0){ this.slideValue = 0; }else if(slideValue > this.width){ this.slideValue = this.width; }else{ this.slideValue = slideValue; } } },
8.「掴んで動かしたものを離す」を実装する
複雑な処理などは特に無いのですが、やることが意外に多いです。
handleEndDrag(e){ this.fixedSlideValue = this.slideValue; this.draggable = false; // 8-1 this.removeEventListenerSet(); // 8-3 const ratio = this.fixedSlideValue / this.width; // 8-4 const answer = this.min + Math.ceil(this.distance * ratio); // 8-5 console.log(answer); },
draggableをfalseにしてmmouseup後はmousemoveで処理を走らせないようにします。 今回はeventをremoveするだけでも十分かもしれません。
catchで動かした値を比率に変換しさらにそれを使って元の最小値〜最大値の量に再変換しています。
また、小数点が大量発生する可能性があるので、Math.ceilで整数化してます。
9.おわり
できたものがこちら↓
https://jsfiddle.net/takuto_ex/3rpmL5by/
ドラッグするようなUIをライブラリ無しで作るとなると結構構えてしまうものですが、この手のUIは段階的に整理すれば驚くほど難しいようなものはあまりありません。(驚くほどめんどくさいものはあります)
今後も色々なUIの作り方や細々としたOSS等を紹介、制作していきたいと思いますのでよろしくおねがいします。