BetterTopMenu

mec-itutorのメニュー画面を修正

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name           BetterTopMenu
// @author         Nozom.u
// @match          *://mec-itutor.jp/rpv/home/default.aspx
// @match          *://mec-itutor.jp/rpv/home/
// @version        2.0.7
// @run-at      window-load
// @description  mec-itutorのメニュー画面を修正
// @namespace https://greasyfork.org/users/1534273
// ==/UserScript==
(function() {
    'use strict';
    function addStyles() {
        const styleSheet = document.createElement('style');
        styleSheet.textContent = `
            /* ===== 単元タイトル部分の余白 ===== */
            .mec-unit-title-wrap,
            .mec-unit-title-wrap-small {
              padding: 8px 0 !important;
            }
            #ctl00_cplPageContent_infomationPanel > div.list-group-item,
            #scheduledDisplayListView > div.list-group-item,
            #scheduledRedisplayListView > div.list-group-item {
              padding: 0px 15px;
            }
            /* ===== デッキ・期限リンクの装飾 ===== */
            .list-group > .list-group-item > .mec-unit-title-wrap ul li.mec-list-title > a,
            .list-group > .list-group-item > .mec-unit-title-wrap ul li.mec-list-date > a {
              text-decoration: none; /* 通常はアンダーラインなし */
              line-height: 40px;
            }
            /* hover時にアンダーライン表示 */
            .list-group > .list-group-item:hover > .mec-unit-title-wrap ul li.mec-list-title > a,
            .list-group > .list-group-item:hover > .mec-unit-title-wrap ul li.mec-list-date > a {
              text-decoration: underline;
              display: block;
              line-height: 40px;
            }
            /* ヘッダーのスタイル */
            .mec-header-title.collapsible-header {
                cursor: pointer !important;
                user-select: none !important;
                transition: background-color 0.2s ease !important;
            }
            /* 矢印アイコン */
            .collapse-arrow {
                display: inline-block;
                width: 0;
                height: 0;
                border-left: 8px solid transparent;
                border-right: 8px solid transparent;
                border-top: 10px solid white;
                transition: transform 0.3s ease;
                margin-right: 8px;
                vertical-align: middle;
                position: relative;
                top: -2px;
            }
            .collapse-arrow.collapsed {
                transform: rotate(-90deg);
            }

            /* アイテムのアニメーション */
            .list-group-item.collapsible-item {
                overflow: hidden !important;
                transition: all 0.3s ease !important;
                transform-origin: top !important;
            }
            .list-group-item.collapsible-item.collapsed {
                max-height: 0 !important;
                opacity: 0 !important;
                padding-top: 0 !important;
                padding-bottom: 0 !important;
                margin-top: 0 !important;
                margin-bottom: 0 !important;
                border: none !important;
            }
        `;
        document.head.appendChild(styleSheet);
    }

    // 初期化関数
    function initializeCollapsibleSections() {
        const sections = [
            {
                container: document.getElementById('ctl00_cplPageContent_infomationPanel'),
                name: 'お知らせ',
                storageKey: 'info'
            },
            {
                container: document.getElementById('scheduledRedisplayListView'),
                parentContainer: document.getElementById('ctl00_cplPageContent_scheduledRedisplayPanel'),
                name: '提出期限切れ',
                storageKey: 'expired'
            },
            {
                container: document.getElementById('scheduledDisplayListView'),
                parentContainer: document.getElementById('ctl00_cplPageContent_scheduledPanel'),
                name: '学習予定',
                storageKey: 'scheduled'
            }
        ];

        sections.forEach(section => {
            if (!section.container && !section.parentContainer) {
                return;
            }

            const targetContainer = section.container || section.parentContainer;
            const header = targetContainer.querySelector('.info-item-heading');

            if (!header) {
                return;
            }

            // ヘッダーに折りたたみ機能を追加
            setupCollapsibleSection(targetContainer, header, section.name, section.storageKey);
        });
    }

    // 個別セクションのセットアップ
    function setupCollapsibleSection(container, header, sectionName, storageKey) {
        // すでに設定済みの場合はスキップ
        if (header.classList.contains('collapsible-header')) {
            return;
        }

        // ヘッダーにクラスを追加とデータ属性を設定
        header.classList.add('collapsible-header');
        header.setAttribute('data-storage-key', storageKey);

        // h4.info-item-headingを探す
        const h4Element = header.querySelector('h4.info-item-heading');

        if (h4Element) {
            // 矢印アイコンを作成
            const arrow = document.createElement('span');
            arrow.className = 'collapse-arrow';

            // h4の最初に矢印を挿入(img.mec-header-iconの前)
            h4Element.insertBefore(arrow, h4Element.firstChild);
        } else {
            // h4が見つからない場合は、ヘッダーの最初に追加
            const arrow = document.createElement('span');
            arrow.className = 'collapse-arrow';
            header.insertBefore(arrow, header.firstChild);
        }

        // アイテムを取得(ヘッダー以外のlist-group-item)
        const items = Array.from(container.querySelectorAll('.list-group-item'))
            .filter(item => !item.classList.contains('mec-header-title'));

        // アイテムにクラスを追加
        items.forEach(item => {
            item.classList.add('collapsible-item');
        });

        // 状態を管理
        let isCollapsed = loadSectionState(storageKey);

        // 矢印要素を取得
        const arrow = header.querySelector('.collapse-arrow');

        // 初期状態を設定
        if (isCollapsed && arrow) {
            arrow.classList.add('collapsed');
            items.forEach(item => item.classList.add('collapsed'));
        }

        // クリックイベントを追加
        header.addEventListener('click', function(e) {
            e.preventDefault();
            e.stopPropagation();

            isCollapsed = !isCollapsed;

            // アニメーション
            if (isCollapsed) {
                arrow?.classList.add('collapsed');
                items.forEach(item => item.classList.add('collapsed'));
            } else {
                arrow?.classList.remove('collapsed');
                items.forEach(item => item.classList.remove('collapsed'));
            }

            // 状態を保存
            saveSectionState(storageKey, isCollapsed);
        });
    }

    // 状態を保存
    function saveSectionState(storageKey, isCollapsed) {
        try {
            const key = `collapsible_section_${storageKey}`;
            localStorage.setItem(key, JSON.stringify(isCollapsed));
        } catch (e) {
            // ローカルストレージエラーを無視
        }
    }

    // 状態を読み込み
    function loadSectionState(storageKey) {
        try {
            const key = `collapsible_section_${storageKey}`;
            const saved = localStorage.getItem(key);
            return saved ? JSON.parse(saved) : false; // デフォルトは展開状態
        } catch (e) {
            // ローカルストレージエラーを無視
            return false;
        }
    }

    // キーボードショートカットのセットアップ(サイトに注入)
    function setupKeyboardShortcuts() {
        // サイトのグローバルスコープで実行するスクリプトを作成
        const script = document.createElement('script');
        script.type = 'text/javascript';
        script.textContent = `
        (function() {
            // セクションマップ
            const sectionMap = {
                '1': { storageKey: 'info', sectionName: 'お知らせ' },
                '2': { storageKey: 'expired', sectionName: '提出期限切れ' },
                '3': { storageKey: 'scheduled', sectionName: '学習予定' }
            };

            // キーボードイベントをリッスン
            document.addEventListener('keydown', function(e) {
                // 数字キー(1, 2, 3)を検出
                if (e.key in sectionMap) {
                    const { storageKey, sectionName } = sectionMap[e.key];
                    toggleSectionByKey(storageKey, sectionName);
                }
            });

            // セクションを切り替える関数
            function toggleSectionByKey(storageKey, sectionName) {
                const header = document.querySelector(\`.collapsible-header[data-storage-key="\${storageKey}"]\`);

                if (header) {
                    header.click();
                }
            }
        })();
        `;

        // スクリプトをDOCUMENT_START時点で実行するために、headに追加
        if (document.head) {
            document.head.appendChild(script);
        } else {
            document.documentElement.appendChild(script);
        }
    }

    // 初期化を実行
    function initialize() {
        // スタイルを追加
        addStyles();
        // キーボードショートカットをセットアップ
        setupKeyboardShortcuts();
        // DOMが完全に読み込まれるまで待つ
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initializeCollapsibleSections);
        } else {
            // 既に読み込まれている場合は直ちに実行
            initializeCollapsibleSections();
        }
    }

    // MutationObserverで動的なコンテンツ変更を監視
    function setupObserver() {
        let debounceTimer = null;
        const observer = new MutationObserver(function(mutations) {
            let shouldReinitialize = false;
            mutations.forEach(function(mutation) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    // 新しいノードが追加されたかチェック
                    mutation.addedNodes.forEach(function(node) {
                        if (node.nodeType === 1 && node.classList &&
                            (node.classList.contains('list-group') ||
                             node.classList.contains('list-group-item'))) {
                            shouldReinitialize = true;
                        }
                    });
                }
            });

            if (shouldReinitialize) {
                // デバウンス処理:前のタイマーをクリア
                clearTimeout(debounceTimer);
                debounceTimer = setTimeout(initializeCollapsibleSections, 100);
            }
        });

        // main-gadget-areaの監視を開始
        const mainArea = document.getElementById('main-gadget-area');
        if (mainArea) {
            observer.observe(mainArea, {
                childList: true,
                subtree: true
            });
        }
    }

    // グローバル関数として公開(デバッグ用)
    window.collapsibleSections = {
        // すべてのセクションを折りたたむ
        collapseAll: function() {
            const headers = document.querySelectorAll('.mec-header-title.collapsible-header');
            headers.forEach(header => {
                const arrow = header.querySelector('.collapse-arrow');
                const items = header.parentElement.querySelectorAll('.list-group-item.collapsible-item');

                arrow?.classList.add('collapsed');
                items.forEach(item => item.classList.add('collapsed'));
            });
        },

        // すべてのセクションを展開
        expandAll: function() {
            const headers = document.querySelectorAll('.mec-header-title.collapsible-header');
            headers.forEach(header => {
                const arrow = header.querySelector('.collapse-arrow');
                const items = header.parentElement.querySelectorAll('.list-group-item.collapsible-item');

                arrow?.classList.remove('collapsed');
                items.forEach(item => item.classList.remove('collapsed'));
            });
        },

        // 状態をリセット
        resetStates: function() {
            ['info', 'expired', 'scheduled'].forEach(key => {
                localStorage.removeItem(`collapsible_section_${key}`);
            });
            location.reload();
        },

        // 再初期化
        reinitialize: function() {
            initializeCollapsibleSections();
        },

        // 現在の状態を確認
        getStates: function() {
            const states = {};
            ['info', 'expired', 'scheduled'].forEach(key => {
                const saved = localStorage.getItem(`collapsible_section_${key}`);
                states[key] = saved ? JSON.parse(saved) : false;
            });
            return states;
        }
    };

    // エラーハンドリング
    try {
        // 初期化実行
        initialize();

        // オブザーバーをセットアップ
        setupObserver();
    } catch (error) {
        // エラーを無視
    }
})();