HTML5の主要機能でもあるドラッグ&ドロップですが、
普段、UI周りの実装をすることが少ないエンジニアの方には馴染みが薄い処理の一つではないでしょうか。
私もAPIの存在は知っていたものの、最近まで実装したことがなかったのですが、ついこの間WEBサービスのリニューアル案件で使う機会があったのでその時にインプットしたSortableというライブラリを紹介したいと思います。
jQueryUIを使わずに実装できるライブラリ「Sortable」
自分は特に反jQuery側の人間ではないのですが、やっぱり依存するライブラリが少ないというのは組んでいて安心しますよね!そんな時に見つけたのがこのSortableです。
Sortable / デモページ
主要ブラウザ(IE9含む)はもちろん、タッチデバイスにも対応している優れものです。
そんな万能なライブラリですが、日本語のドキュメントが少ないというのが導入における唯一のリスクだったのですが、
幸いなことに案件のスパンも長かったため、検証も兼ねて初めて自分でドキュメントの日本語化をしてみました。
(ところどころ怪しい部分がありますが、優しくコメントをいただけると助かります)
基本的な使い方
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」を使用した実装例
基本的な使い方とオプションなどが理解できたところで、様々な実装例を見ていきたいと思います。
ドラッグできるポインタの範囲を指定する
let sortable = Sortable.create(hoge, {
group: "hoge",
handle: ".my-handle",
animation: 100
});
特に何も指定せずにそのままの状態だと、ドラッグに反応してリスト内の項目上でテキストを選択することができません。
そこで、handle
オプションを使ってドラッグできるポインタの範囲を指定してあげることで、項目の用途はそのままにドラッグ&ドロップさせることができます。
複数のリスト間でドラッグ&ドロップ
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
});
複数あるリスト間でお互いの項目をドラッグ&ドロップさせるには、二通り実装する方法があります。
一つは、group
のname
の値を同じ値に設定してあげます。
もう一つは、group
のput
で引き受けるリストのname
をそれぞれ指定します。
使い分けは時と場合によりますが、
この場合は前者の方が移動するリストが増えた際に楽なため、前者の方をおすすめします。
ドラッグさせない項目を指定する
Sortable.create(hoge, {
group: {
name: "shares",
},
filter: ".ignore-elements",
animation: 100
});
Sortable.create(fuga, {
group: {
name: "shares",
},
filter: ".ignore-elements",
animation: 100
});
項目の中には、ドラッグさせたくないものが発生する場合もあります。
その場合には、オプションのfilter
を用いドラッグさせたくない要素を指定します。
リストの削除と復活
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ボタンがクリックされたことを検知し、
ボタンの削除とその削除されたボタンを元のリストに復活させます。
リストの削除と復活(別パターン)
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,
});
先ほどの実装例とは多少異なりますが、
追加する元のリストの項目は削除せずに、移動先にドラッグ&ドロップした項目をクローンしてあげるだけで済む場合もあるかもしれません。
その場合には、group
のpull
で'clone'
を返してあげるようにします。
そうすることで、元のリスト内の項目は削除されずに自分のコピーを移動先に渡してあげることができます。
項目の順番を保存して取得
<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>
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
で設定している順番を、store
のset
でlocalStorageに保存します。
反対に、保存した順番を取得するには、
store
のget
を用い取得し、getしたものをreturnさせてあげることで、リロードする前の項目順で表示されます。
まとめ
少ない記述で、なおかつ、拡張性も高いため、
ある程度の規模と機能には対応できるライブラリかなという印象です。
また、肝心のパフォーマンスもスムーズに動いてくれているため(SP/TBも)、このライブラリを選んで正解でした。
今後クライアントのフィードバッグを受けて、より深いところまで動かしてみようと思います。
細々出てきた問題に関しては、随時更新していきたいと思います。
日本語化してみて
今回初めて日本語ドキュメントがほとんど無いライブラリを検証してみましたが、
改めてやってみると、検証しながら意味を調べたりすることで、
そのオプションの持っている意味などが、普段誰かがドキュメントに起こしてくれたものを見て理解するより、数倍理解を深めることができたなと思いました。
今回のように時間が取れるかどうかはわかりませんが、次回も新しいものにどんどん触れていきたいと思います!
では、また次回!