/** * Homepage Fixed - Interactive Layer (v2.0 legacy-aligned) * * Unified AJAX protocol for homepage tabs, sort, and pagination. * Uses legacy-style ajaxpager DOM conventions and custom event flow: * post_ajax.before → fetch → post_ajax.ed → aksrt:posts:rendered * * @package AKSRT * @since 4.0.0 */ (function () { 'use strict'; var SORT_QUERY_KEY = 'home_sort'; var DEFAULT_SORT = 'modified'; var REST_PATH = '/wp-json/aksrt/v1/homepage/tab-posts'; /* ============================================================ * Utilities * ============================================================ */ function getViewMode() { return getDefaultView(); } function getRenderedPostCount(scope) { if (!scope) return 0; return scope.querySelectorAll('.posts-item, .posts-mini').length; } function getHomepagePerPage(panel) { var panelCount = getRenderedPostCount(panel); if (panelCount > 0) return panelCount; var configured = window.aksrtData && parseInt(aksrtData.homepagePostsPerPage, 10); if (configured && configured > 0) return configured; var latestPanel = document.querySelector('[data-home-tab-panel="latest"]'); var latestCount = getRenderedPostCount(latestPanel); return latestCount || 10; } function getDefaultSort() { var configured = window.aksrtData && aksrtData.homepageDefaultSort; var allowed = ['modified', 'date', 'downloads', 'views', 'likes', 'comments']; return allowed.indexOf(configured) !== -1 ? configured : DEFAULT_SORT; } function getDefaultView() { var panel = document.querySelector('[data-home-tab-panel][data-view]'); return panel && panel.getAttribute('data-view') === 'card' ? 'card' : 'list'; } function isSortAjaxEnabled(sortbar) { var attr = sortbar && sortbar.getAttribute('data-sort-ajax'); if (attr === 'false') return false; return !(window.aksrtData && aksrtData.homepageSortAjax === false); } function isStickyTabsEnabled() { return !(window.aksrtData && aksrtData.homepageStickyTabs === false); } function getRestBaseUrl() { var baseUrl = (window.aksrtData && aksrtData.siteUrl) || '/'; return baseUrl.replace(/\/?$/, ''); } function getSortFromUrl() { return new URLSearchParams(window.location.search).get(SORT_QUERY_KEY) || getDefaultSort(); } function getCurrentSort(stream) { var activeLink = stream ? stream.querySelector('.aksrt-homepage__sortbar-link.is-active') : null; return (activeLink && activeLink.getAttribute('data-sort')) || getSortFromUrl(); } function setActiveSortLink(sortbar, sort) { var links = sortbar.querySelectorAll('.aksrt-homepage__sortbar-link, .aksrt-homepage__sortbar-dropdown-item'); links.forEach(function (link) { link.classList.toggle('is-active', link.getAttribute('data-sort') === sort); }); // Update dropdown trigger text to match the active item var activeItem = sortbar.querySelector('.aksrt-homepage__sortbar-dropdown-item[data-sort="' + sort + '"]'); var trigger = sortbar.querySelector('[data-sort-dropdown-trigger]'); if (activeItem && trigger) { trigger.firstChild.textContent = activeItem.textContent; } } function normalizeHomepageUrl(url) { url.searchParams.delete('paged'); url.searchParams.delete('page'); url.pathname = url.pathname.replace(/\/page\/\d+\/?$/, '/'); return url; } function replaceUrlSort(sort) { if (!window.history || !window.history.replaceState) return; var url = normalizeHomepageUrl(new URL(window.location.href)); url.searchParams.set(SORT_QUERY_KEY, sort); window.history.replaceState({}, '', url.toString()); } function getStreamViewMode(stream) { var panel = getActivePanel(stream); return panel && panel.getAttribute('data-view') === 'card' ? 'card' : 'list'; } function getActivePanel(stream) { return stream.querySelector('.aksrt-homepage__panel.is-active, .aksrt-homepage__panel:not([hidden])'); } function getPanelTermId(panel) { if (!panel) return 0; var target = panel.getAttribute('data-home-tab-panel'); if (target === 'latest') return 0; var match = target ? target.match(/^term-(\d+)$/) : null; return match ? parseInt(match[1], 10) : 0; } function getPanelTabOrderby(panel) { if (!panel) return ''; return panel.getAttribute('data-tab-orderby') || ''; } function getPanelSource(panel) { var target = panel ? panel.getAttribute('data-home-tab-panel') : ''; if (target === 'latest') return 'home'; return target ? 'home' : 'widget'; } function buildRestUrl(options) { options = options || {}; return getRestBaseUrl() + REST_PATH + '?term_id=' + encodeURIComponent(options.termId || 0) + '&page=' + encodeURIComponent(options.page || 1) + '&per_page=' + encodeURIComponent(options.perPage || 10) + '&sort=' + encodeURIComponent(options.sort || getDefaultSort()) + '&page_url=' + encodeURIComponent(options.pageUrl || window.location.href) + '&panel=' + encodeURIComponent(options.panel || 'latest') + '&source=' + encodeURIComponent(options.source || 'home') + '&tab_orderby=' + encodeURIComponent(options.tabOrderby || ''); } /* ============================================================ * legacy-style event dispatching * ============================================================ */ /** * Dispatch post_ajax.before — fired before AJAX request is sent. * Detail: { termId, page, sort, viewMode, panel, source, url } */ function dispatchBeforeEvent(container, detail) { try { container.dispatchEvent(new CustomEvent('post_ajax.before', { bubbles: true, detail: detail })); } catch (e) { /* */ } } /** * Dispatch post_ajax.ed — fired after AJAX response is injected. * Detail: { termId, page, sort, viewMode, panel, source, total, hasMore } */ function dispatchAfterEvent(container, detail) { try { container.dispatchEvent(new CustomEvent('post_ajax.ed', { bubbles: true, detail: detail })); } catch (e) { /* */ } } /** * Dispatch aksrt:posts:rendered — fired after DOM is hydrated (lazyload etc). * Detail: { viewMode, panel } */ function dispatchRenderedEvent(container, detail) { try { container.dispatchEvent(new CustomEvent('aksrt:posts:rendered', { bubbles: true, detail: detail })); } catch (e) { /* */ } } /* ============================================================ * Skeleton, lazyload, and hydration * ============================================================ */ function buildSkeleton(count) { count = count || 3; var wrapper = document.createElement('div'); wrapper.className = 'aksrt-homepage__tab-skeleton'; for (var i = 0; i < count; i++) { var item = document.createElement('div'); item.className = 'aksrt-homepage__tab-skeleton-item'; item.innerHTML = '
' + '' + ((window.aksrtData && aksrtData.ajaxEmpty) || '暂无文章') + '
'; panel.appendChild(empty); dispatchAfterEvent(panel, { termId: options.termId, page: options.page, sort: options.sort, viewMode: options.viewMode, panel: options.panel, source: options.source, total: 0, hasMore: false }); }) .catch(function () { if (retryCount < maxRetries) { retryCount++; window.setTimeout(doFetch, 3000); return; } if (!options.append) panel.innerHTML = ''; var failedFooter = panel.querySelector('.aksrt-homepage__panel-footer'); if (failedFooter) failedFooter.remove(); panel.appendChild(buildErrorState(panel, options)); }); } doFetch(); } /* ============================================================ * Tab Navigation * ============================================================ */ function initHomeTabs(scope) { var tablists = (scope || document).querySelectorAll('[data-aksrt-home-tabs]'); tablists.forEach(function (list, listIndex) { var stream = list.closest('.aksrt-homepage__stream'); if (!stream) return; var buttons = Array.from(list.querySelectorAll('[data-home-tab-target]')); var panels = Array.from(stream.querySelectorAll('[data-home-tab-panel]')); if (buttons.length < 2 || !panels.length) return; function getPanel(button) { var target = button.getAttribute('data-home-tab-target'); return panels.find(function (p) { return p.getAttribute('data-home-tab-panel') === target; }); } function activate(button) { var target = button.getAttribute('data-home-tab-target'); var panel = getPanel(button); var termId = getPanelTermId(panel); var currentSort = getCurrentSort(stream); var viewMode = getStreamViewMode(stream); var tabOrderby = button.getAttribute('data-tab-orderby') || getPanelTabOrderby(panel) || ''; // Toggle active state on tab buttons buttons.forEach(function (item) { var isActive = item === button; var itemWrap = item.closest('.aksrt-homepage__tab-item'); if (itemWrap) itemWrap.classList.toggle('is-active', isActive); item.setAttribute('aria-selected', isActive ? 'true' : 'false'); item.tabIndex = isActive ? 0 : -1; }); // Toggle active state on panels (ajaxpager convention) panels.forEach(function (tabPanel) { var isActive = tabPanel === panel; tabPanel.classList.toggle('is-active', isActive); if (isActive) { tabPanel.removeAttribute('hidden'); tabPanel.setAttribute('aria-hidden', 'false'); } else { tabPanel.setAttribute('hidden', ''); tabPanel.setAttribute('aria-hidden', 'true'); } }); // Load content if needed (lazy AJAX load like legacy) // For tab panels, the "loaded sort" check uses tabOrderby, not main stream sort var effectiveSort = tabOrderby || currentSort; if (panel && (panel.dataset.loaded !== 'true' || panel.dataset.loadedSort !== effectiveSort)) { requestHomepagePanel(panel, { termId: termId, page: 1, perPage: getHomepagePerPage(panel), sort: currentSort, viewMode: viewMode, panel: target, source: 'home', tabOrderby: tabOrderby }); } } // Set up ARIA relationships buttons.forEach(function (button) { var target = button.getAttribute('data-home-tab-target'); var panel = getPanel(button); var buttonId = 'aksrt-home-tab-' + listIndex + '-' + target; button.id = button.id || buttonId; if (panel) { panel.id = panel.id || buttonId + '-panel'; panel.setAttribute('aria-labelledby', button.id); button.setAttribute('aria-controls', panel.id); // Add ajaxpager class for legacy DOM convention panel.classList.add('ajaxpager'); } button.addEventListener('click', function () { activate(button); }); button.addEventListener('keydown', function (event) { if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft') return; event.preventDefault(); var direction = event.key === 'ArrowRight' ? 1 : -1; var currentIndex = buttons.indexOf(button); var nextIndex = (currentIndex + direction + buttons.length) % buttons.length; var nextButton = buttons[nextIndex]; if (nextButton) { nextButton.focus(); activate(nextButton); } }); }); // Mark server-rendered panels as already loaded. The panel itself owns // the posts-row contract, so checking descendants would miss SSR items. panels.forEach(function (panel) { if (getRenderedPostCount(panel) < 1) return; panel.dataset.loaded = 'true'; panel.dataset.loadedPage = '1'; panel.dataset.loadedSort = getCurrentSort(stream); panel.dataset.loadedTermId = String(getPanelTermId(panel)); }); // Activate initial tab var initialButton = buttons.find(function (b) { return b.getAttribute('aria-selected') === 'true'; }) || buttons[0]; activate(initialButton); }); } /* ============================================================ * Sortbar Dropdown Toggle * ============================================================ */ function initSortbarDropdown() { document.querySelectorAll('[data-sort-dropdown-trigger]').forEach(function (trigger) { var wrapper = trigger.closest('.aksrt-homepage__sortbar-dropdown'); if (!wrapper) return; var menu = wrapper.querySelector('.aksrt-homepage__sortbar-dropdown-menu'); if (!menu) return; function close() { menu.setAttribute('hidden', ''); trigger.setAttribute('aria-expanded', 'false'); } function toggle() { var isOpen = !menu.hasAttribute('hidden'); if (isOpen) { close(); } else { menu.removeAttribute('hidden'); trigger.setAttribute('aria-expanded', 'true'); } } trigger.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); toggle(); }); // Close when clicking an item menu.addEventListener('click', function (e) { var item = e.target.closest('.aksrt-homepage__sortbar-dropdown-item'); if (!item) return; // Update trigger text trigger.firstChild.textContent = item.textContent; // Update active state menu.querySelectorAll('.aksrt-homepage__sortbar-dropdown-item').forEach(function (el) { el.classList.toggle('is-active', el === item); }); close(); }); // Close on outside click document.addEventListener('click', function (e) { if (!wrapper.contains(e.target)) close(); }); // Close on Escape document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !menu.hasAttribute('hidden')) { close(); trigger.focus(); } }); }); } /* ============================================================ * Sortbar AJAX * ============================================================ */ function initSortbar() { document.querySelectorAll('.aksrt-homepage__stream').forEach(function (stream) { var sortbar = stream.querySelector('.aksrt-homepage__sortbar'); if (!sortbar || !isSortAjaxEnabled(sortbar)) return; sortbar.addEventListener('click', function (event) { var link = event.target.closest('.aksrt-homepage__sortbar-link, .aksrt-homepage__sortbar-dropdown-item'); var activePanel = getActivePanel(stream); var sort = link ? link.getAttribute('data-sort') : ''; var termId = getPanelTermId(activePanel); if (!link || !activePanel || !sort) return; event.preventDefault(); setActiveSortLink(sortbar, sort); replaceUrlSort(sort); requestHomepagePanel(activePanel, { termId: termId, page: 1, perPage: getHomepagePerPage(activePanel), sort: sort, viewMode: getStreamViewMode(stream), panel: activePanel.getAttribute('data-home-tab-panel') || 'latest', source: 'home' }); }); }); } /* ============================================================ * Sticky Tabs * ============================================================ */ function supportsStickyPosition() { if (window.CSS && typeof window.CSS.supports === 'function') { return window.CSS.supports('position', 'sticky') || window.CSS.supports('position', '-webkit-sticky'); } var testNode = document.createElement('div'); testNode.style.position = 'sticky'; return testNode.style.position.indexOf('sticky') !== -1; } function getStickyOffset() { var docStyles = window.getComputedStyle(document.documentElement); var offset = parseFloat(docStyles.getPropertyValue('--nav-height') || '60'); return isNaN(offset) ? 60 : offset; } function initStickyTabs() { if (!isStickyTabsEnabled()) return; document.querySelectorAll('.aksrt-homepage__tabs.home-tab-nav-box').forEach(function (tabs) { var parent = tabs.parentNode; var sentinel = tabs.previousElementSibling; var canUseSticky = supportsStickyPosition(); var hasObserver = 'IntersectionObserver' in window; var observer = null; if (!parent || tabs.getAttribute('data-sticky-tabs') === 'false') return; if (!sentinel || !sentinel.classList.contains('aksrt-homepage__tabs-sentinel')) { sentinel = document.createElement('div'); sentinel.className = 'aksrt-homepage__tabs-sentinel'; parent.insertBefore(sentinel, tabs); } function clearAffixBox() { sentinel.style.height = '1px'; tabs.style.left = ''; tabs.style.width = ''; } function syncAffixBox() { var rect = sentinel.getBoundingClientRect(); sentinel.style.height = tabs.offsetHeight + 'px'; tabs.style.left = rect.left + 'px'; tabs.style.width = rect.width + 'px'; } function setStickyState(isStuck) { tabs.classList.toggle('is-stuck', isStuck); if (!canUseSticky) { tabs.classList.toggle('is-affixed', isStuck); if (isStuck) { syncAffixBox(); } else { clearAffixBox(); } return; } tabs.classList.remove('is-affixed'); clearAffixBox(); } function evaluateFallbackState() { setStickyState(sentinel.getBoundingClientRect().top <= getStickyOffset()); } if (canUseSticky && hasObserver) { var bindObserver = function () { if (observer) observer.disconnect(); observer = new IntersectionObserver(function (entries) { setStickyState(!entries[0].isIntersecting); }, { rootMargin: '-' + getStickyOffset() + 'px 0px 0px 0px', threshold: [1] }); observer.observe(sentinel); }; bindObserver(); window.addEventListener('resize', bindObserver); return; } window.addEventListener('scroll', evaluateFallbackState, { passive: true }); window.addEventListener('resize', evaluateFallbackState); evaluateFallbackState(); }); } /* ============================================================ * Init * ============================================================ */ function init() { initHomeTabs(); initSortbarDropdown(); initSortbar(); initStickyTabs(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();