前言
在 WordPress 網站中,分類清單常用於側邊欄或導覽列,當分類層級較多時,使用者體驗會因為清單過長而降低。這段程式碼的目標是為多層分類清單加入手風琴(Accordion)功能,使得使用者可以展開或收合子分類,提升導覽的清晰度與操作便利性。本文適合有基礎 JavaScript 與 DOM 操作經驗的前端工程師或自學者,想了解如何結合無障礙設計(ARIA)與動態 DOM 操作來優化分類清單。
目標與使用情境
這段程式碼主要解決以下問題:
- 多層分類清單展開收合控制
- 一次只允許展開一個分類(手風琴嚴格模式)
- 兼顧無障礙操作,使用 ARIA 屬性提示狀態
- 保持原有連結功能不受影響
適用於 WordPress 產生的分類清單(如 .wp-block-categories 或 .wp-block-categories-list),並且希望加強使用者操作體驗與無障礙支援。
主要流程解析
1. 選取目標清單容器
const roots = Array.from(document.querySelectorAll('.wp-block-categories, .wp-block-categories-list'));
if (!roots.length) return;
這裡先找出所有符合條件的分類清單容器,若無則直接結束,避免不必要的後續操作。
2. 設定手風琴模式
const singleOpen = true;
設定是否一次只允許展開一個分類項目,true 表示嚴格手風琴模式。
3. 標記並處理有子分類的項目
const parents = Array.from(root.querySelectorAll('li.cat-item > ul.children'))
.map(ul => ul.parentElement)
.filter(li => li && li.matches('.cat-item'));
找出所有有子清單的分類項目(li.cat-item 中包含 ul.children),這些項目需要被加上展開收合功能。
4. 加入切換按鈕與 ARIA 屬性
const toggleBtn = document.createElement('button');
toggleBtn.className = 'cat-toggle';
toggleBtn.type = 'button';
toggleBtn.setAttribute('aria-expanded', 'false');
toggleBtn.setAttribute('aria-controls', subId);
toggleBtn.setAttribute('title', '展開/收合');
為每個有子分類的 li 加入一個按鈕,用於切換展開狀態,同時透過 aria-expanded 與 aria-controls 屬性提升無障礙體驗。
5. 切換展開收合狀態的邏輯
function toggle(open) {
const willOpen = (typeof open === 'boolean') ? open : !li.classList.contains('is-open');
if (willOpen) {
if (singleOpen) {
const siblings = Array.from(li.parentElement.children)
.filter(el => el !== li && el.classList && el.classList.contains('is-open'));
siblings.forEach(sib => {
sib.classList.remove('is-open');
const btn = sib.querySelector(':scope > .cat-toggle');
if (btn) btn.setAttribute('aria-expanded', 'false');
});
}
li.classList.add('is-open');
toggleBtn.setAttribute('aria-expanded', 'true');
} else {
li.classList.remove('is-open');
toggleBtn.setAttribute('aria-expanded', 'false');
}
}
此函式負責切換展開狀態,若啟用嚴格手風琴模式,會同時關閉同層其他已展開的分類。狀態透過 is-open 類別與按鈕的 aria-expanded 屬性同步更新。
6. 綁定事件與點擊行為控制
- 按鈕點擊時切換展開狀態
- 點擊非連結與非按鈕的
li項目空白處也能切換展開,避免影響原本連結導頁
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
toggle();
});
li.addEventListener('click', function(e) {
const isLink = e.target.closest('a');
const isBtn = e.target.closest('.cat-toggle');
if (!isLink && !isBtn) {
e.preventDefault();
toggle();
}
});
7. 預設展開目前分類項目
若分類項目本身或其子分類包含 current-cat 或 current-cat-parent 類別,則預設展開,方便使用者辨識目前所在分類。
if (li.classList.contains('current-cat') ||
li.classList.contains('current-cat-parent') ||
sub.querySelector('.current-cat, .current-cat-parent')) {
toggle(true);
}
實務應用與優化方向
- 可依需求調整
singleOpen為false,允許多個分類同時展開 - 按鈕樣式與動畫可加入 CSS 過渡效果,提升使用者體驗
- 可擴充鍵盤操作支援,增強無障礙友善度
- 若分類清單動態更新,需額外處理事件重新綁定
常見問題與注意事項
- 插入按鈕位置需謹慎,避免破壞原本的連結結構
- 使用
aria-controls需確保對應的子清單有唯一 ID - 點擊事件阻止預設行為,避免影響連結跳轉,需確認邏輯正確
完整程式碼
document.addEventListener('DOMContentLoaded', function() {
// 你貼的清單容器 class
const roots = Array.from(document.querySelectorAll('.wp-block-categories, .wp-block-categories-list'));
if (!roots.length) return;
// 設定:是否一次只開一個(true = 手風琴嚴格模式)
const singleOpen = true;
roots.forEach(function(root) {
// 標記啟用
root.classList.add('cat-accordion');
// 找到有 children 的 cat-item
const parents = Array.from(root.querySelectorAll('li.cat-item > ul.children'))
.map(ul => ul.parentElement)
.filter(li => li && li.matches('.cat-item'));
// 為每個需要的 li 補上 class 與切換按鈕/ARIA
parents.forEach(function(li, idx) {
li.classList.add('has-children');
const sub = li.querySelector(':scope > ul.children');
// 建唯一 id,便於 aria-controls 連結
const subId = sub.id || ('cat-sub-' + Math.random().toString(36).slice(2));
sub.id = subId;
// 建立切換按鈕(放在
<a> 後面,不影響原有點擊導覽)
const toggleBtn = document.createElement('button');
toggleBtn.className = 'cat-toggle';
toggleBtn.type = 'button';
toggleBtn.setAttribute('aria-expanded', 'false');
toggleBtn.setAttribute('aria-controls', subId);
toggleBtn.setAttribute('title', '展開/收合');
// 插到 a 後(若沒有 a,則插到 li 內最前)
const anchor = li.querySelector(':scope > a');
if (anchor && anchor.nextSibling) {
anchor.parentNode.insertBefore(toggleBtn, anchor.nextSibling);
} else if (anchor) {
anchor.parentNode.appendChild(toggleBtn);
} else {
li.insertBefore(toggleBtn, li.firstChild);
}
// 切換邏輯
function toggle(open) {
const willOpen = (typeof open === 'boolean') ? open : !li.classList.contains('is-open');
if (willOpen) {
if (singleOpen) {
// 關閉同層其他
const siblings = Array.from(li.parentElement.children)
.filter(el => el !== li && el.classList && el.classList.contains('is-open'));
siblings.forEach(sib => {
sib.classList.remove('is-open');
const btn = sib.querySelector(':scope > .cat-toggle');
if (btn) btn.setAttribute('aria-expanded', 'false');
});
}
li.classList.add('is-open');
toggleBtn.setAttribute('aria-expanded', 'true');
} else {
li.classList.remove('is-open');
toggleBtn.setAttribute('aria-expanded', 'false');
}
}
// 綁定按鈕點擊
toggleBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
toggle();
});
// 如果想讓點 li 標題也能展開(但避免影響連結導頁):
// 只在點擊「li 空白處」才切換;點到 a 時仍正常導頁
li.addEventListener('click', function(e) {
const isLink = e.target.closest('a');
const isBtn = e.target.closest('.cat-toggle');
if (!isLink && !isBtn) {
e.preventDefault();
toggle();
}
});
// 進階:如果此項或其子層有 "current-cat" / "current-cat-parent" 類別,預設展開
if (li.classList.contains('current-cat') ||
li.classList.contains('current-cat-parent') ||
sub.querySelector('.current-cat, .current-cat-parent')) {
toggle(true);
}
});
});
});