はじめに
ちょっとお仕事で必要が生じて、jsTreeというツリー構造を作れるライブラリで作ったツリーのノードに、外部のドラッグソース(具体的にはテーブルの行)からドラッグ&ドロップする機能を実装することになったので、その検証メモ。
今回は、Laravelを使って組んでいく。
なお、実行環境は以下だが、構築の詳細は割愛。
- VirtualBox 7.1.0
- AlmaLinux 9.4
- PHP 8.3.22
下準備
まずはLaravelのプロジェクトを作る。
cd /var/www/html composer create-project laravel/laravel:11.* sample08 chmod -R a+w sample08/storage/ chmod a+w sample08/bootstrap/cache
お仕事がLaravel 11なので、バージョンとしてLaravel 11系を指定しているが、最新のLaravel 12系でも問題なく動くと思う。
ファイルの作成・編集
いろいろと調べながら組み立てていったのだが、最終的にjsTreeの実装に依存する部分が出てしまったのはご容赦いただきたい。
sample08/resources/views/sample/index.blade.php
<!DOCTYPE html> <html lang="ja"> <head> <base href="/sample08/public/" /> <meta charset="UTF-8" /> <title>jsTreeにドロップ</title> </head> <body> <iframe src="sample/menu"></iframe> <iframe src="sample/list"></iframe> </body> </html>
sample08/resources/views/sample/list.blade.php
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <base href="/sample07/public/" /> <title>ドラッグ元</title> <link rel="stylesheet" type="text/css" href="sample.css" /> <script type="text/javascript" src="sample.js"></script> </head> <body> <table id="articleList"> <thead> <tr> <th>商品番号</th> <th>商品名</th> </tr> </thead> <tbody> <tr data-id="A12345" class="draggable jstree-draggable" draggable="true"> <td>A12345</td> <td>こしひかり</td> </tr> <tr data-id="B67890" class="draggable jstree-draggable" draggable="true"> <td>B67890</td> <td>あきたこまち</td> </tr> <tr data-id="C54321" class="draggable jstree-draggable" draggable="true"> <td>C54321</td> <td>ゆめぴりか</td> </tr> </tbody> </table> <script type="text/javascript"> initDrag('#articleList tr'); </script> </body> </html>
sample08/resources/views/sample/menu.blade.php
<!DOCTYPE html> <html lang="ja"> <head> <base href="/sample08/public/" /> <meta charset="UTF-8" /> <title>ドロップ先</title> <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> <!-- jsTree --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/themes/default/style.min.css" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/jstree/3.2.1/jstree.min.js"></script> <link rel="stylesheet" type="text/css" href="sample.css" /> <script type="text/javascript" src="sample.js"></script> </head> <body> <div id="tree">Loading ...</div> <script type="text/javascript"> const treeData = [{ id: 0, parent: '#', text: 'root', icon: 'jstree-folder', }, { id: 1, parent: 0, text: 'folder1', icon: 'jstree-folder', }, { id: 2, parent: 0, text: 'folder2', icon: 'jstree-folder', }, { id: 3, parent: 2, text: 'folder2-1', icon: 'jstree-folder', }, { id: 4, parent: 2, text: 'folder2-2', icon: 'jstree-folder', }, ]; initDrop('#tree'); </script> </body> </html>
sample08/public/sample.css
table { border-collapse: collapse; outline: none; } th, td { border: 1px solid #ccc; } .draggable { cursor: grab; }
sample08/public/sample.js
function initDrag(selector) { const draggables = document.querySelectorAll(selector); draggbles.forEach((draggable) => { draggable.addEventListener("dragstart", function (ev) { console.log(draggable.dataset.id + ": Drag start"); // ドラッグするデータの種類と値を設定 ev.dataTransfer.setData( "text/dataIds", JSON.stringify([draggable.dataset.id]) ); // ドラッグ元の要素を一時的に非表示にする setTimeout(() => { draggable.style.opacity = "0.5"; }, 0); }); draggable.addEventListener("dragend", (ev) => { // ドラッグ終了時に元のスタイルに戻す draggable.style.opacity = "1"; }); }); } function initDrop(selector) { $(selector) .jstree({ core: { check_callback: function ( operation, node, node_parent, node_position, more ) { // operation: 実行される操作 (例: 'move_node', 'copy_node') // node: ドラッグされているノード // node_parent: ドロップ先の親ノード // node_position: ドロップ先の位置 // more: その他の情報 // ツリーの外部からのドロップでかつフォルダの場合にのみ許可 if ( more.is_foreign && node_parent.icon === "jstree-folder" ) { return true; } return false; // それ以外はドロップを禁止 }, data: treeData, }, plugins: ["dnd"], }) .on("loaded.jstree", function () { $(selector).jstree("open_all"); }); function setDropable(selector) { const allNodes = $(selector).jstree(true).get_json("#", { flat: true, }); console.log(allNodes); allNodes.forEach((nodeObj) => { const node = document.getElementById(nodeObj.id); if (node && !node.dropable) { console.log(node); node.addEventListener("dragover", function (ev) { console.log("dragover"); ev.preventDefault(); }); node.addEventListener("dragleave", function (ev) { console.log("dragleave"); }); node.addEventListener("drop", function (ev) { // 本来のドロップ先の親(の親・・)まで呼ばれてしまうので、それをはじく // TODO jsTreeの実装依存 if (ev.target.parentNode !== node) { return; } if (ev.ctrlKey) { console.log("copy"); } else { console.log("move"); } console.log(ev); console.log(node); console.log(ev.target); console.log(ev.target.parentNode); console.log(ev.dataTransfer.getData("text/dataIds")); console.log(node.id); }); node.dropable = true; } }); } $(selector).on("ready.jstree", function (ev) { console.log("ready.jstree"); setDropable(selector); }); $(selector).on("open_node.jstree", function (ev) { console.log("open_node.jstree"); setDropable(selector); }); $(selector).on("create_node.jstree", function (ev) { console.log("create_node.jstree"); setDropable(selector); }); $(selector).on("move_node.jstree", function (ev) { console.log("move_node.jstree"); setDropable(selector); }); $(selector).on("copy_node.jstree", function (ev) { console.log("copy_node.jstree"); setDropable(selector); }); }
sample08/routes/web.php
<?php use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); Route::get('/sample', function () { return view('sample.index'); }); Route::get('/sample/menu', function () { return view('sample.menu'); }); Route::get('/sample/list', function () { return view('sample.list'); });
動作確認
以下のURLにアクセスして動作確認。
http://<your-ip-address>/sample08/public/sample
ドラッグ元のテーブルの行をドラッグして、ドロップ先にドロップしたときにIDがコンソールに出力されれば完成(ほかにもいろいろとデバッグメッセージが出るけど)。
懸念点
ドロップしたときに、本来のドロップ先のノードの親(の親・・)のイベントハンドラーも呼ばれてしまうので、それによる処理が走らないようにするために入れた判定条件の中にjsTreeの実装に依存する部分が含まれてしまっている。
参考
最初、Geminiとの会話で頑張ろうとしたのだが、どうにもうまく動かなくて、参照しているサイトを覗いてコードを組み立てた。