Edited at

【jQueryUIを使わずにドラッグ&ドロップを実装したい】Javascriptライブラリ「Sortable」を使ってみた

HTML5の主要機能でもあるドラッグ&ドロップですが、

普段、UI周りの実装をすることが少ないエンジニアの方には馴染みが薄い処理の一つではないでしょうか。

私もAPIの存在は知っていたものの、最近まで実装したことがなかったのですが、ついこの間WEBサービスのリニューアル案件で使う機会があったのでその時にインプットしたSortableというライブラリを紹介したいと思います。

banner.jpg


jQueryUIを使わずに実装できるライブラリ「Sortable」

自分は特に反jQuery側の人間ではないのですが、やっぱり依存するライブラリが少ないというのは組んでいて安心しますよね!そんな時に見つけたのがこのSortableです。

Sortable / デモページ

主要ブラウザ(IE9含む)はもちろん、タッチデバイスにも対応している優れものです。

そんな万能なライブラリですが、日本語のドキュメントが少ないというのが導入における唯一のリスクだったのですが、

幸いなことに案件のスパンも長かったため、検証も兼ねて初めて自分でドキュメントの日本語化をしてみました。

(ところどころ怪しい部分がありますが、優しくコメントをいただけると助かります)


基本的な使い方

capani.gif


script

let sortable = Sortable.create(hoge, {

group: "hoge",
animation: 100
});

基本的な使い方としては、

Sortable.create()の第一引数にドラッグ&ドロップを適用させるリストのIDを指定し、第二引数に適用させるオプションを指定します。


オプション

オプション名
デフォルト値
説明

group
"name"
ドラッグ&ドロップするグループ名(名前はなんでもいい)

sort
true
リスト内でのソート可否

delay
0
並び替えを開始するタイミング

disabled
false
sortオプションがtrueに設定されている際に、ソートを無効化するか

store
null
リストの順番の保存と取得

animation
150
ソート時のアニメーションスピード

handle
".my-handle"
ドラッグできる範囲の指定(そのままだとテキスト選択ができないため)

filter
".ignore-elements"
ドラッグさせたくないセレクタを指定

preventOnFilter
true
filterがトリガされたときに「event.preventDefault()」を呼び出すかどうか

draggable
".item"
リスト内のどの部分をドラッグ可能にするかを指定

ghostClass
"sortable-ghost"
ドロップ中のクラス名の指定

chosenClass
"sortable-chosen"
選択中のクラス名の指定

dragClass
"sortable-drag"
ドラッグ中のクラス名の指定

dataIdAttr
'data-id'
不明

forceFallback
false
trueに設定すると古いブラウザのフォールバックが適用される(開発用)

fallbackClass
"sortable-fallback"
forceFallbackを使用したときに複製されたDOM要素のクラス名

fallbackOnBody
false
forceFallbackを使用したときにcloneされたDOM要素をドキュメントの本文に追加するか

fallbackTolerance
0
forceFallbackを使用したときにドラッグすることができる範囲をピクセルで指定

scroll
true
ドラッグしながらスクロールできるようにするか

scrollFn
function(offsetX, offsetY, originalEvent) { ... }
専用のスクロール機能を持​​つカスタムスクロールバーがある場合に使用

scrollSensitivity
30
スクロールが開始する位置をピクセルで指定

scrollSpeed
10
スクロールするスピードをピクセルで指定


イベント

イベント名
説明

setData
データを格納・保存した時

onChoose
要素を選択している時

onStart
ドラッグが開始された時

onEnd
ドラッグが終了した時

onAdd
リスト内に新たに要素が追加された時

onUpdate
ソートされリスト内が新たに更新された時

onSort
リストの変更(追加/更新/削除)がされた時

onRemove
要素がリストから別のリストに移動し削除された時

onFilter
filterで指定された要素をドラッグしようとした時

onMove
リスト内またはリスト間で項目を移動した時

onClone
要素が複製された時


「Sortable」を使用した実装例

基本的な使い方とオプションなどが理解できたところで、様々な実装例を見ていきたいと思います。


ドラッグできるポインタの範囲を指定する

capani2.gif


script

let sortable = Sortable.create(hoge, {

group: "hoge",
handle: ".my-handle",
animation: 100
});

特に何も指定せずにそのままの状態だと、ドラッグに反応してリスト内の項目上でテキストを選択することができません。

そこで、handleオプションを使ってドラッグできるポインタの範囲を指定してあげることで、項目の用途はそのままにドラッグ&ドロップさせることができます。


複数のリスト間でドラッグ&ドロップ

capani3.gif


script

Sortable.create(hoge, {

group: {
name: "shares",
},
animation: 100
});

Sortable.create(fuga, {
group: {
name: "shares",
},
animation: 100
});

// または

Sortable.create(hoge, {
group: {
name: "hoge",
put: "fuga",
},
animation: 100
});

Sortable.create(fuga, {
group: {
name: "fuga",
put: "hoge",
},
animation: 100
});


