Greasy Fork

Greasy Fork is available in English.

AnkiWeb_js

メニュー画面に階層的な折りたたみを実装

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            AnkiWeb_js
// @namespace  http://tampermonkey.net/
// @version         1.0.1
// @author          zom.u
// @description  メニュー画面に階層的な折りたたみを実装
// @match           https://ankiweb.net/decks
// @grant            none
// ==/UserScript==
(function() {
    'use strict';

    let isInitialized = false; // 二重初期化を防ぐフラグ

    // スタイルを追加
    const style = document.createElement('style');
    style.textContent = `
        .collapse-toggle {
            display: inline-block;
            width: 24px;
            cursor: pointer;
            user-select: none;
            margin-left: -28px;
            margin-right: 4px;
            text-align: center;
            padding: 2px 4px;
        }
        .collapse-toggle.no-children {
            cursor: default;
            opacity: 0.3;
        }
        .collapse-toggle:not(.no-children):hover {
            background-color: rgba(0, 0, 0, 0.1);
            border-radius: 3px;
        }
        .collapsed-item {
            display: none !important;
        }
        .has-children {
            font-weight: 500;
        }
    `;
    document.head.appendChild(style);

    // メイン処理
    function initializeCollapsible() {
        // 既に初期化済みの場合はスキップ(トグルボタンの存在で判定)
        const existingToggle = document.querySelector('.collapse-toggle');
        if (existingToggle) {
            return;
        }

        const rows = document.querySelectorAll('div.row.light-bottom-border.svelte-p9sq8d');
        if (rows.length === 0) {
            return;
        }

        console.log(`折りたたみ機能を初期化中... (${rows.length}個の項目を検出)`);

        // 各行のレベルを計算
        const items = Array.from(rows).map((row, index) => {
            const button = row.querySelector('button');
            if (!button) return null;

            // 既に処理済みの場合はスキップ
            if (button.querySelector('.collapse-toggle')) {
                return null;
            }

            //  の数を数える
            const textContent = button.textContent;
            let nbspCount = 0;
            for (let i = 0; i < textContent.length; i++) {
                if (textContent[i] === '\u00A0') {
                    nbspCount++;
                }
            }

            // レベルを計算(3つが最上位レベル1、6つがレベル2...)
            const level = Math.floor(nbspCount / 3);

            return {
                element: row,
                button: button,
                level: level,
                index: index,
                isExpanded: false, // 初期状態を折りたたみ
                hasChildren: false,
                originalText: button.textContent // 元のテキストを保存
            };
        }).filter(item => item !== null);

        if (items.length === 0) {
            return;
        }

        // 子要素の有無を判定
        for (let i = 0; i < items.length; i++) {
            if (i < items.length - 1 && items[i + 1].level > items[i].level) {
                items[i].hasChildren = true;
            }
        }

        // トグルボタンを追加(全ての項目に)
        items.forEach((item, index) => {
            // ボタンの内容を再構築
            item.button.textContent = '';

            // nbsp部分を追加(トグル分のスペースを追加)
            const nbspSpan = document.createElement('span');
            let nbspText = '';
            for (let i = 0; i < item.level * 3; i++) {
                nbspText += '\u00A0';
            }
            // トグルボタン分のスペースを追加
            nbspText += '\u00A0\u00A0\u00A0\u00A0'; // 少し増やして調整
            nbspSpan.textContent = nbspText;
            item.button.appendChild(nbspSpan);

            // トグル要素を作成(全ての項目に追加)
            const toggle = document.createElement('span');
            toggle.className = 'collapse-toggle';

            if (item.hasChildren) {
                toggle.textContent = '▶'; // 初期状態は折りたたみ

                // クリックイベントを追加
                toggle.addEventListener('click', (e) => {
                    e.stopPropagation();
                    toggleChildren(items, index);
                });

                // 親要素にクラスを追加
                item.element.classList.add('has-children');
            } else {
                // 子要素がない場合は薄い表示
                toggle.textContent = '▶';
                toggle.classList.add('no-children');
            }

            item.button.appendChild(toggle);

            // 元のテキスト(nbspを除く)を追加
            const textSpan = document.createElement('span');
            textSpan.textContent = item.originalText.trim();
            item.button.appendChild(textSpan);
        });

        // 初期状態:レベル1(最上位)以外を非表示
        items.forEach(item => {
            if (item.level > 1) {  // レベル2以降を非表示
                item.element.classList.add('collapsed-item');
            }
        });

        isInitialized = true;
        console.log('折りたたみ機能の初期化完了');
    }

    // 子要素の表示/非表示を切り替え
    function toggleChildren(items, parentIndex) {
        const parent = items[parentIndex];
        parent.isExpanded = !parent.isExpanded;

        // トグルアイコンを更新
        const toggle = parent.button.querySelector('.collapse-toggle');
        if (toggle) {
            toggle.textContent = parent.isExpanded ? '▼' : '▶';
        }

        // 子要素を表示/非表示
        for (let i = parentIndex + 1; i < items.length; i++) {
            if (items[i].level <= parent.level) {
                // 同じレベルか上位レベルに達したら終了
                break;
            }

            if (items[i].level === parent.level + 1) {
                // 直接の子要素
                if (parent.isExpanded) {
                    items[i].element.classList.remove('collapsed-item');
                    // 子要素が折りたたまれていた場合、その配下も適切に処理
                    if (items[i].hasChildren && !items[i].isExpanded) {
                        // 子要素のトグルアイコンも更新
                        const childToggle = items[i].button.querySelector('.collapse-toggle');
                        if (childToggle && !childToggle.classList.contains('no-children')) {
                            childToggle.textContent = '▶';
                        }
                        collapseAllChildren(items, i);
                    }
                } else {
                    items[i].element.classList.add('collapsed-item');
                    // 子要素も折りたたみ状態にリセット
                    items[i].isExpanded = false;
                    const childToggle = items[i].button.querySelector('.collapse-toggle');
                    if (childToggle && !childToggle.classList.contains('no-children')) {
                        childToggle.textContent = '▶';
                    }
                }
            } else if (items[i].level > parent.level + 1) {
                // 孫要素以降
                if (!parent.isExpanded) {
                    items[i].element.classList.add('collapsed-item');
                }
            }
        }
    }

    // 指定した要素の全ての子要素を折りたたむ
    function collapseAllChildren(items, parentIndex) {
        const parent = items[parentIndex];
        for (let i = parentIndex + 1; i < items.length; i++) {
            if (items[i].level <= parent.level) {
                break;
            }
            items[i].element.classList.add('collapsed-item');
        }
    }

    // 初期化を試みる関数
    function tryInitialize() {
        const rows = document.querySelectorAll('div.row.light-bottom-border.svelte-p9sq8d');
        if (rows.length > 0 && !isInitialized) {
            initializeCollapsible();
        }
    }

    // 複数のタイミングで初期化を試みる
    // 1. DOMContentLoaded
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', tryInitialize);
    } else {
        tryInitialize();
    }

    // 2. window.onload
    window.addEventListener('load', tryInitialize);

    // 3. 遅延実行(SPAの場合に有効)
    setTimeout(tryInitialize, 500);
    setTimeout(tryInitialize, 1000);
    setTimeout(tryInitialize, 2000);

    // 4. MutationObserverで動的な要素追加を監視
    const observer = new MutationObserver((mutations) => {
        // 新しい要素が追加されたかチェック
        const hasNewRows = mutations.some(mutation => {
            return Array.from(mutation.addedNodes).some(node => {
                return node.nodeType === 1 &&
                       (node.matches && (
                           node.matches('div.row.light-bottom-border.svelte-p9sq8d') ||
                           node.querySelector && node.querySelector('div.row.light-bottom-border.svelte-p9sq8d')
                       ));
            });
        });

        if (hasNewRows) {
            isInitialized = false; // リセットして再初期化を許可
            setTimeout(tryInitialize, 100);
        }
    });

    // 監視を開始
    if (document.body) {
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    } else {
        // bodyがまだない場合は、documentを監視
        const tempObserver = new MutationObserver(() => {
            if (document.body) {
                tempObserver.disconnect();
                observer.observe(document.body, {
                    childList: true,
                    subtree: true
                });
                tryInitialize();
            }
        });
        tempObserver.observe(document.documentElement, {
            childList: true,
            subtree: true
        });
    }

})();