今やパソコンを使う時に当たり前の操作となっている「ドラッグしてファイルをフォルダに入れる」のような、ドラッグ操作で並び替えが可能なUIをネイティブなJavaScriptで作ってみます。
Webブラウザでも使える・できるようにしてみます。
是非、最後までご覧いただけると嬉しいです。
dataTransfer.setData()
JavaScriptの dataTransfer.setData()
は、ドラッグ操作で文字列データを格納するメソッドです。
DataTransfer.setData( format , data );
第1引数には「ドラッグデータの型・データのタイプ名」を指定して、第2引数には「対象のデータ」を指定します。
特に、第1引数の指定は難しいので、いつもお世話になっている以下「mdn web docs」で深く学んでおきましょう。
外部リンク DataTransfer.setData()
自分も学習中なので「mdn web docs」で情報を補完ください。
要素をドラッグして並び替え可能なサンプル
早速サンプルです。
要素が12個並んでいますが、ドラッグで要素の並び替えが可能です。
マウスによるドラッグ&ドロップと、スマホでのタッチイベントにも反応します。
ECサイトで買い物カゴにドラックしてカートインとかに使われるような挙動です。
実装の手順と方法
それぞれのコードの解説の前に、実装の手順と方法について簡単にご説明します。
はじめに、設置したい場所へHTMLを記述します。
<div class="container" id="container">
<div class="dragList">
<div class="item" data-id="1" draggable="true"></div>
<div class="item" data-id="2" draggable="true"></div>
<div class="item" data-id="3" draggable="true"></div>
<div class="item" data-id="4" draggable="true"></div>
<div class="item" data-id="5" draggable="true"></div>
<div class="item" data-id="6" draggable="true"></div>
<div class="item" data-id="7" draggable="true"></div>
<div class="item" data-id="8" draggable="true"></div>
<div class="item" data-id="9" draggable="true"></div>
<div class="item" data-id="10" draggable="true"></div>
<div class="item" data-id="11" draggable="true"></div>
<div class="item" data-id="12" draggable="true"></div>
</div>
</div>
次に、JavaScriptのコードをページに記述します。これは、Grid.jsの機能やテーブルセルを指定するオプションです。
コードは <body>〜</body>
で、</body>
の閉じタグ(クロージングタグ)の前に記述しましょう。
// itemの要素を全て取得
const items = document.querySelectorAll('.item');
// 更新速度を制限してチラつき防止
const framerate = 100;
const state = {};
document.querySelectorAll('.item').forEach(item => {
// ドラッグ開始したかを監視
item.ondragstart = handleDragStart;
item.ondragover = handleDragOver;
item.ondragend = handleDragEnd;
});
// ドラッグを開始
function handleDragStart(e) {
const item = e.currentTarget;
e.dataTransfer.setData('text', item.dataset.id);
state.dragging = item;
state.anchor = item.nextElementSibling;
state.lastFrame = Date.now();
setTimeout(() => {
state.dragging.classList.add('dragging');
});
}
// ドラッグしている途中
function handleDragOver(e) {
// limit by framerate
if (state.lastFrame + framerate > Date.now()) return;
const target = e.target;
if (state.dragging !== target) {
const displace = getIndex(state.dragging) > getIndex(target) ? target : target.nextElementSibling;
// move nodes
state.dragging.parentNode.insertBefore(state.dragging, displace);
}
}
// ドラッグが終わったら
function handleDragEnd(e) {
state.dragging.classList.remove('dragging');
}
function getIndex(item) {
return [...item.parentNode.children].indexOf(item);
}
最後にCSSを記述します。
/* 全体 */
.dragList {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 25px;
}
/* 各item */
.item {
position: relative;
z-index: 0;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
cursor: grab;
width: 30%;
background: #fff;
box-shadow: 0 0 3px 0 rgb(0 0 0 / 12%), 0 2px 3px 0 rgb(0 0 0 / 22%);
}
/* itemの番号 */
.item:after {
content: attr(data-id);
font-size: 1.8rem;
font-weight: bold;
color: #313131;
position: relative;
text-align: center;
z-index: 1;
width: 50%;
pointer-events: none;
}
.item.dragging {
background-color: #aaa;
box-shadow: none;
opacity: 0.7;
}
.item.dragging:after {
display: none;
}
/*ここからitemの背景色 */
.item[data-id="1"], .item[data-id="7"] {
background: #e1f3ff;
background-image:
linear-gradient(45deg, #c8e4ff 25%, transparent 0),
linear-gradient(45deg, transparent 75%, #c8e4ff 0),
linear-gradient(45deg, #c8e4ff 25%, transparent 0),
linear-gradient(45deg, transparent 75%, #c8e4ff 0);
background-position: 0 0, 15px 15px, 15px 15px, 30px 30px;
background-size: 30px 30px;
}
.item[data-id="2"], .item[data-id="8"] {
background: repeating-linear-gradient(-45deg, #ddd 0, #ddd 2px, #FFF 4px, #FFF 6px);
}
.item[data-id="3"], .item[data-id="9"] {
background: #e1f3ff;
background-image: linear-gradient(#c8e4ff 1px, transparent 0), linear-gradient(90deg, #c8e4ff 1px, transparent 0);
background-size: 30px 30px;
}
.item[data-id="4"], .item[data-id="10"] {
background: #e1f3ff;
background-image: radial-gradient(#c8e4ff 30%, transparent 0), radial-gradient(#c8e4ff 30%, transparent 0);
background-size: 30px 30px;
background-position: 0 0, 15px 15px;
}
.item[data-id="5"], .item[data-id="11"] {
background-image: radial-gradient(currentColor 1px, transparent 1px);
background-size: calc(10 * 1px) calc(10 * 1px);
color: #c8e4ff !important;
}
.item[data-id="6"], .item[data-id="12"] {
background-color: #FFF;
background-image: linear-gradient( transparent 95%, rgba(0, 0, 0, .06) 50%, rgba(0, 0, 0, .06)), linear-gradient( 90deg, transparent 95%, rgba(0, 0, 0, .06) 50%, rgba(0, 0, 0, .06) );
background-size: 16px 16px;
background-repeat: repeat;
}
/* スマホのブレイクポイント */
@media screen and (max-width: 767px) {
.item {
width: 45%;
}
}
これで完成です。
ざっくりとしたコードの解説
コードはHTML・JavaScript・CSSの3種類です。ざっくりですが、順に解説していきます。
HTML
HTMLは、「container」のid名が付く div
タグを親要素にして、「dragList」のclass名の付く要素が子要素。そして、「item」のclass名が付く12個の孫要素です。
<div class="container" id="container">
<div class="dragList">
<div class="item" data-id="1" draggable="true"></div>
<div class="item" data-id="2" draggable="true"></div>
<div class="item" data-id="3" draggable="true"></div>
<div class="item" data-id="4" draggable="true"></div>
<div class="item" data-id="5" draggable="true"></div>
<div class="item" data-id="6" draggable="true"></div>
<div class="item" data-id="7" draggable="true"></div>
<div class="item" data-id="8" draggable="true"></div>
<div class="item" data-id="9" draggable="true"></div>
<div class="item" data-id="10" draggable="true"></div>
<div class="item" data-id="11" draggable="true"></div>
<div class="item" data-id="12" draggable="true"></div>
</div>
</div>
孫要素の「item」のclass名が付く要素には、data-id=
の各属性はJavaScriptとCSSにも連動しています。
「item」の要素は、li
のリストタグで置いてもいいかもしれません。
JavaScript
JavaScriptは、ざっくり言うと「itemの要素を全て取得」をして、ドラッグ開始・ドラッグ中・ドラッグ終了後の3つで処理を記述しています。
// itemの要素を全て取得
const items = document.querySelectorAll('.item');
// 更新速度を制限してチラつき防止
const framerate = 100;
const state = {};
document.querySelectorAll('.item').forEach(item => {
// ドラッグ開始したかを監視
item.ondragstart = handleDragStart;
item.ondragover = handleDragOver;
item.ondragend = handleDragEnd;
});
// ドラッグを開始
function handleDragStart(e) {
const item = e.currentTarget;
e.dataTransfer.setData('text', item.dataset.id);
state.dragging = item;
state.anchor = item.nextElementSibling;
state.lastFrame = Date.now();
setTimeout(() => {
state.dragging.classList.add('dragging');
});
}
// ドラッグしている途中
function handleDragOver(e) {
// limit by framerate
if (state.lastFrame + framerate > Date.now()) return;
const target = e.target;
if (state.dragging !== target) {
const displace = getIndex(state.dragging) > getIndex(target) ? target : target.nextElementSibling;
// move nodes
state.dragging.parentNode.insertBefore(state.dragging, displace);
}
}
// ドラッグが終わったら
function handleDragEnd(e) {
state.dragging.classList.remove('dragging');
}
function getIndex(item) {
return [...item.parentNode.children].indexOf(item);
}
いろんな処理内容が記載されていますが、各ドラッグのアクション毎に処理内容を記述していけばいいので、比較的みやすい内容です。
CSS
CSSは、flexboxで並べたレイアウトで、「item」のclassが付いた要素にそれぞれのスタイルをあてています。
/* 全体 */
.dragList {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 25px;
}
/* 各item */
.item {
position: relative;
z-index: 0;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
cursor: grab;
width: 30%;
background: #fff;
box-shadow: 0 0 3px 0 rgb(0 0 0 / 12%), 0 2px 3px 0 rgb(0 0 0 / 22%);
}
/* itemの番号 */
.item:after {
content: attr(data-id);
font-size: 1.8rem;
font-weight: bold;
color: #313131;
position: relative;
text-align: center;
z-index: 1;
width: 50%;
pointer-events: none;
}
.item.dragging {
background-color: #aaa;
box-shadow: none;
opacity: 0.7;
}
.item.dragging:after {
display: none;
}
/*ここからitemの背景色 */
.item[data-id="1"], .item[data-id="7"] {
background: #e1f3ff;
background-image:
linear-gradient(45deg, #c8e4ff 25%, transparent 0),
linear-gradient(45deg, transparent 75%, #c8e4ff 0),
linear-gradient(45deg, #c8e4ff 25%, transparent 0),
linear-gradient(45deg, transparent 75%, #c8e4ff 0);
background-position: 0 0, 15px 15px, 15px 15px, 30px 30px;
background-size: 30px 30px;
}
.item[data-id="2"], .item[data-id="8"] {
background: repeating-linear-gradient(-45deg, #ddd 0, #ddd 2px, #FFF 4px, #FFF 6px);
}
.item[data-id="3"], .item[data-id="9"] {
background: #e1f3ff;
background-image: linear-gradient(#c8e4ff 1px, transparent 0), linear-gradient(90deg, #c8e4ff 1px, transparent 0);
background-size: 30px 30px;
}
.item[data-id="4"], .item[data-id="10"] {
background: #e1f3ff;
background-image: radial-gradient(#c8e4ff 30%, transparent 0), radial-gradient(#c8e4ff 30%, transparent 0);
background-size: 30px 30px;
background-position: 0 0, 15px 15px;
}
.item[data-id="5"], .item[data-id="11"] {
background-image: radial-gradient(currentColor 1px, transparent 1px);
background-size: calc(10 * 1px) calc(10 * 1px);
color: #c8e4ff !important;
}
.item[data-id="6"], .item[data-id="12"] {
background-color: #FFF;
background-image: linear-gradient( transparent 95%, rgba(0, 0, 0, .06) 50%, rgba(0, 0, 0, .06)), linear-gradient( 90deg, transparent 95%, rgba(0, 0, 0, .06) 50%, rgba(0, 0, 0, .06) );
background-size: 16px 16px;
background-repeat: repeat;
}
/* スマホのブレイクポイント */
@media screen and (max-width: 767px) {
.item {
width: 45%;
}
}
JavaScriptでも使用している [data-id=""]
の属性も、CSSで属性セレクタを指定すれば簡単にスタイルをあてることができます。
例えば、.item[data-id="6"]
のように記述すればOKなので、CSSの後半部分は「item」classの属性セレクタで背景を指定しています。
after
の疑似要素にも content: attr(data-id);
でattr関数を使って番号を表示させます。
さいごに
いかがでしたでしょうか?
今回のようなドラッグを使ったUIでは、「狭すぎず・広すぎず」のユーザーが操作する絶妙な領域も考慮する必要があるので、その辺を考慮して使ってみてください。