以下の内容はhttps://hhelibex.hatenablog.jp/entry/2025/10/13/132733より取得しました。


tableのカラムをドラッグ&ドロップで入れ替えて状態を保存&復元する

はじめに

ちょっとお仕事で必要が生じて、HTMLのテーブルのカラムをドラッグ&ドロップで入れ替え、その状態を保存&復元する機能を実装することになったので、その検証メモ。

今回は、Laravelを使って組んでいく。

なお、実行環境は以下だが、構築の詳細は割愛。

下準備

まずは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として、カラム位置のリセットはあった方がいいかも。(サーバー側の口だけは作ってある)

参考

www.studycloudtech.com

writening.net

1つ目のサイトのサンプルだと、dragoverイベントで入れ替えを行っているが、それだとちらつきが激しいので、2つ目のサイトのサンプルを参考にdropイベントで入れ替えるように変更している。




以上の内容はhttps://hhelibex.hatenablog.jp/entry/2025/10/13/132733より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14