複数あるリスト間でお互いの項目をドラッグ&ドロップさせるには、二通り実装する方法があります。

一つは、groupnameの値を同じ値に設定してあげます。

もう一つは、groupputで引き受けるリストのnameをそれぞれ指定します。

使い分けは時と場合によりますが、

この場合は前者の方が移動するリストが増えた際に楽なため、前者の方をおすすめします。


ドラッグさせない項目を指定する

capani4.gif


script

Sortable.create(hoge, {

group: {
name: "shares",
},
filter: ".ignore-elements",
animation: 100
});

Sortable.create(fuga, {
group: {
name: "shares",
},
filter: ".ignore-elements",
animation: 100
});


項目の中には、ドラッグさせたくないものが発生する場合もあります。

その場合には、オプションのfilterを用いドラッグさせたくない要素を指定します。


リストの削除と復活

capani5.gif


script

Sortable.create(hoge, {

group: {
name: "hoge",
},
animation: 100
});

var editableList = Sortable.create(fuga, {
group: {
name: "fuga",
put: ["hoge"],
},
filter: ".js-remove",
onAdd: function (evt) {
var el = editableList.closest(evt.item);
el.innerHTML += '<i class="js-remove">✖</i>';
},
onFilter: function (evt) {
var el = editableList.closest(evt.item);
var hoge = document.getElementById("hoge");
var removeBtn = document.querySelector("#fuga li .js-remove");
el.removeChild(removeBtn);
el && el.parentNode.removeChild(el);
hoge.appendChild(el);
},
animation: 100,
});


Wordpressのウィジェット管理画面のような、項目の追加と削除・復活を実装します。

onAdd: function (evt) {

var el = editableList.closest(evt.item);
el.innerHTML += '<i class="js-remove">✖</i>';
},

onAddイベントを使い、hogeリストからfugaリストに項目が追加されたことを検知し、

removeボタンを追加します。

onFilter: function (evt) {

var el = editableList.closest(evt.item);
var hoge = document.getElementById("hoge");
var removeBtn = document.querySelector("#fuga li .js-remove");
el.removeChild(removeBtn);
el && el.parentNode.removeChild(el);
hoge.appendChild(el);
},

onFilterイベントを使い、removeボタンがクリックされたことを検知し、

ボタンの削除とその削除されたボタンを元のリストに復活させます。


リストの削除と復活(別パターン)

capani6.gif


script

Sortable.create(hoge, {

group: {
name: "hoge",
pull: function () {
return "clone";
}
},
sort: false,
animation: 100
});

var editableList = Sortable.create(fuga, {
group: {
name: "fuga",
put: ["hoge"],
},
filter: ".js-remove",
onAdd: function (evt) {
var el = editableList.closest(evt.item);
el.innerHTML += '<i class="js-remove">✖</i>';
},
onFilter: function (evt) {
var el = editableList.closest(evt.item);
el && el.parentNode.removeChild(el);
},
animation: 100,
});


先ほどの実装例とは多少異なりますが、

追加する元のリストの項目は削除せずに、移動先にドラッグ&ドロップした項目をクローンしてあげるだけで済む場合もあるかもしれません。

その場合には、grouppull'clone'を返してあげるようにします。

そうすることで、元のリスト内の項目は削除されずに自分のコピーを移動先に渡してあげることができます。


項目の順番を保存して取得

capani7.gif


html

<ul id="hoge">

<li data-id="1">element 1</li>
<li data-id="2">element 2</li>
<li data-id="3">element 3</li>
</ul>


script

Sortable.create(hoge, {

group: "save",
store: {
get: function (sortable) {
var order = localStorage.getItem(sortable.options.group.name);
return order ? order.split('|') : [];
},
set: function (sortable) {
var order = sortable.toArray();
localStorage.setItem(sortable.options.group.name, order.join('|'));
}
}
});

順番を保存するには、

HTMLのdata-idで設定している順番を、storesetでlocalStorageに保存します。

反対に、保存した順番を取得するには、

storegetを用い取得し、getしたものをreturnさせてあげることで、リロードする前の項目順で表示されます。


まとめ

少ない記述で、なおかつ、拡張性も高いため、

ある程度の規模と機能には対応できるライブラリかなという印象です。

また、肝心のパフォーマンスもスムーズに動いてくれているため(SP/TBも)、このライブラリを選んで正解でした。

今後クライアントのフィードバッグを受けて、より深いところまで動かしてみようと思います。

細々出てきた問題に関しては、随時更新していきたいと思います。


日本語化してみて

今回初めて日本語ドキュメントがほとんど無いライブラリを検証してみましたが、

改めてやってみると、検証しながら意味を調べたりすることで、

そのオプションの持っている意味などが、普段誰かがドキュメントに起こしてくれたものを見て理解するより、数倍理解を深めることができたなと思いました。

今回のように時間が取れるかどうかはわかりませんが、次回も新しいものにどんどん触れていきたいと思います!

では、また次回〜

banner.jpg