455 lines
15 KiB
JavaScript
455 lines
15 KiB
JavaScript
/**
|
|
* AKSRT Page Navigation — v3.0
|
|
*
|
|
* 功能:
|
|
* - TOC 高亮 + 平滑滚动(文档阅读页)
|
|
* - 文档导航搜索 + 结果接管(文档导航页)
|
|
* - 分类切换 AJAX 加载 + 分页(文档导航页)
|
|
* - 加载状态和分页控件
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
var API_BASE = (window.aksrtData && window.aksrtData.restNonce)
|
|
? (window.aksrtData.siteUrl || '') + '/wp-json/aksrt/v1'
|
|
: '';
|
|
var NONCE = (window.aksrtData && window.aksrtData.restNonce) || '';
|
|
|
|
var PAGE_NAVS = {
|
|
tocItems: [],
|
|
headings: [],
|
|
scrollTimer: null,
|
|
activeCategory: 0,
|
|
currentPage: 1,
|
|
totalPages: 1,
|
|
searchQuery: '',
|
|
ajaxInFlight: null,
|
|
|
|
init: function () {
|
|
this.initTocHighlight();
|
|
this.initSmoothScroll();
|
|
this.initDocSearch();
|
|
this.initCategoryChips();
|
|
this.initPagination();
|
|
},
|
|
|
|
/* ============================================================
|
|
* TOC 高亮(文档阅读页)
|
|
* ============================================================ */
|
|
initTocHighlight: function () {
|
|
var toc = document.querySelector('.aksrt-doc-toc');
|
|
if (!toc) return;
|
|
|
|
this.tocItems = Array.from(toc.querySelectorAll('.aksrt-doc-toc__item a'));
|
|
if (this.tocItems.length === 0) return;
|
|
|
|
this.headings = this.tocItems.map(function (link) {
|
|
var id = link.getAttribute('href');
|
|
if (id && id.startsWith('#')) {
|
|
return document.getElementById(id.substring(1));
|
|
}
|
|
return null;
|
|
}).filter(Boolean);
|
|
|
|
if (this.headings.length === 0) return;
|
|
|
|
var self = this;
|
|
window.addEventListener('scroll', function () {
|
|
if (self.scrollTimer) clearTimeout(self.scrollTimer);
|
|
self.scrollTimer = setTimeout(function () { self.updateTocHighlight(); }, 50);
|
|
}, { passive: true });
|
|
|
|
this.updateTocHighlight();
|
|
},
|
|
|
|
updateTocHighlight: function () {
|
|
var scrollY = window.scrollY + 100;
|
|
var currentIndex = -1;
|
|
|
|
for (var i = this.headings.length - 1; i >= 0; i--) {
|
|
if (this.headings[i].offsetTop <= scrollY) {
|
|
currentIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.tocItems.forEach(function (link, index) {
|
|
var li = link.parentElement;
|
|
if (index === currentIndex) {
|
|
li.classList.add('is-active');
|
|
link.classList.add('is-active');
|
|
} else {
|
|
li.classList.remove('is-active');
|
|
link.classList.remove('is-active');
|
|
}
|
|
});
|
|
},
|
|
|
|
/* ============================================================
|
|
* 平滑滚动
|
|
* ============================================================ */
|
|
initSmoothScroll: function () {
|
|
document.addEventListener('click', function (e) {
|
|
var link = e.target.closest('a[href^="#"]');
|
|
if (!link) return;
|
|
|
|
var id = link.getAttribute('href').substring(1);
|
|
if (!id) return;
|
|
|
|
var target = document.getElementById(id);
|
|
if (!target) return;
|
|
|
|
e.preventDefault();
|
|
var offset = 80; // nav height
|
|
var top = target.getBoundingClientRect().top + window.scrollY - offset;
|
|
window.scrollTo({ top: top, behavior: 'smooth' });
|
|
});
|
|
},
|
|
|
|
/* ============================================================
|
|
* 文档搜索(AJAX 接管)
|
|
* ============================================================ */
|
|
initDocSearch: function () {
|
|
var searchInput = document.querySelector('.aksrt-doc-search__input');
|
|
if (!searchInput) return;
|
|
|
|
var resultsContainer = document.querySelector('.aksrt-doc-search__results');
|
|
var takeover = document.querySelector('.aksrt-doc-takeover');
|
|
var self = this;
|
|
|
|
var debounceTimer = null;
|
|
searchInput.addEventListener('input', function () {
|
|
var query = searchInput.value.trim();
|
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
|
|
if (query.length < 2) {
|
|
self.clearSearch(resultsContainer, takeover);
|
|
return;
|
|
}
|
|
|
|
debounceTimer = setTimeout(function () {
|
|
self.searchDocs(query, resultsContainer, takeover);
|
|
}, 300);
|
|
});
|
|
|
|
searchInput.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape') {
|
|
searchInput.value = '';
|
|
self.clearSearch(resultsContainer, takeover);
|
|
}
|
|
});
|
|
},
|
|
|
|
searchDocs: function (query, resultsContainer, takeover) {
|
|
if (this.ajaxInFlight) {
|
|
this.ajaxInFlight.abort();
|
|
}
|
|
|
|
var self = this;
|
|
this.showLoading(takeover || resultsContainer);
|
|
|
|
var url = API_BASE + '/docs/search?q=' + encodeURIComponent(query);
|
|
this.ajaxInFlight = this.ajaxRequest(url, function (data) {
|
|
self.hideLoading(takeover || resultsContainer);
|
|
self.renderSearchResults(data, resultsContainer, takeover, query);
|
|
}, function () {
|
|
self.hideLoading(takeover || resultsContainer);
|
|
});
|
|
},
|
|
|
|
renderSearchResults: function (data, resultsContainer, takeover, query) {
|
|
var items = data.items || data.data || [];
|
|
if (items.length === 0) {
|
|
if (resultsContainer) {
|
|
resultsContainer.hidden = false;
|
|
resultsContainer.innerHTML = '<div class="aksrt-doc-search__empty">未找到相关文档</div>';
|
|
}
|
|
return;
|
|
}
|
|
|
|
var html = '<div class="aksrt-doc-search__list">';
|
|
items.forEach(function (item) {
|
|
html += '<a class="aksrt-doc-search__item" href="' + this.escHtml(item.url) + '">';
|
|
html += '<span class="aksrt-doc-search__item-title">' + this.highlightQuery(this.escHtml(item.title), query) + '</span>';
|
|
if (item.excerpt) {
|
|
html += '<span class="aksrt-doc-search__item-excerpt">' + this.highlightQuery(this.escHtml(item.excerpt), query) + '</span>';
|
|
}
|
|
html += '</a>';
|
|
}.bind(this));
|
|
html += '</div>';
|
|
|
|
if (takeover) {
|
|
takeover.innerHTML = html;
|
|
takeover.hidden = false;
|
|
} else if (resultsContainer) {
|
|
resultsContainer.innerHTML = html;
|
|
resultsContainer.hidden = false;
|
|
}
|
|
},
|
|
|
|
highlightQuery: function (text, query) {
|
|
if (!query) return text;
|
|
var escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
return text.replace(new RegExp('(' + escaped + ')', 'gi'), '<mark>$1</mark>');
|
|
},
|
|
|
|
clearSearch: function (resultsContainer, takeover) {
|
|
if (takeover) {
|
|
takeover.hidden = true;
|
|
takeover.innerHTML = '';
|
|
}
|
|
if (resultsContainer) {
|
|
resultsContainer.hidden = true;
|
|
resultsContainer.innerHTML = '';
|
|
}
|
|
// 显示所有卡片
|
|
var cards = document.querySelectorAll('.aksrt-doc-index-card');
|
|
cards.forEach(function (card) { card.style.display = ''; });
|
|
},
|
|
|
|
showLoading: function (container) {
|
|
if (!container) return;
|
|
var existing = container.querySelector('.aksrt-doc-loading');
|
|
if (existing) return;
|
|
|
|
var loader = document.createElement('div');
|
|
loader.className = 'aksrt-doc-loading';
|
|
loader.innerHTML = '<div class="aksrt-doc-loading__spinner"></div><span>搜索中...</span>';
|
|
container.appendChild(loader);
|
|
},
|
|
|
|
hideLoading: function (container) {
|
|
if (!container) return;
|
|
var loader = container.querySelector('.aksrt-doc-loading');
|
|
if (loader) loader.remove();
|
|
},
|
|
|
|
/* ============================================================
|
|
* 分类切换 — AJAX 加载 + 分页
|
|
* ============================================================ */
|
|
initCategoryChips: function () {
|
|
var toolbar = document.querySelector('.aksrt-doc-toolbar');
|
|
if (!toolbar) return;
|
|
|
|
var chips = toolbar.querySelectorAll('.aksrt-doc-toolbar__chip[data-cat-id]');
|
|
if (chips.length === 0) return;
|
|
|
|
var self = this;
|
|
chips.forEach(function (chip) {
|
|
chip.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
var catId = parseInt(chip.getAttribute('data-cat-id') || '0', 10);
|
|
|
|
// 切换激活状态
|
|
chips.forEach(function (c) { c.classList.remove('is-active'); });
|
|
chip.classList.add('is-active');
|
|
|
|
self.activeCategory = catId;
|
|
self.currentPage = 1;
|
|
self.loadCategoryDocs(catId, 1);
|
|
});
|
|
});
|
|
},
|
|
|
|
loadCategoryDocs: function (catId, page) {
|
|
if (this.ajaxInFlight) {
|
|
this.ajaxInFlight.abort();
|
|
}
|
|
|
|
var self = this;
|
|
var grid = document.querySelector('.aksrt-doc-grid');
|
|
if (!grid) return;
|
|
|
|
// 显示加载状态
|
|
grid.classList.add('is-loading');
|
|
|
|
var url = API_BASE + '/docs/list?cat_id=' + catId + '&page=' + page + '&per_page=12';
|
|
this.ajaxInFlight = this.ajaxRequest(url, function (data) {
|
|
grid.classList.remove('is-loading');
|
|
self.renderCategoryDocs(data, grid);
|
|
self.updatePagination(data.total || 0, data.pages || 1, page);
|
|
self.updateMetaCount(data.total || 0);
|
|
}, function () {
|
|
grid.classList.remove('is-loading');
|
|
});
|
|
},
|
|
|
|
renderCategoryDocs: function (data, grid) {
|
|
var items = data.items || data.data || [];
|
|
if (items.length === 0) {
|
|
grid.innerHTML = '<div class="aksrt-doc-empty">暂无文档</div>';
|
|
return;
|
|
}
|
|
|
|
var html = '';
|
|
items.forEach(function (item) {
|
|
html += '<a class="aksrt-doc-index-card" href="' + this.escHtml(item.url) + '" data-cat-id="' + (item.cat_id || 0) + '">';
|
|
if (item.thumbnail) {
|
|
html += '<img' + window.aksrtGetLazyImageAttr('lazy_other', item.thumbnail, 'aksrt-doc-index-card__thumb') + ' alt="">';
|
|
}
|
|
html += '<div class="aksrt-doc-index-card__body">';
|
|
html += '<h3 class="aksrt-doc-index-card__title">' + this.escHtml(item.title) + '</h3>';
|
|
if (item.excerpt) {
|
|
html += '<p class="aksrt-doc-index-card__desc">' + this.escHtml(item.excerpt) + '</p>';
|
|
}
|
|
html += '<div class="aksrt-doc-index-card__meta">';
|
|
if (item.child_count) html += '<span>' + item.child_count + ' 篇子文档</span>';
|
|
if (item.date) html += '<span>' + this.escHtml(item.date) + '</span>';
|
|
html += '</div></div></a>';
|
|
}.bind(this));
|
|
|
|
grid.innerHTML = html;
|
|
},
|
|
|
|
updateMetaCount: function (total) {
|
|
var meta = document.querySelector('.aksrt-doc-toolbar__meta');
|
|
if (meta) {
|
|
meta.textContent = total + ' 篇文档';
|
|
}
|
|
},
|
|
|
|
/* ============================================================
|
|
* 分页控件
|
|
* ============================================================ */
|
|
initPagination: function () {
|
|
var self = this;
|
|
var paginationContainer = document.querySelector('.aksrt-doc-pagination');
|
|
if (!paginationContainer) return;
|
|
|
|
paginationContainer.addEventListener('click', function (e) {
|
|
var btn = e.target.closest('[data-page]');
|
|
if (!btn) return;
|
|
|
|
e.preventDefault();
|
|
var page = parseInt(btn.getAttribute('data-page'), 10);
|
|
if (page < 1 || page > self.totalPages || page === self.currentPage) return;
|
|
|
|
self.currentPage = page;
|
|
self.loadCategoryDocs(self.activeCategory, page);
|
|
|
|
// 滚动到顶部
|
|
var grid = document.querySelector('.aksrt-doc-grid');
|
|
if (grid) {
|
|
grid.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
});
|
|
},
|
|
|
|
updatePagination: function (total, totalPages, currentPage) {
|
|
this.totalPages = totalPages;
|
|
this.currentPage = currentPage;
|
|
|
|
var container = document.querySelector('.aksrt-doc-pagination');
|
|
if (!container) return;
|
|
|
|
if (totalPages <= 1) {
|
|
container.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
var html = '<div class="aksrt-doc-pagination__inner">';
|
|
|
|
// 上一页
|
|
if (currentPage > 1) {
|
|
html += '<button class="aksrt-doc-pagination__btn" data-page="' + (currentPage - 1) + '"><i class="fas fa-chevron-left"></i></button>';
|
|
}
|
|
|
|
// 页码
|
|
var start = Math.max(1, currentPage - 2);
|
|
var end = Math.min(totalPages, currentPage + 2);
|
|
|
|
if (start > 1) {
|
|
html += '<button class="aksrt-doc-pagination__btn" data-page="1">1</button>';
|
|
if (start > 2) html += '<span class="aksrt-doc-pagination__ellipsis">...</span>';
|
|
}
|
|
|
|
for (var i = start; i <= end; i++) {
|
|
html += '<button class="aksrt-doc-pagination__btn' + (i === currentPage ? ' is-active' : '') + '" data-page="' + i + '">' + i + '</button>';
|
|
}
|
|
|
|
if (end < totalPages) {
|
|
if (end < totalPages - 1) html += '<span class="aksrt-doc-pagination__ellipsis">...</span>';
|
|
html += '<button class="aksrt-doc-pagination__btn" data-page="' + totalPages + '">' + totalPages + '</button>';
|
|
}
|
|
|
|
// 下一页
|
|
if (currentPage < totalPages) {
|
|
html += '<button class="aksrt-doc-pagination__btn" data-page="' + (currentPage + 1) + '"><i class="fas fa-chevron-right"></i></button>';
|
|
}
|
|
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
},
|
|
|
|
/* ============================================================
|
|
* 工具函数
|
|
* ============================================================ */
|
|
escHtml: function (str) {
|
|
var div = document.createElement('div');
|
|
div.appendChild(document.createTextNode(str || ''));
|
|
return div.innerHTML;
|
|
},
|
|
|
|
filterDocItems: function (query) {
|
|
var cards = document.querySelectorAll('.aksrt-doc-index-card');
|
|
if (cards.length > 0) {
|
|
cards.forEach(function (card) {
|
|
var title = card.querySelector('.aksrt-doc-index-card__title');
|
|
var desc = card.querySelector('.aksrt-doc-index-card__desc');
|
|
var text = (title ? title.textContent : '') + ' ' + (desc ? desc.textContent : '');
|
|
if (query === '' || text.toLowerCase().indexOf(query) >= 0) {
|
|
card.style.display = '';
|
|
} else {
|
|
card.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
var sidebarItems = document.querySelectorAll('.aksrt-doc-sidebar__item');
|
|
if (sidebarItems.length > 0) {
|
|
sidebarItems.forEach(function (item) {
|
|
var link = item.querySelector('.aksrt-doc-sidebar__link');
|
|
var text = link ? link.textContent : '';
|
|
if (query === '' || text.toLowerCase().indexOf(query) >= 0) {
|
|
item.style.display = '';
|
|
} else {
|
|
item.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
ajaxRequest: function (url, onSuccess, onError) {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open('GET', url, true);
|
|
if (NONCE) {
|
|
xhr.setRequestHeader('X-WP-Nonce', NONCE);
|
|
}
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState !== 4) return;
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
try {
|
|
var data = JSON.parse(xhr.responseText);
|
|
if (onSuccess) onSuccess(data);
|
|
} catch (e) {
|
|
if (onError) onError(e);
|
|
}
|
|
} else {
|
|
if (onError) onError(new Error('HTTP ' + xhr.status));
|
|
}
|
|
};
|
|
xhr.send();
|
|
return xhr;
|
|
}
|
|
};
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', function () { PAGE_NAVS.init(); });
|
|
} else {
|
|
PAGE_NAVS.init();
|
|
}
|
|
})();
|