はじめに
ちょっとお仕事で必要が生じて、HTMLのテーブルのカラムをドラッグ&ドロップで入れ替え、その状態を保存&復元する機能を実装することになったので、その検証メモ。
今回は、Laravelを使って組んでいく。
なお、実行環境は以下だが、構築の詳細は割愛。
- VirtualBox 7.1.0
- AlmaLinux 9.4
- PHP 8.3.22
下準備
まずはLaravelのプロジェクトを作る。
cd /var/www/html composer create-project laravel/laravel:11.* sample02 chmod -R a+w sample02/storage/ chmod a+w sample02/bootstrap/cache
お仕事がLaravel 11なので、バージョンとしてLaravel 11系を指定しているが、最新のLaravel 12系でも問題なく動くと思う。
ファイルの作成・編集
sample02/resources/views/sample/index.blade.php
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <link rel="stylesheet" type="text/css" href="sample.css" /> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <script src="sample.js" type="text/javascript"></script> </head> <body> <table id="listTable"> <thead> <tr> @php $idx = 0; @endphp @while ($idx < count($show_order)) @foreach ($show_order as $column_name=> $order) @if ($order == $idx) <th>{{$column_name}}</th> @endif @endforeach @php ++$idx; @endphp @endwhile </tr> </thead> <tbody> @foreach ($persons as $person) <tr> @php $idx = 0; @endphp @while ($idx < count($show_order)) @foreach ($show_order as $column_name=> $order) @if ($order == $idx) <td> @if ($column_name === '姓') {{$person->LastName}} @elseif ($column_name === '名') {{$person->FirstName}} @elseif ($column_name === 'メールアドレス') {{$person->MailAddress}} @elseif ($column_name === '性別') {{$person->Gender}} @elseif ($column_name === '郵便番号') {{$person->ZipCode}} @elseif ($column_name === '都道府県') {{$person->Prefecture}} @elseif ($column_name === '住所') {{$person->Address}} @endif </td> @endif @endforeach @php ++$idx; @endphp @endwhile </tr> @endforeach </tbody> </table> <script type="text/javascript"> initOrderingColumns('listTable'); </script> </body> </html>
sample02/app/Http/Controllers/SampleController.php
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Session; use stdClass; class SampleController extends Controller { public function index() { // TODO 本当はDBから読み込む if (Session::has('ShowOrder')) { $showOrder = Session::get('ShowOrder'); } else { $showOrder = [ '姓' => 0, '名' => 1, 'メールアドレス' => 2, '性別' => 3, '郵便番号' => 4, '都道府県' => 5, '住所' => 6, ]; } // テーブル形式で表示するサンプルデータ $persons = []; // 1行目 $person = new stdClass(); $person->LastName = '姓1'; $person->FirstName = '名1'; $person->MailAddress = 'メールアドレス1'; $person->Gender = '性別1'; $person->ZipCode = '郵便番号1'; $person->Prefecture = '都道府県1'; $person->Address = '住所1'; $persons[] = $person; // 2行目 $person = new stdClass(); $person->LastName = '姓2'; $person->FirstName = '名2'; $person->MailAddress = 'メールアドレス2'; $person->Gender = '性別2'; $person->ZipCode = '郵便番号2'; $person->Prefecture = '都道府県2'; $person->Address = '住所2'; $persons[] = $person; // 3行目 $person = new stdClass(); $person->LastName = '姓3'; $person->FirstName = '名3'; $person->MailAddress = 'メールアドレス3'; $person->Gender = '性別3'; $person->ZipCode = '郵便番号3'; $person->Prefecture = '都道府県3'; $person->Address = '住所3'; $persons[] = $person; return view( 'sample.index', [ 'persons' => $persons, 'show_order' => $showOrder, ] ); } public function save(Request $request) { $orders = $request->get('orders'); if (isset($orders)) { // TODO 本当はDBに保存する Session::put('ShowOrder', $orders); } else { // TODO 本当はDBから削除する Session::forget('ShowOrder'); } echo json_encode(['status' => true]); } }
sample02/public/sample.css
table { border-collapse: collapse; } th, td { border: 1px solid #ccc; padding: 0.5rem; text-align: center; } th { cursor: move; } th.dropper { background-color: #fee; }
sample02/public/sample.js
function initOrderingColumns(id) { let draggingColumn; // ドラッグ開始時に実行する関数 function handleDragStart(ev) { // ドラッグされた列の位置を保存する draggingColumn = ev.target.cellIndex; } // ドロップ時に実行する関数 function handleDrop(ev) { // ドロップ先の列の位置を取得する const dropIndex = ev.target.cellIndex; if (draggingColumn !== dropIndex) { const rows = document.querySelectorAll("#" + id + " tr"); rows.forEach((row) => { if (draggingColumn < dropIndex) { for (let i = draggingColumn; i < dropIndex; ++i) { row.insertBefore(row.cells[i + 1], row.cells[i]); } } else { for (let i = draggingColumn; i > dropIndex; --i) { row.insertBefore(row.cells[i], row.cells[i - 1]); } } }); } ev.target.classList.remove("dropper"); } // ドラッグ終了時に実行する関数 function handleDragEnd(ev) { let orders = {}; const headers = document.querySelectorAll("#" + id + " thead th"); headers.forEach((header, idx) => { orders[header.innerText] = idx; }); console.log(orders); $.ajax({ type: "get", url: "sample/save", dataType: "json", data: { orders: orders, }, }) .done((result) => { // alert(result.status); }) .fail((jqXHR, textStatus, errorThrown) => { alert("Ajax通信に失敗しました。"); console.log("jqXHR : " + jqXHR.status); // HTTPステータスを表示 console.log("textStatus : " + textStatus); // タイムアウト、パースエラーなどのエラー情報を表示 console.log("errorThrown : " + errorThrown.message); // 例外情報を表示 }); } function handleDragEnter(ev) { ev.target.classList.add("dropper"); } function handleDragLeave(ev) { ev.target.classList.remove("dropper"); } // 各列のヘッダにイベントリスナーを登録する const columns = document.querySelectorAll("#" + id + " thead tr th"); columns.forEach((column) => { column.setAttribute("draggable", true); column.addEventListener("dragstart", handleDragStart); column.addEventListener("dragover", (ev) => ev.preventDefault()); column.addEventListener("drop", handleDrop); column.addEventListener("dragend", handleDragEnd); column.addEventListener("dragenter", handleDragEnter); column.addEventListener("dragleave", handleDragLeave); }); }
sample02/routes/web.php
<?php use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); Route::get('/sample', [\App\Http\Controllers\SampleController::class, 'index']); Route::get('/sample/save', [\App\Http\Controllers\SampleController::class, 'save']);
動作確認
以下のURLにアクセスして、動作確認。
http://<your-ip-address>/sample02/public/sample
- テーブルのヘッダ名をドラッグ&ドロップして入れ替え
- ページをリロードして、入れ替え内容が復元されていること
を確認できれば完成。
課題
- UIとして、カラム位置のリセットはあった方がいいかも。(サーバー側の口だけは作ってある)
参考
1つ目のサイトのサンプルだと、dragoverイベントで入れ替えを行っているが、それだとちらつきが激しいので、2つ目のサイトのサンプルを参考にdropイベントで入れ替えるように変更している。