ドラッグアンドドロップでタスクを移動させる ガントチャートをWebアプリとしてつくる(4)
drag-drop-task-js
カテゴリを作成してドラッグアンドドロップでカテゴリの表示順序やタスクを別カテゴリに移動できるようにします。
Contents
前回への追加と変更
まずHTML部分を示します。task1.jsは一部を除き前回のカレンダーをポップアップ表示させるで作成したJavaScriptと同じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>ドラッグアンドドロップでタスクを移動させる</title> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <button onclick="AddCategory ()">カテゴリ追加</button> <div id="categorys"></div> <div id="popup-calendar"> <div id="close-btn">?</div> <div id="next-prev-button"> <button id="prev" onclick="Prev()">前月</button> <button id="next" onclick="Next()">次月</button> </div> <h1 id="calendar-header"></h1> <div id="calendar"></div> </div> <script src='./mini-calendar.js'></script> <script src='./task1.js'></script> <script src='./task2.js'></script> </body> </html> |
前回との変更部分ですが、グローバル変数と最初に実行するInitPopupCalendar関数だけ以下のように変更します。
task1.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
let curDateElement = null; let popupCalendar = null; const today = new Date(); let showDate = new Date(today.getFullYear(), today.getMonth(), 1); InitPopupCalendar(); function InitPopupCalendar(){ popupCalElement = document.getElementById('popup-calendar'); popupCalElement.style.position = 'fixed'; popupCalElement.style.top = '0px'; popupCalElement.style.visibility = 'hidden'; popupCalElement.style.width = '280px'; SettingClosebtn(); SettingClickDay(); let prevElement = document.getElementById('prev'); let nextElement = document.getElementById('next'); prevElement.style.float = 'left'; nextElement.style.float = 'right'; InitPrevNextBotton(prevElement); InitPrevNextBotton(nextElement); let calendarHeader = document.getElementById('calendar-header'); let calendar = document.getElementById('calendar'); popupCalendar = new Calendar(calendarHeader, calendar); } |
今回追加する部分ですが、最初にグローバル変数として要素内のクリックされた位置と要素の位置の差を保存しておく変数を用意しておきます。そしてドラッグアンドドロップに対応できるようにイベントリスナーを追加します。
task2.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 要素内のクリックされた位置と要素の位置の差を保存しておく変数 let dx; let dy; let categorysElement = document.getElementById("categorys"); categorysElement.style.marginTop = '10px'; categorysElement.style.paddingBottom = '100px'; // マウスボタンが押されたときに発火 document.body.addEventListener('mousedown', mdown, false); document.body.addEventListener('touchstart', mdown, false); // マウスが移動したときに発火 document.body.addEventListener('mousemove', mmove, false); document.body.addEventListener('touchmove', mmove, false); // マウスボタンが離されたとき発火 document.body.addEventListener("mouseup", mup, false); document.body.addEventListener("touchend", mup, false); //カーソルが外れたとき発火 document.body.addEventListener("touchleave", mleave, false); document.body.addEventListener("mouseleave", mleave, false); |
カテゴリを追加する
最初にカテゴリ追加ボタンをクリックしたら新しいカテゴリを追加する処理を追加します。ここではカテゴリ名を設定するためのテキストボックスとタスクに追加ボタンを表示させます。
要素のうえにマウスが移動したときに移動可能であることがわかるようにマウスポインタの形状を変えます。それからclassを調べればカテゴリであることがわかるようにclass =”category”にするとともに、テキストボックスをクリックしたときはドラッグアンドドロップの処理が始まらないようにclass =”no-drag”にしておきます。
task2.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
function AddCategory(){ let newElement = document.createElement('div'); newElement.style.textAlign = 'left'; newElement.style.cursor = 'move'; newElement.style.width = 'auto'; newElement.style.height = 'auto'; newElement.style.fontSize = '14px'; newElement.style.backgroundColor = '#fff8dc'; newElement.style.padding = '10px'; newElement.style.paddingLeft = '30px'; newElement.style.marginBottom = '5px'; newElement.style.lineHeight = '24px'; newElement.classList.add('category'); let label = document.createElement('label'); label.innerText = 'カテゴリ名:'; label.style.cursor = 'move'; newElement.appendChild(label); let input = document.createElement('input'); input.type = 'text'; input.value = '新しいカテゴリ'; input.size = 24; input.classList.add('no-drag'); newElement.appendChild(input); let buttonElement = document.createElement('button'); buttonElement.innerText = 'タスクの追加'; buttonElement.classList.add('no-drag'); buttonElement.onclick = AddTask; buttonElement.style.marginLeft = '20px'; newElement.appendChild(buttonElement); categorysElement.appendChild(newElement); } |
タスクを追加する
次にタスクを追加する処理を追加します。
タスク名と担当者名を設定するためのテキストボックスを表示させるとともに、日時を表示しクリックしたらカレンダーで日時を変更できる要素を追加します。そしてクリックされたらカレンダーを表示できるようにイベントリスナーを追加しておきます。
カテゴリと同様に要素のうえにマウスが移動したときに移動可能であることがわかるようにマウスポインタの形状を変えます。それからclassを調べればタスクであることがわかるようにclass =”task”にするとともに、テキストボックスや日時を変更する要素をクリックしたときはドラッグアンドドロップの処理が始まらないようにclass =”no-drag”にしておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
function AddTask(){ let newElement = document.createElement('div'); newElement.style.textAlign = 'left'; newElement.style.cursor = 'move'; newElement.style.width = 'auto'; newElement.style.height = 'auto'; newElement.style.fontSize = '14px'; newElement.style.backgroundColor = '#eee8aa'; newElement.style.padding = '10px'; newElement.style.paddingLeft = '30px'; newElement.style.marginTop = '5px'; newElement.style.lineHeight = '24px'; newElement.classList.add('task'); let label = document.createElement('label'); label.innerText = 'タスク名:'; label.style.cursor = 'move'; newElement.appendChild(label); let input = document.createElement('input'); input.type = 'text'; input.value = '新しいタスク'; input.size = 24; input.classList.add('no-drag'); newElement.appendChild(input); label = document.createElement('label'); label.innerText = '担当者名:'; label.style.cursor = 'move'; label.style.marginLeft = '10px'; newElement.appendChild(label); input = document.createElement('input'); input.type = 'text'; input.value = '担当者'; input.size = 24; input.classList.add('no-drag'); newElement.appendChild(input); label = document.createElement('label'); label.innerText = '開始日:'; label.style.cursor = 'move'; label.style.marginLeft = '10px'; newElement.appendChild(label); let start = document.createElement('span'); let today = new Date(); let defaultDayText = `${today.getFullYear()}-${ZeroPadding(today.getMonth()+1, 2)}-${ZeroPadding(today.getDate(), 2)}`; start.innerText = defaultDayText; start.classList.add('no-drag'); newElement.appendChild(start); label = document.createElement('label'); label.innerText = ' 終了日:'; label.style.cursor = 'move'; newElement.appendChild(label); let end = document.createElement('span'); end.innerText = defaultDayText; end.classList.add('no-drag'); newElement.appendChild(end); // クリックされたらカレンダーを表示できるようにする start.addEventListener('click', function(e){ if(curDateElement== null) ShowPopupCalendar(start); }); end.addEventListener('click', function(e){ if(curDateElement== null) ShowPopupCalendar(end); }); this.parentElement.appendChild(newElement); } |
ドラッグアンドドロップで移動する処理
ドラッグアンドドロップに関する処理を追加します。ドラッグアンドドロップが終わるまでは実際の移動処理は行なわず枠だけの矩形を描画してここに移動されることがわかるようにします。
以下は実際に移動させる要素と枠だけの矩形を描画するための要素を保存しておくグローバル変数です。
1 2 3 |
// グローバル変数 let movingElement = null; let dragSource = null; |
マウスボタンが押されたときの処理
PCとスマホ両方に対応できるようにしているのですが、スマホでは使いにくいと思います。
クリックされた要素のclassを調べて”no-drag”であればなにもしません。そうでない場合は移動しようとしているのがカテゴリなのかタスクなのかを調べます。どちらでもない場合はなにもしません。
デフォルトの動作だと文字が選択されている状態でドラッグが開始されると期待している動きとは異なる現象がおきる場合があります。そこでデフォルトの動作ではなく自分でコントロールできるようにしています。
要素の座標とカーソルの座標の差をグローバル変数に保存し、移動中であることを示す枠だけの矩形を描画するための要素(ここでは movingElement)を生成します。また前回の残骸が残っている場合は先に削除しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
function mdown(e) { // タッチイベントとマウスのイベントの差異を吸収 let event; if(e.type === "mousedown") event = e; else event = e.changedTouches[0]; if(e.target.classList.contains("no-drag")) return; if(e.target.classList.contains("category")) dragSource = e.target; else{ if(e.target.classList.contains("task")) dragSource = e.target; else if(e.target.parentElement.classList.contains("task")) dragSource = e.target.parentElement; else return; } // デフォルト動作を抑制 e.preventDefault(); // 要素の座標を取得 let x = dragSource.offsetLeft; let y = dragSource.offsetTop; // 要素の座標とカーソルの座標の差を取得 dx = event.pageX - x; dy = event.pageY - y; if(movingElement != null) movingElement.remove(); movingElement = document.createElement('div'); movingElement.style.cursor = 'move'; movingElement.style.position = 'absolute'; movingElement.style.width = `${dragSource.offsetWidth}px`; movingElement.style.height = `${dragSource.offsetHeight}px`; movingElement.style.fontSize = '20px'; movingElement.style.textAlign = 'center'; movingElement.style.border = '2px solid #cccccc'; movingElement.style.left = x + 'px'; movingElement.style.top = y + 'px'; categorysElement.appendChild(movingElement); } |
マウスカーソルが動いたときの処理
以下はマウスカーソルが動いたときに実行される処理です。マウスが動いた場所にmovingElementを移動させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function mmove(e) { if(movingElement == null) return; // マウスとタッチの差異を吸収 let event; if(e.type === "mousemove") event = e; else event = e.changedTouches[0]; // デフォルト動作を抑制 e.preventDefault(); // マウスが動いた場所に要素を動かす movingElement.style.left = event.pageX - dx + "px"; movingElement.style.top = event.pageY - dy + "px"; } |
マウスボタンが離されたときの処理
以下はマウスボタンが離されたときに実行される処理です。
movingElementが存在しない場合はなにもしません。存在する場合は取り除き、movingElementにはnullを代入します。ドロップされた場合それはカテゴリかもしれないし、タスクかもしれません。そこでclassからどちらなのか判断し、それぞれに応じた処理を別の関数でおこなっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
function mup(e) { if(movingElement != null){ movingElement.remove(); movingElement = null; // マウスとタッチの差異を吸収 let event; if(e.type === "mouseup") event = e; else event = e.changedTouches[0]; // デフォルト動作を抑制 e.preventDefault(); // ドロップされた処理をおこなう if(dragSource.classList.contains("category")) DropToCategoryFromCategory(event); else if(dragSource.classList.contains("task")) DropToCategoryFromTask(event); // ドラッグアンドドロップ終了。dragSourceにnullを代入 dragSource = null; } } |
先にカーソルが外れたときの処理を示します。ここでやっているのは移動中であることを示す枠だけの矩形を消去してmovingElementにnullを代入しているだけです。
1 2 3 4 5 6 |
function mleave(e) { if(movingElement != null){ movingElement.remove(); movingElement = null; } } |
カテゴリがカテゴリの上にドロップされたときの処理を示します。上から何番目のカテゴリにドロップされたかを調べて並べ替えているだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
function DropToCategoryFromCategory(evnt){ let dropIndex = -1; // 全体のなかからカテゴリの要素だけ集める let categoryElements = GetCategoryElements(); let childCount = categoryElements.length; // 移動されるカテゴリの移動先よりも上に表示されるべきカテゴリの配列 let upperChilden = []; // 移動されるカテゴリの移動先よりも下に表示されるべきカテゴリの配列 let lowerChilden = []; // このループのなかでドロップされたカテゴリは上からdropIndex番目に移動されることが確定する for(let i=0; i< childCount; i++) { let categoryElement = categoryElements[i]; let x = categoryElement.offsetLeft; let y = categoryElement.offsetTop; let h = categoryElement.offsetHeight; if(evnt.pageY < y + h){ if(dropIndex == -1) dropIndex = i; } // ドロップされたカテゴリ以外はドロップされたカテゴリより // 上に表示されるべきか下に表示されるべきか? // ドロップされたカテゴリの位置が確定する前なら上側。確定後は下側 if(categoryElement != dragSource){ if(dropIndex == -1) upperChilden.push(categoryElement); else lowerChilden.push(categoryElement); } } let children = []; for(let i=0; i<childCount; i++) { children.push(categoryElements[i]); } // 並べかえる。一旦全部削除して再挿入する children.forEach(x =>{ x.remove(); }); upperChilden.forEach(x =>{ categorysElement.appendChild(x); }); categorysElement.appendChild(dragSource); lowerChilden.forEach(x =>{ categorysElement.appendChild(x); }); } |
categorysElementのなかにはカテゴリ以外の要素があるかもしれません(いまのところない)。そこでカテゴリ要素だけ集めて返す関数を作成します。
1 2 3 4 5 6 7 8 9 |
function GetCategoryElements(){ let categorys = []; let childCount = categorysElement.childElementCount; for(let i=0; i<childCount; i++){ if(categorysElement.children[i].classList.contains('category')) categorys.push(categorysElement.children[i]); } return categorys; } |
タスクがカテゴリの上にドロップされたときの処理を示します。
まずどのカテゴリの上にドロップされたのかを調べます。以下の処理で取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function DropToCategoryFromTask(evnt){ // ドロップされたカテゴリが見つからない場合はなにもしない let dropTargetCategory = GetCategoryElementDropToTask(evnt); if(dropTargetCategory == null) return; // ・・・・ } function GetCategoryElementDropToTask(evnt){ let categoryElements = GetCategoryElements(); let childCount = categoryElements.length; for(let i=0; i<childCount; i++) { let y = categoryElements[i].offsetTop; let h = categoryElements[i].offsetHeight; if(evnt.pageY < y + h) return categoryElements[i]; } return null; } |
それからカテゴリ要素にはボタンやラベルなどタスク要素以外のものが存在します。カテゴリ要素のなかにあるタスク要素を取得する必要があります。
1 2 3 4 5 6 7 8 9 10 11 |
function GetTaskElements(categoryElement){ let childCnt = categoryElement.childElementCount; let children = categoryElement.children; let tasks = []; for(let i=0; i<childCnt; i++){ if(children[i].classList.contains('task')) tasks.push(children[i]); } return tasks; } |
どのカテゴリの上にドロップされたのかがわかったら、次にどのタスクの上にドロップされたのかを調べます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
function DropToCategoryFromTask(evnt){ // ドロップされたカテゴリが見つからない場合はなにもしない let dropTargetCategory = GetCategoryElementDropToTask(evnt); if(dropTargetCategory == null) return; let dropIndex = -1; let taskElements = GetTaskElements(dropTargetCategory); let lowerChilden = []; let upperChilden = []; for(let i=0; i<taskElements.length; i++) { let taskElement = taskElements[i]; let x = taskElement.offsetLeft; let y = taskElement.offsetTop; let h = taskElement.offsetHeight; if(evnt.pageY < y + h){ if(dropIndex == -1) dropIndex = i; } if(dragSource != taskElement){ if(dropIndex == -1) upperChilden.push(taskElement); else lowerChilden.push(taskElement); } } // 並べかえる。一旦全部削除して再挿入する taskElements.forEach(x =>{ x.remove(); }); // 再挿入 upperChilden.forEach(x =>{ dropTargetCategory.appendChild(x); }); dropTargetCategory.appendChild(dragSource); lowerChilden.forEach(x =>{ dropTargetCategory.appendChild(x); }); } |