はじめに
ちょっとお仕事で必要が生じて、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.* sample05 chmod -R a+w sample05/storage/ chmod a+w sample05/bootstrap/cache
お仕事がLaravel 11なので、バージョンとしてLaravel 11系を指定しているが、最新のLaravel 12系でも問題なく動くと思う。
ファイルの作成・編集
sample05/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" tabindex="0"> <thead> <tr> <th> <div>姓</div> </th> <th> <div>名</div> </th> <th> <div>メールアドレス</div> </th> </tr> </thead> <tbody> @for ($i = 1; $i <= 100; ++$i) <tr class="selectable"> <td>姓{{mb_convert_kana($i, 'N')}}</td> <td>名{{mb_convert_kana($i, 'N')}}</td> <td>メールアドレス{{mb_convert_kana($i, 'N')}}</td> </tr> @endfor </tbody> </table> <script type="text/javascript"> initSelectingRows('listTable'); </script> </body> </html>
sample05/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'); } }
今回も、クライアント側の処理だけなので、コントローラーは要らなかったかも。
sample05/public/sample.css
table { border-collapse: collapse; outline: none; } th, td { border: 1px solid #ccc; } .selectable:hover { background-color: #f0f8ff; } .selectable.selected { background-color: #d0d8ff; /* 選択時の背景色 */ border-left: 3px solid #007bff; /* 選択時の左ボーダー */ } .selectable.focused { border: 2px dashed #ccc; }
sample05/public/sample.js
function initSelectingRows(id) { const table = document.querySelector("#" + id); const rows = table.querySelectorAll("tr.selectable"); let selectedIndex = -1; let focusedIndex = -1; let addedByArrowKey = new Array(); let rangeStartIndex = -1; // 初期フォーカス設定 if (rows.length > 0) { selectedIndex = 0; rows[selectedIndex].classList.add("selected"); focusedIndex = 0; rows[selectedIndex].classList.add("focused"); } function handleKeyDown(ev) { // テーブル全体にフォーカスがある場合のみ処理 if (document.activeElement !== table) { return; } const isCtrl = ev.ctrlKey || ev.metaKey; const isShift = ev.shiftKey; if (!isShift) { addedByArrowKey.splice(0, addedByArrowKey.length); if (!isCtrl) { rows.forEach((row, index) => { if (index !== selectedIndex) { row.classList.remove("selected"); } }); } } let newIndex = -1; if (isCtrl) { newIndex = focusedIndex; } else if (selectedIndex !== focusedIndex) { newIndex = focusedIndex; } else { newIndex = selectedIndex; } if (isShift) { if (rangeStartIndex === -1) { rangeStartIndex = newIndex; } } else { rangeStartIndex = -1; } // 下矢印キー(ArrowDown) if (ev.key === "ArrowDown") { if (isCtrl || selectedIndex !== focusedIndex) { if (focusedIndex < rows.length - 1) { ++newIndex; } } else { if (selectedIndex < rows.length - 1) { ++newIndex; } } } // 上矢印キー(ArrowUp) else if (ev.key === "ArrowUp") { if (isCtrl || selectedIndex !== focusedIndex) { if (focusedIndex > 0) { --newIndex; } } else { if (selectedIndex > 0) { --newIndex; } } } // それ以外はスキップ else { return; } if (newIndex !== selectedIndex || newIndex !== focusedIndex) { if (!isCtrl) { // 古い行の選択を解除 if (selectedIndex >= 0 && selectedIndex < rows.length) { if (isShift) { // rangeStartIndex と selectedIndex の間にある行のみ選択状態にし、 // 残りのうち addedByArrowKey にあるものは非選択状態に戻す addedByArrowKey.forEach((row) => { let idx = 0; for (; idx < rows.length; ++idx) { if (rows[idx] === row) { break; } } const [start, end] = [ newIndex, rangeStartIndex, ].sort((a, b) => a - b); if (idx < start || idx > end) { row.classList.remove("selected"); } }); } else { const row = rows[selectedIndex]; row.classList.remove("selected"); addedByArrowKey.splice(addedByArrowKey.indexOf(row), 1); } } } // 古い行のフォーカスを削除 if (focusedIndex >= 0 && focusedIndex < rows.length) { rows[focusedIndex].classList.remove("focused"); } if (!isCtrl) { // 新しい行を選択 selectedIndex = newIndex; const row = rows[selectedIndex]; row.classList.add("selected"); if (isShift) { addedByArrowKey.push(row); } } // 新しい行にフォーカス focusedIndex = newIndex; rows[focusedIndex].classList.add("focused"); // 行がview内に入るようにスクロール if (isCtrl) { rows[focusedIndex].scrollIntoView({ behavior: "smooth", block: "center", }); } else { rows[selectedIndex].scrollIntoView({ behavior: "smooth", block: "center", }); } } ev.preventDefault(); } table.addEventListener("keydown", handleKeyDown); }
sample05/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>/sample05/public/sample
- (テーブル内をクリックしてフォーカスをテーブルに当てた後に)
- カーソルキーの上下を押すと選択状態が変わること
- Ctrlキーを押しながらカーソルキーの上下を押すとフォーカス(破線)のみが移動すること
- Shiftキーを押しながらカーソルキーの上下を押すと範囲選択ができること
辺りが確認できれば完成。
参考
Geminiに手伝ってもらってました。