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


iframe内のコンテンツに対する右クリックメニューを表示する

はじめに

ちょっとお仕事で必要が生じて、iframe内のコンテンツに対する右クリックメニューを表示しなければならなくなったので、その検証メモ。

要件を箇条書きにすると、以下のような感じ。

  • iframe内のコンテンツを操作するための右クリックメニューの表示
  • iframeの枠で見切れないようにする
  • サブメニューもある

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

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

下準備

まずは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にたくさん手伝ってもらっていました。こんなに会話して作り上げたのは初めて。

share.google

share.google

share.google

share.google

share.google




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

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