Greasy Fork is available in English.
メニュー画面に階層的な折りたたみを実装
// ==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
});
}
})();