dashboard-search script

URLを検索し飛べるダッシュボード用のスクリプト

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="euc-jp">
    <title>dashboard</title>
    <script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.min.js"></script>
    <meta http-equiv="refresh" content="120"/>
    <!--<style> body { margin: 0; overflow: hidden; } </style>-->
    <style>
        /* ===== 基本スタイル ===== */
        body {
            font-family: sans-serif;
            padding-top: 60px; /* 検索バーのためのスペース */
            margin: 0;
        }
        /* 既存の要素のスタイル例 */
        dt { font-weight: bold; margin-top: 1em; margin-left: 1em; }
        dd { margin-left: 3em; margin-bottom: 0.5em; }
        p, dl, details { margin: 1em; }
        details { border: 1px solid #ccc; padding: 0.5em; border-radius: 8px; }
        summary { font-weight: bold; cursor: pointer; }
        a { color: blue; text-decoration: underline; }

        /* ===== 検索機能のスタイル ===== */
        .search-overlay {
            position: fixed;
            top: 0;
            left: 50%;
            transform: translateX(-50%);
            width: 90%;
            max-width: 512px;
            padding: 1rem;
            z-index: 50;
            background-color: white;
            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
            border: 1px solid #e5e7eb;
            border-top: none;
            border-radius: 0 0 0.5rem 0.5rem;
            box-sizing: border-box;
        }

        .search-input {
            width: 100%;
            padding: 0.5rem;
            border: 1px solid #d1d5db;
            border-radius: 0.375rem;
            box-sizing: border-box;
            font-size: 1rem;
        }

        .search-input:focus {
            outline: none;
            border-color: #3b82f6;
            box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4);
        }

        .search-results {
            margin-top: 0.5rem;
            list-style: none;
            padding: 0;
            margin-left: 0;
            margin-right: 0;
            margin-bottom: 0;
            background-color: white;
            border: 1px solid #e5e7eb;
            border-radius: 0.375rem;
            max-height: calc(100vh - 100px);
            overflow-y: auto;
        }

        .search-result-item {
            padding: 0.5rem;
            border-bottom: 1px solid #f3f4f6;
            cursor: pointer;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .search-result-item:last-child {
            border-bottom: none;
        }

        .search-result-item:hover {
            background-color: #f9fafb;
        }

        /* 選択された項目のスタイル */
        .search-result-item.selected {
            background-color: #e0e7ff;
        }

        /* 見つからない場合のメッセージ */
        .no-results {
            padding: 0.5rem;
            color: #6b7280;
            text-align: center;
        }

        /* 非表示用クラス */
        .hidden {
            display: none;
        }

        /* ===== 検索を開くボタンのスタイル ===== */
        .search-open-button {
            padding: 0.5em 1em;
            margin-bottom: 1em;
            cursor: pointer;
            border: 1px solid #ccc;
            background-color: #f0f0f0;
            border-radius: 4px;
            font-size: 0.9em;
            display: none;
        }

        @media (max-width: 767px) {
            .search-open-button {
                display: inline-block;
            }
        }

        .content-wrapper {
            padding: 1em;
        }
    </style>
  </head>
  <body>
    <div id="searchOverlay" class="search-overlay hidden">
      <input type="text" id="searchInput" placeholder="検索..." class="search-input">
      <ul id="searchResults" class="search-results">
          </ul>
    </div>
    <div>
      <button id="openSearchButton" class="search-open-button">検索を開く</button>
    </div>


    <a href="hoge">
        <img src="./logos/hoge.png">
        hoge
    </a>
    <dt>hoge</dt>
    <dd>hoge<dd>
    <details>
        <summary>hoge</summary>
        <a href="fuga">fuga</a>
    </details>
  </body>
  <script>
    // --- Settings ---
    const searchData = [
    // { url: "", searchwords: "", displayText: "" },
    { url: "./hoge", searchwords: "fuga", displayText: "piyo" },
];

const aliases = {
    "hg": { url: "./hoge" }
};

    const fuseOptions = {
        keys: [
            "searchwords",
            "url"
        ],
        threshold: 0.4,
    };
    // --- End of Settings ---

    const fuse = new Fuse(searchData, fuseOptions);

    // --- Element References ---
    const searchOverlay = document.getElementById('searchOverlay');
    const searchInput = document.getElementById('searchInput');
    const searchResults = document.getElementById('searchResults');
    const openSearchButton = document.getElementById('openSearchButton');

    // --- State Variables ---
    let selectedIndex = -1;
    let dKeyPressed = false;

    // --- Functions ---
    function showSearch() {
        searchOverlay.classList.remove('hidden');
        searchInput.focus();
        searchInput.value = '';
        clearResults();
    }

    function hideSearch() {
        searchOverlay.classList.add('hidden');
        searchInput.blur();
        clearResults();
        selectedIndex = -1;
        dKeyPressed = false;
    }

    function clearResults() {
        searchResults.innerHTML = '';
    }

    function updateResults() {
        const query = searchInput.value.trim();
        clearResults();
        // Reset selectedIndex *before* populating new results
        selectedIndex = -1;

        if (!query) {
            return;
        }

        let results = [];
        let aliasResult = null;

        // Alias check
        if (aliases[query]) {
            aliasResult = aliases[query];
            // Find the corresponding item in searchData to ensure consistency
            const aliasInSearchData = searchData.find(item => item.url === aliasResult.url);
              // Use the full object from searchData if found, otherwise use the alias object directly
            results.push({ item: aliasInSearchData || aliasResult });
        }

        // Fuse search
        const fuseResults = fuse.search(query);

        // Merge alias and Fuse results
        fuseResults.forEach(fuseResult => {
            if (!aliasResult || fuseResult.item.url !== aliasResult.url) {
                results.push(fuseResult);
            }
        });


        if (results.length > 0) {
            results.forEach((result, index) => {
                const li = document.createElement('li');
                li.classList.add('search-result-item');
                // Display format: displayText (url)
                li.textContent = `${result.item.displayText || ''} (${result.item.url})`;
                // Set title attribute (optional, keeping it simpler)
                li.title = result.item.displayText || result.item.url;
                li.dataset.url = result.item.url;
                li.dataset.index = index;

                // Event listeners for click and mouseover
                li.addEventListener('click', () => {
                    window.location.href = result.item.url;
                });
                li.addEventListener('mouseover', () => {
                    deselectAll();
                    li.classList.add('selected');
                    selectedIndex = index;
                });

                searchResults.appendChild(li);
            });

            // Select the first item by default
            selectedIndex = 0;
            highlightResult(selectedIndex);

        } else {
            // No results found message
            const li = document.createElement('li');
            li.classList.add('no-results');
            li.textContent = '結果が見つかりません';
            searchResults.appendChild(li);
        }
    }

    // deselectAll, highlightResult, navigateResults, openSelected functions remain the same
    function deselectAll() {
        const items = searchResults.querySelectorAll('.search-result-item');
        items.forEach(item => item.classList.remove('selected'));
    }

    function highlightResult(index) {
        deselectAll();
        const items = searchResults.querySelectorAll('.search-result-item');
        if (items[index]) {
            items[index].classList.add('selected');
            // Scroll into view if needed
            items[index].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
        }
    }

    function navigateResults(direction) {
        const items = searchResults.querySelectorAll('.search-result-item');
        const itemCount = items.length;

        if (itemCount === 0) return;

        let newIndex = selectedIndex;

        if (direction === 'down') {
            // If nothing is selected, start from the first item
            newIndex = selectedIndex === -1 ? 0 : (selectedIndex + 1) % itemCount;
        } else if (direction === 'up') {
             // If nothing is selected, start from the last item
            newIndex = selectedIndex === -1 ? itemCount - 1 : (selectedIndex - 1 + itemCount) % itemCount;
        }

        selectedIndex = newIndex;
        highlightResult(selectedIndex);
    }

    function openSelected() {
        const selectedItem = searchResults.querySelector('.search-result-item.selected');
        if (selectedItem && selectedItem.dataset.url) {
            window.location.href = selectedItem.dataset.url;
        }
    }

    // --- Event Listeners ---

    // *** Add event listener for the open search button ***
    if (openSearchButton) { // Check if the button exists
      openSearchButton.addEventListener('click', showSearch);
    }

    // Keyboard event listener (remains the same)
    document.addEventListener('keydown', (e) => {
        // When search overlay is hidden
        if (searchOverlay.classList.contains('hidden')) {
            // Exclude modifier keys and special keys
            // Start search on character input or spacebar
            if (!e.ctrlKey && !e.altKey && !e.metaKey && e.key.length === 1) {
                 // Further exclude function keys etc. if needed
                if (!e.key.startsWith('F') && e.key !== 'Escape' && e.key !== 'Tab' && e.key !== 'Shift' && e.key !== 'Control' && e.key !== 'Alt' && e.key !== 'Meta' && e.key !== 'CapsLock' && e.key !== 'ArrowUp' && e.key !== 'ArrowDown' && e.key !== 'ArrowLeft' && e.key !== 'ArrowRight' && e.key !== 'Enter' && e.key !== 'Backspace' && e.key !== 'Delete' && e.key !== 'Home' && e.key !== 'End' && e.key !== 'PageUp' && e.key !== 'PageDown') {
                    showSearch();
                }
            }
        }
        // When search overlay is visible
        else {
            if (e.key === 'Escape') {
                hideSearch();
            } else if (e.key === 'ArrowDown') {
                e.preventDefault(); // Prevent page scroll
                navigateResults('down');
            } else if (e.key === 'ArrowUp') {
                e.preventDefault(); // Prevent page scroll
                navigateResults('up');
            } else if (e.key === 'Enter') {
                e.preventDefault(); // Prevent form submission etc.
                openSelected();
            } else if (e.key.toLowerCase() === 'd') {
                dKeyPressed = true; // 'd' key pressed
            } else if (e.key.toLowerCase() === 'u' && dKeyPressed) {
                e.preventDefault(); // Prevent default browser action (e.g., view source)
                hideSearch();
                dKeyPressed = false; // Reset
            }
        }
    });

    // Reset flag when 'd' key is released
    document.addEventListener('keyup', (e) => {
        if (e.key.toLowerCase() === 'd') {
            dKeyPressed = false;
        }
    });

    // Search input event
    searchInput.addEventListener('input', updateResults);
  </script>
</html>

ライセンス

MITです。AIが全部書いたし。一応。

Copyright (c) 2025 15km Released under the MIT license https://opensource.org/licenses/mit-license.php