はじめに
ちょっとお仕事で必要が生じて、iframe内のコンテンツに対する右クリックメニューを表示しなければならなくなったので、その検証メモ。
要件を箇条書きにすると、以下のような感じ。
- iframe内のコンテンツを操作するための右クリックメニューの表示
- iframeの枠で見切れないようにする
- サブメニューもある
今回は、Laravelを使って組んでいく。
なお、実行環境は以下だが、構築の詳細は割愛。
- VirtualBox 7.1.0
- AlmaLinux 9.4
- PHP 8.3.22
下準備
まずはLaravelのプロジェクトを作る。
cd /var/www/html composer create-project laravel/laravel:11.* sample06 chmod -R a+w sample06/storage/ chmod a+w sample06/bootstrap/cache
お仕事がLaravel 11なので、バージョンとしてLaravel 11系を指定しているが、最新のLaravel 12系でも問題なく動くと思う。
ファイルの作成・編集
いろいろと調べた結果として、iframe内で右クリックメニューを出してもiframeの枠で見切れてしまうので、postMessage()を使ってiframe内から親ページにメッセージを送り、親ページの方で右クリックメニューを表示する。メニュー項目がクリックされたら、親ページからiframeにメッセージを送り返してiframe側で実際の処理を行う。
sample06/resources/views/sample/index.blade.php
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <base href="/sample06/public/" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>親ページ</title> <link rel="stylesheet" type="text/css" href="sample.css" /> <script type="text/javascript" src="sample.js"></script> </head> <body> <h1>親ページ</h1> <iframe src="sample/frame" id="my-iframe"></iframe> <div id="custom-context-menu" class="context-menu"> <ul> <li>メニュー項目1</li> <li class="has-submenu"> <span>メニュー項目2</span> <ul class="submenu"> <li> <nobr>サブメニュー項目2ー1</nobr> </li> <li> <nobr>サブメニュー項目2-2</nobr> </li> <li> <nobr>サブメニュー項目2-3</nobr> </li> <li> <nobr>サブメニュー項目2-4</nobr> </li> <li> <nobr>サブメニュー項目2-5</nobr> </li> </ul> </li> <li>メニュー項目3</li> <li>メニュー項目4</li> <li>メニュー項目5</li> <li class="has-submenu"> <span>メニュー項目6</span> <ul class="submenu"> <li> <nobr>サブメニュー項目6ー1</nobr> </li> <li> <nobr>サブメニュー項目6-2</nobr> </li> <li> <nobr>サブメニュー項目6-3</nobr> </li> <li> <nobr>サブメニュー項目6-4</nobr> </li> <li> <nobr>サブメニュー項目6-5</nobr> </li> </ul> </li> <li>メニュー項目7</li> <li>メニュー項目8</li> <li>メニュー項目9</li> </ul> </div> <script type="text/javascript"> initContextMenu('custom-context-menu'); </script> </body> </html>
sample06/resources/views/sample/frame.blade.php
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>iframeページ</title> <style> body { margin: 0; padding: 20px; font-family: sans-serif; background-color: #f9f9f9; } .content { /* iframe内のコンテンツのスタイル */ background-color: #f9f9ff; } </style> </head> <body> <h1>iframeページ内のコンテンツ</h1> <p>ここで右クリックすると、親ページにメニューが表示されます。</p> <script> document.addEventListener('contextmenu', function(ev) { // デフォルトの右クリックメニューを非表示にする ev.preventDefault(); // 親ページにメッセージを送る window.parent.postMessage({ type: 'contextmenu', x: ev.clientX, y: ev.clientY, }, '*'); // '*' は全てのオリジンを許可。本番環境では親ページのオリジンを指定する。 }); // どこかをクリックしたらメニューを非表示にする document.addEventListener('click', function(ev) { window.parent.postMessage({ type: 'click', x: ev.clientX, y: ev.clientY, }, '*'); }); // 親ページからのメッセージを受け取る window.addEventListener('message', function (ev) { console.log(ev.data); }); </script> </body> </html>
sample06/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'); } public function frame() { return view('sample.frame'); } }
今回も、クライアント側だけで片付くので、検証ソースとしてはコントローラーは要らなかったかもしれない。
sample06/public/sample.css
/* 親ページ内の iframe のスタイル */ iframe { width: 100%; height: 400px; border: 1px solid #ccc; } /* 右クリックメニューのスタイル */ .context-menu { position: fixed; display: none; background-color: #fff; border: 1px solid #ccc; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); z-index: 1000; padding: 5px 0; } .context-menu ul { list-style: none; margin: 0; padding: 0; } .context-menu li { padding: 8px 12px; cursor: pointer; } .context-menu li:hover { background-color: #f0f0f0; } .has-submenu .submenu { display: none; position: fixed; background-color: #fff; border: 1px solid #ccc; box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); } /* サブメニューを持つ項目にアイコンを追加(任意) */ .has-submenu > span::after { content: "▶"; /* ▶のような矢印アイコン */ float: right; margin-left: 10px; }
sample06/public/sample.js
function initContextMenu(id) { const menu = document.getElementById(id); const iframe = document.getElementById("my-iframe"); // iframe からのメッセージを受信する window.addEventListener("message", function (ev) { // メッセージの送信元が信頼できるか確認する // production では、ev.origin を検証する console.log(ev.origin); console.log(window.location); if (ev.source === iframe.contentWindow) { const data = ev.data; if (data.type === "contextmenu") { // iframe 内の座標を親ページの座標に変換 const iframeRect = iframe.getBoundingClientRect(); let x = iframeRect.left + data.x; let y = iframeRect.top + data.y; // メニューのサイズを取得 menu.style.display = "block"; const menuWidth = menu.offsetWidth; const menuHeight = menu.offsetHeight; const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; // はみ出しを判定して位置を調整 if (x + menuWidth > windowWidth) { x = windowWidth - menuWidth; } if (y + menuHeight > windowHeight) { y = windowHeight - menuHeight; } // 調整した位置でメニューを表示 menu.style.left = x + "px"; menu.style.top = y + "px"; } else if (data.type === "click") { handleClick(undefined); } } }); function handleClick(target) { if (menu.style.display === "block" && !menu.contains(target)) { menu.style.display = "none"; } else if (menu.contains(target)) { console.log("Menu clicked: " + target.innerText); iframe.contentWindow.postMessage( { action: target.innerText, }, "*" ); } } function handleMouseEnter(ev) { const submenu = this.querySelector(".submenu"); if (submenu) { submenu.style.display = "block"; const thisRect = this.getBoundingClientRect(); let x = thisRect.right; let y = thisRect.top; submenu.style.left = x + "px"; submenu.style.top = y + "px"; const rect = submenu.getBoundingClientRect(); // サブメニューがウィンドウ右側にはみ出す場合 if (rect.right > window.innerWidth) { x = thisRect.left - rect.width; submenu.style.left = x + "px"; } // サブメニューがウィンドウ下側にはみ出す場合 if (rect.bottom > window.innerHeight) { y = thisRect.bottom - rect.height; submenu.style.top = y + "px"; } } } function handleMouseLeave(ev) { const submenu = this.querySelector(".submenu"); if (submenu) { submenu.style.display = "none"; } } // サブメニューのはみ出し調整 document.querySelectorAll(".has-submenu").forEach((item) => { item.addEventListener("mouseenter", handleMouseEnter); item.addEventListener("mouseleave", handleMouseLeave); }); // どこかをクリックしたらメニューを非表示にする document.addEventListener("click", function (ev) { handleClick(ev.target); }); // 親ページのコンテキストメニューも表示しない document.addEventListener("contextmenu", function (ev) { ev.preventDefault(); }); }
sample06/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/frame', [\App\Http\Controllers\SampleController::class, 'frame']);
動作確認
以下のURLにアクセスして動作確認。
http://<your-ip-address>/sample06/public/sample
- iframeの枠を超えて右クリックメニューが表示される
- メニュー、サブメニューをクリックすると、コンソールにメッセージが出る
- ウィンドウの枠外にメニュー、サブメニューが出る場合は位置が調整される
辺りが確認できれば完成。
参考
Geminiにたくさん手伝ってもらっていました。こんなに会話して作り上げたのは初めて。