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


tableのカラム幅をドラッグ&ドロップで変更して状態を保存&復元する

はじめに

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

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

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

下準備

まずはLaravelのプロジェクトを作る。

cd /var/www/html
composer create-project laravel/laravel:11.* sample03
chmod -R a+w  sample03/storage/
chmod a+w  sample03/bootstrap/cache

お仕事がLaravel 11なので、バージョンとしてLaravel 11系を指定しているが、最新のLaravel 12系でも問題なく動くと思う。

ファイルの作成・編集

sample03/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>
        <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>
        </tbody>
    </table>
</body>

</html>

コツとしては、thタグに直接resizeプロパティを設定しちゃうと、あとで幅を縮小できないという悩みにぶつかるので、thタグの子としてdivタグなどを挟んであげる必要がある。

sample03/app/Http/Controllers/SampleController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;

class SampleController extends Controller
{
    public function index()
    {
        return view('sample.index');
    }
    public function save(Request $request)
    {
        $columnName = $request->get('column_name');
        $width = $request->get('width');
        if ($width > 0) {
            // TODO 本当はDBから読み書きする
            if (Session::has('ShowWidth')) {
                $widths = Session::get('ShowWidth');
            } else {
                $widths = [];
            }
            $widths[$columnName] = $width;
            Session::put('ShowWidth', $widths);
        }
        echo json_encode(['status' => true]);
    }
    public function load()
    {
        // TODO 本当はDBから読み込む
        echo json_encode(['status' => true, 'widths' => Session::get('ShowWidth')]);
    }
}

sample03/public/sample.css

table {
    border-collapse: collapse;
    table-layout: fixed;
}
th,
td {
    border: 1px solid #ccc;
    /* セル内のテキストが改行されないようにする */
    white-space: nowrap;
    /* セルからはみ出た内容を非表示にする */
    overflow: hidden;
    /* 非表示になったテキストの末尾を「...」で表示する */
    text-overflow: ellipsis;
}
th div {
    resize: horizontal;
    overflow: hidden;
}

sample03/public/sample.js

$(function () {
    // テーブルヘッダのリサイズを検知して変更後の幅を保存するためのResizeObserver
    const resizeObserver = new ResizeObserver((entries) => {
        // DOMツリー生成時のイベントを無視する(やっつけ)
        if (entries.length === 1) {
            entries.forEach((entry) => {
                console.log(
                    entry.target.innerText +
                        " is resized: " +
                        entry.target.style.width +
                        ", " +
                        entry.target.offsetWidth
                );
                $.ajax({
                    type: "get",
                    url: "sample/save",
                    dataType: "json",
                    data: {
                        column_name: entry.target.innerText,
                        width: entry.target.offsetWidth,
                    },
                })
                    .done((result) => {
                        const allRows = document.querySelectorAll("table tbody tr");
                        const header = entry.target;
                        allRows.forEach((row) => {
                            const columnIndex =
                                header.parentNode.cellIndex;
                            const cells = row.children;
                            if (cells.length > columnIndex) {
                                cells[columnIndex].style.width =
                                    header.style.width;
                                cells[columnIndex].style.maxWidth =
                                    header.style.width;
                            }
                        });
                    })
                    .fail((jqXHR, textStatus, errorThrown) => {
                        alert("Ajax通信に失敗しました。");
                        console.log("jqXHR          : " + jqXHR.status); // HTTPステータスを表示
                        console.log("textStatus     : " + textStatus); // タイムアウト、パースエラーなどのエラー情報を表示
                        console.log("errorThrown    : " + errorThrown.message); // 例外情報を表示
                    });
            });
        }
    });
    // テーブルのヘッダにResizeObserverを紐づけ
    const headers = document.querySelectorAll("table thead th div");
    headers.forEach((header) => {
        resizeObserver.observe(header);
    });
    // 初期サイズをロード
    $.ajax({
        type: "get",
        url: "sample/load",
        dataType: "json",
    })
        .done((result) => {
            if (result.status && result.widths) {
                const headers = document.querySelectorAll("table thead th div");
                const allRows = document.querySelectorAll("table tbody tr");

                headers.forEach((header) => {
                    if (result.widths[header.innerText]) {
                        header.style.width =
                            result.widths[header.innerText] + "px";
                        allRows.forEach((row) => {
                            const columnIndex =
                                header.parentNode.cellIndex;
                            const cells = row.children;
                            if (cells.length > columnIndex) {
                                cells[columnIndex].style.maxWidth =
                                    result.widths[header.innerText] + "px";
                                cells[columnIndex].style.width =
                                    result.widths[header.innerText] + "px";
                            }
                        });
                    }
                });
            }
        })
        .fail((jqXHR, textStatus, errorThrown) => {
            alert("Ajax通信に失敗しました。");
            console.log("jqXHR          : " + jqXHR.status); // HTTPステータスを表示
            console.log("textStatus     : " + textStatus); // タイムアウト、パースエラーなどのエラー情報を表示
            console.log("errorThrown    : " + errorThrown.message); // 例外情報を表示
        });
});

sample03/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']);
Route::get('/sample/load', [\App\Http\Controllers\SampleController::class, 'load']);

動作確認

以下のURLにアクセスして動作確認。

http://<your-ip-address>/sample03/public/sample

  • ドラッグ&ドロップでカラム幅が拡大、縮小できること
  • リロードすると、状態が復元されること

が確認できれば完成。

参考

end0tknr.hateblo.jp

webfrontend.ninja




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

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