Open2ch ID Search Button

おーぷん2chでIDの左側と本文を選択した右上に検索ボタン(虫眼鏡アイコン)を表示します。

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name        Open2ch ID Search Button
// @namespace   https://greasyfork.org/ja/users/864059
// @version     1.2.0
// @description おーぷん2chでIDの左側と本文を選択した右上に検索ボタン(虫眼鏡アイコン)を表示します。
// @author      七色の彩り
// @match       https://*.open2ch.net/test/read.cgi/*
// @icon          https://open2ch.net/favicon.ico
// @grant       none
// @run-at      document-idle
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // URLからbbs名を取得する関数
    const getBbsName = () => {
        const path = window.location.pathname;
        const match = path.match(/\/read\.cgi\/([a-zA-Z0-9_-]+)\//);
        return match ? match[1] : '';
    };

    const bbsName = getBbsName();

    const $searchUrl = 'https://find.open2ch.net/?bbs=&t=f&q='; // ID: は searchQuery で付与する

    /**
     * 指定されたIDスパンに検索ボタンを追加する
     * @param {HTMLElement} idSpan - <span class="_id"> 要素
     * @param {string} searchUrlTemplate - 検索URLの雛形
     */
    const processIdSpan = (idSpan, searchUrlTemplate) => {

        // 1. ID情報の取得
        const idText = idSpan.getAttribute('val');
        if (!idText || idText === '???') return;

        // 2. ID整形ロジック (余計なピリオド問題を解決したもの)
        let fullId = '';
        const idLinks = idSpan.querySelectorAll('.id');

        if (idLinks.length > 0) {
            fullId = Array.from(idLinks).map(link => link.textContent).join('.');
        } else if (idText.length > 4) {
            fullId = idText.slice(0, 2) + '.' + idText.slice(2, 4) + '.' + idText.slice(4);
        } else {
            fullId = idText;
        }

        const searchQuery = 'ID:' + fullId;
        const encodedQuery = encodeURIComponent(searchQuery);
        const finalUrl = searchUrlTemplate + encodedQuery + '&wh=&d=';

        // 3. 重複チェックと既存ボタンの再利用
        const existingButton = idSpan.previousElementSibling;

        // 既に虫眼鏡アイコンを持つSPAN要素が存在する場合
        if (existingButton && existingButton.tagName === 'SPAN' && existingButton.querySelector('.fas.fa-search')) {
            // 既存のボタンに正しい機能を持つクリックイベントを追加/上書きする
            existingButton.addEventListener('click', function() {
                window.open(finalUrl, '_blank', 'noopener noreferrer');
            });
            return; // 既存要素を再利用したため、ここで終了
        }

        // 4. 新しいボタンの作成・挿入 (通常時の動作)
        const searchButton = document.createElement('span');
        searchButton.title = 'ID:' + fullId + 'を検索';
        searchButton.style.cssText = 'margin-left: 5px; cursor: pointer;';

        const icon = document.createElement('i');
        icon.className = 'fas fa-search';
        icon.style.cssText = 'color: #333;';

        searchButton.appendChild(icon);

        searchButton.addEventListener('click', function() {
            window.open(finalUrl, '_blank', 'noopener noreferrer');
        });

        // idSpanの直前に挿入 (時刻とID:の間)
        idSpan.insertAdjacentElement('beforebegin', searchButton);
    };

    // スタイルの追加(丸いボタンのデザイン)
    const style = document.createElement('style');
    style.textContent = `
        #floating-search-btn {
            position: absolute;
            z-index: 10001;
            background: #fff;
            border: 1px solid #ccc;
            box-shadow: 0 2px 5px rgba(0,0,0,0.3);
            width: 30px;
            height: 30px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            display: none;
            transition: transform 0.1s;
        }
        #floating-search-btn:hover {
            transform: scale(1.1);
            background-color: #f0f0f0;
        }
        #floating-search-btn i {
            color: #333;
            font-size: 14px;
        }
    `;
    document.head.appendChild(style);

    // ボタン要素を作成
    const floatBtn = document.createElement('div');
    floatBtn.id = 'floating-search-btn';
    floatBtn.title = 'おーぷん全体検索';
    floatBtn.innerHTML = '<i class="fas fa-search"></i>';
    document.body.appendChild(floatBtn);

    // テキスト選択時のイベント
    document.addEventListener('mouseup', (e) => {
        // 少し遅らせて選択範囲を取得(クリック解除時のタイミング調整)
        setTimeout(() => {
            const selection = window.getSelection();
            const selectedText = selection.toString().trim();

            if (selectedText) {
                // ボタンの表示位置をマウス位置の少し右上に設定
                floatBtn.style.left = `${e.pageX + 10}px`;
                floatBtn.style.top = `${e.pageY - 40}px`;
                floatBtn.style.display = 'flex';

                // 検索実行イベント
                floatBtn.onclick = () => {
                    const url = $searchUrl + encodeURIComponent(selectedText) + '&wh=&d=';
                    window.open(url, '_blank', 'noopener noreferrer');
                    floatBtn.style.display = 'none';
                    selection.removeAllRanges(); // 選択解除
                };
            } else {
                // 何もないところをクリックしたらボタンを隠す
                if (e.target.closest('#floating-search-btn')) return;
                floatBtn.style.display = 'none';
            }
        }, 10);
    });

    // スクロール時にも隠す
    document.addEventListener('scroll', () => {
        floatBtn.style.display = 'none';
    }, { passive: true });

    // 初期実行: 既存の要素に対してボタンを追加
    // Mutation Observerで動的に追加される要素を監視
    // 監視対象を最も確実なコンテナである .thread に
    const threadContainer = document.querySelector('.thread'); // <div class="thread"> または <dl class="thread"> の両方に対応

    if (!threadContainer) {
        // コンテナが見つからなければ、処理を終了する
        //console.log('Open2ch ID Search Button: スレッドコンテナ (.thread) が見つかりません。終了します。');
        return;
    }
    // スレッドコンテナ内のみを検索対象とする
    const idSpansInitial = threadContainer.querySelectorAll('._id');
    idSpansInitial.forEach(idSpan => {
        processIdSpan(idSpan, $searchUrl);
    });

    // Mutation Observerで動的に追加される要素を監視
    // threadContainer が取得できた前提で、そのまま observer を設定
    setupObserver(threadContainer, $searchUrl);

    function setupObserver(targetNode, searchUrlTemplate) {
        const observer = new MutationObserver(mutationsList => {
            mutationsList.forEach(mutation => {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        // 要素ノードでない場合はスキップ
                        if (node.nodeType !== 1) return;

                        // 追加された要素(またはその子孫)から '_id' スパンを検索
                        // 新しい書き込み(<dl>)が追加された場合、その中の<dt>の._idを見つける
                        const idSpans = node.querySelectorAll('._id');
                        idSpans.forEach(idSpan => {
                            processIdSpan(idSpan, searchUrlTemplate);
                        });
                        // 追加されたノード自体が '_id' スパンの場合
                        if (node.classList && node.classList.contains('_id')) {
                            processIdSpan(node, searchUrlTemplate);
                        }
                    });
                }
            });
        });
        // スレッドコンテナとその子要素の変更を監視開始
        observer.observe(targetNode, { childList: true, subtree: true });
    }

})();