为使用 timeline 标签功能页面增加筛选功能(安知鱼版)
开发思路
这今天利用安知鱼主题的 timeline 标签来整理大叔作品年表。时间跨度从 1951 年至今,条目众多,想着要不搞个筛选器,分门别类显示,应该还不错。老规矩,说干就干。
流程分析:
- 建立筛选器:在对应页面插入
<button>,通过添加 active 类显示激活
- 建立按钮关联:建立
bindFilterButtons()
- 筛选条件:在每条内容里添加个图标作为筛选条件
getCategoryFromIcon(icon),返回筛选类别
- 进行筛选:
filterTimeline(category) 给对应的 category 打上 hidden 类
- pjax 处理
不算复杂,快速搞定。等下,做都做了,给站点更新记录也加上这个功能吧,但是更新记录用图标不太得劲,改为文字标题吧,加一个 getCategoryFromText(text)。再等下,顺便做个计数器吧,给各个分类做个统计 updateFilterCounts()。
行了,测试通过。
但是,漫画类还是太长了,在底部同步加个筛选器吧。额……为啥头尾的筛选器不同步?得,接着改,来个 syncActiveButtons()。
最终成果
1 2 3 4 5
| inject: head: - <link rel="stylesheet" href="/css/timeline-filter.css"> bottom: - <script src="/js/timeline-filter.js"></script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
| (function() { function getCategoryFromIcon(spanElem) { const classNames = spanElem.className.split(/\s+/); for (let cls of classNames) { if (cls.startsWith('icon-')) { return cls.replace(/^icon-/, '').toLowerCase(); } } return ''; }
function getCategoryFromText(textElem) { let text = textElem.innerText.trim(); text = text.replace(/[::、\s]+$/, ''); return text.toLowerCase(); }
function findItemCategories(item) { const categories = new Set();
const textElems = item.querySelectorAll('.timeline-item-content strong'); textElems.forEach(elem => { const cat = getCategoryFromText(elem); if (cat) categories.add(cat); });
if (categories.size === 0) { const spans = item.querySelectorAll('.timeline-item-content span'); for (let span of spans) { const cat = getCategoryFromIcon(span); if (cat) categories.add(cat); } } return Array.from(categories); }
function filterTimeline(category) { let effectiveCategory = category; if (category !== 'all') { if (category.startsWith('icon ') || category.startsWith('text ')) { effectiveCategory = category.substring(5); } else { effectiveCategory = category; } } effectiveCategory = effectiveCategory.trim().toLowerCase();
const containers = document.querySelectorAll('.timeline'); containers.forEach(container => { container.style.display = '';
let hasMatch = false; const items = container.querySelectorAll('.timeline-item'); items.forEach(item => { if (item.classList.contains('headline')) return;
const itemCategories = findItemCategories(item); const hasIdentifier = itemCategories.length > 0; if (!hasIdentifier) { item.style.display = (category === 'all') ? '' : 'none'; return; }
const matches = (category === 'all') || itemCategories.includes(effectiveCategory); if (matches) { hasMatch = true; item.style.display = ''; } else { item.style.display = 'none'; } });
const headline = container.querySelector('.timeline-item.headline'); if (headline) { headline.style.display = (category === 'all' || hasMatch) ? '' : 'none'; } if (category !== 'all' && !hasMatch) { container.style.display = 'none'; } });
const allContainers = document.querySelectorAll('.timeline'); let firstVisible = null; for (let i = 0; i < allContainers.length; i++) { if (allContainers[i].style.display !== 'none') { firstVisible = allContainers[i]; break; } }
allContainers.forEach(container => { container.style.marginTop = ''; }); if (firstVisible && document.querySelector('.category-filter')) { firstVisible.style.marginTop = '0'; } }
function bindFilterButtons() { const btns = document.querySelectorAll('.filter-btn'); if (btns.length === 0) return;
btns.forEach(btn => { if (btn._filterBound) return; btn._filterBound = true; btn.addEventListener('click', function() { const category = this.getAttribute('data-category'); document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.filter-btn').forEach(b => { if (b.getAttribute('data-category') === category) { b.classList.add('active'); } }); filterTimeline(category); }); }); }
function syncActiveButtons() { const activeBtn = document.querySelector('.filter-btn.active'); if (activeBtn) { const category = activeBtn.getAttribute('data-category'); document.querySelectorAll('.filter-btn').forEach(btn => { if (btn.getAttribute('data-category') === category) { btn.classList.add('active'); } else { btn.classList.remove('active'); } }); } else { const allBtn = document.querySelector('.filter-btn[data-category="all"]'); if (allBtn) allBtn.classList.add('active'); syncActiveButtons(); } }
function updateFilterCounts() { const containers = document.querySelectorAll('.timeline'); const allItems = []; containers.forEach(container => { const items = container.querySelectorAll('.timeline-item:not(.headline)'); items.forEach(item => allItems.push(item)); });
const itemsData = allItems.map(item => ({ item: item, categories: findItemCategories(item) }));
const totalCount = itemsData.length; const btns = document.querySelectorAll('.filter-btn'); btns.forEach(btn => { const existingCount = btn.querySelector('.filter-count'); if (existingCount) existingCount.remove();
let count = 0; const category = btn.getAttribute('data-category');
if (category === 'all') { count = totalCount; } else { let effectiveCategory = category; if (category.startsWith('icon ') || category.startsWith('text ')) { effectiveCategory = category.substring(5); } effectiveCategory = effectiveCategory.trim().toLowerCase(); count = itemsData.filter(data => data.categories.includes(effectiveCategory)).length; }
const countSpan = document.createElement('span'); countSpan.className = 'filter-count'; countSpan.textContent = ` (${count})`;
btn.appendChild(countSpan); if (count === 0) { btn.setAttribute('hidden', 'hidden'); } else { btn.removeAttribute('hidden'); } }); }
function init() { bindFilterButtons(); syncActiveButtons(); updateFilterCounts(); document.addEventListener('pjax:complete', function() { bindFilterButtons(); syncActiveButtons(); updateFilterCounts(); const allBtn = document.querySelector('.filter-btn[data-category="all"]'); if (allBtn) allBtn.click(); }); }
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #article-container .category-filter { display: flex; flex-wrap: wrap; margin: 20px 0; gap: 12px; }
#article-container .category-filter .filter-btn { padding: 6px 16px; border-radius: 30px; border: 1px solid var(--anzhiyu-theme); color: var(--anzhiyu-theme); font-size: 14px; cursor: pointer; transition: all 0.2s ease; }
#article-container .category-filter .filter-btn.active, #article-container .category-filter .filter-btn:hover { border-color: var(--anzhiyu-theme); background: var(--anzhiyu-theme); color: #fff; }
#article-container .category-filter .filter-btn.active span::before { color: #fff; }
#article-container .category-filter .filter-btn[hidden] { display: none; }
#article-container .timeline { border-left-color: var(--anzhiyu-theme); }
#article-container .timeline .timeline-item.headline .timeline-item-title .item-circle::before, #article-container .timeline .timeline-item .item-circle::before { border-color:var(--anzhiyu-theme); }
#article-container .timeline .timeline-item.headline .timeline-item-title:hover .item-circle::before, #article-container .timeline .timeline-item:hover .item-circle::before { border-color: var(--pseudo-hover); }
#article-container .timeline .timeline-item .item-circle:not(:has(p))::before { display: none; }
|
1 2 3 4 5 6
| # example <div class="category-filter"> <button class="filter-btn active" data-category="all">全部</button> <button class="filter-btn" data-category="icon book-text">漫画</button> <button class="filter-btn" data-category="text docs">记录文档</button> </div>
|