はじめに
ちょっとお仕事で必要が生じて、HTMLのテーブルの行をドラッグ&ドロップやマウス操作(Ctrl、Shift付きも)で複数選択する機能を実装することになったので、その検証メモ。
今回は、Laravelを使って組んでいく。
なお、実行環境は以下だが、構築の詳細は割愛。
- VirtualBox 7.1.0
- AlmaLinux 9.4
- PHP 8.3.22
下準備
まずはLaravelのプロジェクトを作る。
cd /var/www/html composer create-project laravel/laravel:11.* sample04 chmod -R a+w sample04/storage/ chmod a+w sample04/bootstrap/cache
お仕事がLaravel 11なので、バージョンとしてLaravel 11系を指定しているが、最新のLaravel 12系でも問題なく動くと思う。
ファイルの作成・編集
sample04/resources/views/sample/index.blade.php
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <link rel="stylesheet" type="text/css" href="sample.css" /> <script type="text/javascript" src="sample.js"></script> </head> <body> <table id="listTable"> <thead> <tr> <th> <div>姓</div> </th> <th> <div>名</div> </th> <th> <div>メールアドレス</div> </th> </tr> </thead> <tbody> <tr> <td>姓1</td> <td>名1</td> <td>メールアドレス1</td> </tr> <tr> <td>姓2</td> <td>名2</td> <td>メールアドレス2</td> </tr> <tr> <td>姓3</td> <td>名3</td> <td>メールアドレス3</td> </tr> <tr> <td>姓4</td> <td>名4</td> <td>メールアドレス4</td> </tr> <tr> <td>姓5</td> <td>名5</td> <td>メールアドレス5</td> </tr> <tr> <td>姓6</td> <td>名6</td> <td>メールアドレス6</td> </tr> <tr> <td>姓7</td> <td>名7</td> <td>メールアドレス7</td> </tr> <tr> <td>姓8</td> <td>名8</td> <td>メールアドレス8</td> </tr> <tr> <td>姓9</td> <td>名9</td> <td>メールアドレス9</td> </tr> </tbody> </table> <script type="text/javascript"> initSelectingRows('listTable'); </script> </body> </html>
sample04/app/Http/Controllers/SampleController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class SampleController extends Controller { public function index() { return view('sample.index'); } }
今回はクライアント側だけで片付く話なので、コントローラーは要らなかったかも。
sample04/public/sample.css
table { border-collapse: collapse; } th, td { border: 1px solid #ccc; } .selected { background-color: #d0d8ff; /* 選択時の背景色 */ border-left: 3px solid #007bff; /* 選択時の左ボーダー */ }
sample04/public/sample.js
function initSelectingRows(id) { const table = document.querySelector("#" + id); if (!table) { return; } let isDragging = false; let startRow = null; let lastClickedRow = null; let evX = null; let evY = null; let selectionAdded = false; let addedByDrag = null; const rows = table.querySelectorAll("tbody tr"); function handleMouseDown(ev) { evX = ev.clientX; evY = ev.clientY; selectionAdded = false; addedByDrag = new Array(); // クリックされたのが行(tr)内かどうかをチェック const clickedRow = ev.target.closest("tr"); if (!clickedRow) { return; } // 選択された行をドラッグ開始した場合はほかの挙動をさせたいのでキャンセル if (clickedRow.classList.contains("selected")) { ev.preventDefault(); return; } isDragging = true; startRow = clickedRow; const isCtrl = ev.ctrlKey || ev.metaKey; const isShift = ev.shiftKey; if (isCtrl) { if (!startRow.classList.contains("selected")) { startRow.classList.add("selected"); selectionAdded = true; } } else if (!isShift) { // 既存の選択をクリア rows.forEach((row) => row.classList.remove("selected")); } if (isShift) { // 直前にクリックされた行とクリックされた行の間の行を選択 const lastIndex = Array.from(rows).indexOf(lastClickedRow); const startIndex = Array.from(rows).indexOf(startRow); const [start, end] = [lastIndex, startIndex].sort((a, b) => a - b); rows.forEach((row, index) => { if (index >= start && index <= end) { if (!row.classList.contains("selected")) { row.classList.add("selected"); selectionAdded = true; } } }); } else { // クリックされた行を選択 if (!startRow.classList.contains("selected")) { startRow.classList.add("selected"); selectionAdded = true; } } // 直前にクリックされた行を保持 lastClickedRow = startRow; // 複数の行を選択するために、テキスト選択を無効化 ev.preventDefault(); } function handleMouseOver(ev) { if (!isDragging || !startRow) { return; } const currentRow = ev.target.closest("tr"); if (!currentRow) { return; } // 開始行と現在の行の間の行を選択 const startIndex = Array.from(rows).indexOf(startRow); const currentIndex = Array.from(rows).indexOf(currentRow); const [start, end] = [startIndex, currentIndex].sort((a, b) => a - b); rows.forEach((row, index) => { if (index >= start && index <= end) { if (!row.classList.contains("selected")) { row.classList.add("selected"); selectionAdded = true; addedByDrag.push(row); } } else { const idx = addedByDrag.indexOf(row); if (idx >= 0) { row.classList.remove("selected"); addedByDrag.splice(idx, 1); } } }); } function handleMouseUp(ev) { isDragging = false; startRow = null; addedByDrag = null; const currentRow = ev.target.closest("tr"); if (!currentRow) { return; } // 選択状態の行をクリックした場合の処理 if ( ev.clientX === evX && ev.clientY === evY && !selectionAdded && currentRow.classList.contains("selected") ) { const isCtrl = ev.ctrlKey || ev.metaKey; if (isCtrl) { // クリックされた行だけを非選択状態に変更する currentRow.classList.remove("selected"); } else { // クリックされた行だけが選択された状態にする rows.forEach((row) => row.classList.remove("selected")); currentRow.classList.add("selected"); } } } table.addEventListener("mousedown", handleMouseDown); table.addEventListener("mouseover", handleMouseOver); document.addEventListener("mouseup", handleMouseUp); }
sample04/routes/web.php
<?php use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); Route::get('/sample', [\App\Http\Controllers\SampleController::class, 'index']);
動作確認
以下のURLにアクセスして動作確認。
http://<your-ip-address>/sample04/public/sample
- ドラッグ&ドロップで行が複数選択できること
- マウス操作で行が選択できること
- CtrlキーやShiftキーとの組み合わせも確認
が確認できれば完成。
参考
Geminiに手伝ってもらってました。