Files
wordpress-theme-aksrt/assets/widgets/frontend.js
T
kate522 b639d57c5b feat(widgets): replicate 12 reference widgets for full Zibll parity
- rewrite schemas for posts, tab_posts, oneline_posts, hot_posts,
  term_cards, term_aggregation, media_cards, combo_info, partners,
  recent_comments, posts_toc and slider_media_combo to mirror the
  modern reference modules field by field (options, defaults,
  dependencies, min/max/step, subheadings)
- port reference renderers: hot ranking with TOP badges, category
  aggregation card, graphic cover cards, partners CSS marquee,
  combo info cards with auto band palette, recent comment list
- rename 分类与标签聚合卡片 to 分类聚合卡片 and restrict it to
  categories (no tags/topics branch in this project)
- drop topics filter fields and the topics query branch; align
  orderby value domain with supported post meta
- add searchable sortable terms admin field type and spinner unit
  suffix rendering
- unify widget pagination and lazy loading on the REST client with
  the runtime-owned ias reveal trigger; remove the legacy
  post_ajax_trigger dual path
- align section-navs TOC with the reference contract (wznav ids,
  3-heading minimum, zib-widget box)
2026-09-05 11:28:00 +08:00

303 lines
13 KiB
JavaScript

(function () {
'use strict';
var config = window.aksrtWidgets || {};
var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function once(el, key) {
var name = 'aksrt' + key;
if (el.dataset[name]) return false;
el.dataset[name] = '1';
return true;
}
function moveTabFocus(event, root) {
var current = event.target.closest('[role="tab"]');
if (!current) return;
var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]'));
var index = tabs.indexOf(current);
var next = index;
if (event.key === 'ArrowRight') next = (index + 1) % tabs.length;
else if (event.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length;
else if (event.key === 'Home') next = 0;
else if (event.key === 'End') next = tabs.length - 1;
else return;
event.preventDefault();
tabs[next].focus();
tabs[next].click();
}
function videoType(url) {
if (/\.m3u8(?:\?|$)/i.test(url)) return 'hls';
if (/\.flv(?:\?|$)/i.test(url)) return 'flv';
if (/\.mpd(?:\?|$)/i.test(url)) return 'dash';
return 'auto';
}
function initVideo(el) {
if (!once(el, 'Video')) return;
var fallback = el.querySelector('video');
if (fallback) {
fallback.addEventListener('error', function () {
var message = el.querySelector('.aksrt-widget-media-error');
if (message) message.hidden = false;
});
}
function mountPlayer(event) {
if (typeof window.DPlayer !== 'function' || el.aksrtPlayer) return;
if (event) event.preventDefault();
var holder = document.createElement('div');
holder.className = 'aksrt-widget-video__player';
el.insertBefore(holder, fallback || el.firstChild);
try {
el.aksrtPlayer = new window.DPlayer({
container: holder,
autoplay: el.dataset.autoplay === '1' && !reducedMotion,
video: {
url: el.dataset.videoUrl || '',
pic: el.dataset.videoPoster || '',
type: videoType(el.dataset.videoUrl || '')
}
});
if (el.dataset.muted === '1' && el.aksrtPlayer.video) el.aksrtPlayer.video.muted = true;
if (fallback) fallback.hidden = true;
} catch (error) {
holder.remove();
}
}
if (fallback && fallback.classList.contains('lazyload')) {
fallback.addEventListener('lazybeforeunveil', mountPlayer, { once: true });
} else {
mountPlayer();
}
}
function updateQuote(figure) {
if (figure.dataset.source !== 'api' || !config.quoteUrl) return Promise.resolve();
var button = figure.querySelector('button');
if (button) button.disabled = true;
return fetch(config.quoteUrl, { credentials: 'same-origin', headers: { Accept: 'application/json' } })
.then(function (response) { if (!response.ok) throw new Error('quote'); return response.json(); })
.then(function (data) {
if (data && data.text) figure.querySelector('blockquote').textContent = data.text;
})
.catch(function () {})
.finally(function () { if (button) button.disabled = false; });
}
function initQuote(figure) {
if (!once(figure, 'Quote')) return;
var button = figure.querySelector('button');
if (button) button.addEventListener('click', function () { updateQuote(figure); });
if (!reducedMotion && figure.dataset.source === 'api') {
window.setInterval(function () { if (!document.hidden) updateQuote(figure); }, Math.max(5, Number(figure.dataset.interval || 30)) * 1000);
}
}
function errorMarkup() {
var i18n = config.i18n || {};
return '<div class="aksrt-widget-request-error" role="alert"><span>' + (i18n.error || '加载失败,请重试。') + '</span><button type="button" data-widget-retry>' + (i18n.retry || '重试') + '</button></div>';
}
function itemsUrl(widget, instance, page, segment) {
var base = String(config.restUrl || '').replace(/\/$/, '');
var url = base + '/' + encodeURIComponent(widget) + '/items?instance=' + encodeURIComponent(instance) + '&page=' + encodeURIComponent(page);
if (segment !== undefined && segment !== '') url += '&segment=' + encodeURIComponent(segment);
return url;
}
function loaderOf(container) {
return container.querySelector('.post_ajax_loader');
}
function setBusy(container, busy) {
if (busy) {
container.dataset.loading = '1';
container.setAttribute('aria-busy', 'true');
var loader = loaderOf(container);
if (loader) loader.style.display = '';
} else {
delete container.dataset.loading;
container.setAttribute('aria-busy', 'false');
var loader = loaderOf(container);
if (loader) loader.style.display = 'none';
}
}
function loadItems(container, page, append) {
var owner = container.closest('[data-widget]') || container;
var widget = owner.dataset.widget || container.dataset.widget;
var instance = owner.dataset.instance || container.dataset.instance;
var segment = container.dataset.segment || owner.dataset.segment || '';
if (!widget || !instance || !config.restUrl || container.dataset.loading === '1') return;
setBusy(container, true);
fetch(itemsUrl(widget, instance, page, segment), { credentials: 'same-origin', headers: { Accept: 'application/json' } })
.then(function (response) { if (!response.ok) throw new Error('items'); return response.json(); })
.then(function (data) {
var fragment = document.createElement('div');
fragment.innerHTML = String(data.html || '');
Array.prototype.slice.call(container.querySelectorAll(':scope > .ajax-pag')).forEach(function (node) { node.remove(); });
if (append) {
Array.prototype.slice.call(fragment.children).forEach(function (node) { container.appendChild(node); });
} else {
container.innerHTML = fragment.innerHTML;
}
if (data.paginate_html) container.insertAdjacentHTML('beforeend', data.paginate_html);
container.dataset.page = String(data.page || page);
scan(container);
})
.catch(function () { container.innerHTML = errorMarkup(); })
.finally(function () { setBusy(container, false); });
}
function initItems(container) {
if (!once(container, 'Items')) return;
if (container.classList.contains('is-loading')) loadItems(container, 1, false);
container.addEventListener('click', function (event) {
var retry = event.target.closest('[data-widget-retry]');
var trigger = event.target.closest('[data-next-page]');
var pageLink = event.target.closest('.page-numbers[data-widget-page]');
if (retry) {
loadItems(container, Number(container.dataset.page || 1), false);
return;
}
if (trigger) {
// data-widget-ias marks the hidden reveal trigger the runtime clicks
// when the lazy container scrolls into view; it reloads page one.
event.preventDefault();
if (trigger.dataset.widgetIas === '1') loadItems(container, 1, false);
else loadItems(container, Number(trigger.dataset.nextPage || 2), true);
return;
}
if (pageLink) {
event.preventDefault();
loadItems(container, Number(pageLink.dataset.widgetPage || 1), false);
}
});
}
function initTabs(tabs) {
if (!once(tabs, 'Tabs')) return;
tabs.addEventListener('keydown', function (event) { moveTabFocus(event, tabs); });
tabs.addEventListener('click', function (event) {
var button = event.target.closest('[role="tab"][data-segment]');
if (!button) return;
event.preventDefault();
tabs.querySelectorAll('[role="tab"]').forEach(function (tab) {
tab.setAttribute('aria-selected', tab === button ? 'true' : 'false');
tab.setAttribute('tabindex', tab === button ? '0' : '-1');
if (tab.parentElement) tab.parentElement.classList.toggle('active', tab === button);
});
tabs.querySelectorAll('.tab-content > .tab-pane').forEach(function (item) {
var active = item.id === button.getAttribute('aria-controls');
item.classList.toggle('active', active); item.classList.toggle('in', active);
});
var panel = tabs.querySelector('[data-widget-items][data-segment="' + button.dataset.segment + '"]');
if (!panel) panel = tabs.querySelector('[data-widget-items]');
if (!panel) return;
panel.dataset.segment = button.dataset.segment;
panel.setAttribute('aria-labelledby', button.id);
loadItems(panel, 1, false);
});
}
function initFaq(faq) {
if (!once(faq, 'Faq')) return;
faq.addEventListener('click', function (event) {
var button = event.target.closest('.aksrt-faq__item > button');
if (!button) return;
var expanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', expanded ? 'false' : 'true');
var panel = document.getElementById(button.getAttribute('aria-controls'));
if (panel) panel.hidden = expanded;
});
var search = faq.querySelector('input[type="search"]');
if (search) search.addEventListener('input', function () {
var needle = search.value.trim().toLowerCase();
faq.querySelectorAll('.aksrt-faq__item').forEach(function (item) { item.hidden = needle && String(item.dataset.search || '').toLowerCase().indexOf(needle) === -1; });
});
}
function initFeatures(features) {
if (!once(features, 'Features')) return;
features.addEventListener('keydown', function (event) { moveTabFocus(event, features); });
features.addEventListener('click', function (event) {
var button = event.target.closest('[data-feature-index]');
if (!button) return;
var index = button.dataset.featureIndex;
features.querySelectorAll('[data-feature-index]').forEach(function (tab) {
tab.setAttribute('aria-selected', tab === button ? 'true' : 'false');
tab.setAttribute('tabindex', tab === button ? '0' : '-1');
});
features.querySelectorAll('[data-feature-panel]').forEach(function (panel) { panel.hidden = panel.dataset.featurePanel !== index; });
});
}
function initCount(el) {
if (!once(el, 'Count') || reducedMotion) return;
var raw = el.dataset.countUp || '';
var match = raw.match(/-?[0-9]+(?:\.[0-9]+)?/);
if (!match) return;
var target = Number(match[0]);
var prefix = raw.slice(0, match.index);
var suffix = raw.slice(match.index + match[0].length);
var start = performance.now();
function frame(now) {
var progress = Math.min(1, (now - start) / 900);
el.textContent = prefix + Math.round(target * (1 - Math.pow(1 - progress, 3))) + suffix;
if (progress < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
/* 幻灯片加图文视频卡片:以左侧幻灯片实际高度重算右侧卡片比例。
* 合作伙伴滚动由运行时 main.js 初始化(.zib-partners-scroll 唯一所有者)。 */
function measureSliderGraphic(widget) {
if (once(widget, 'Combo')) return;
var slider = widget.querySelector('.slider-col .zib-slider') || widget.querySelector('.slider-col');
var covers = widget.querySelector('.cover-col');
if (!slider || !covers) return;
var row = Number(widget.dataset.row || 2);
var col = Number(widget.dataset.col || 2);
var gap = 10;
function update() {
var height = slider.offsetHeight;
var width = covers.offsetWidth;
if (!height || !width) return;
var ratio = (((height - (col - 1) * gap) / col) / ((width - (row - 1) * gap) / row)) * 100;
covers.style.setProperty('--cover-ratio', ratio.toFixed(2) + '%');
}
update();
window.addEventListener('resize', update, { passive: true });
if ('ResizeObserver' in window) new ResizeObserver(update).observe(slider);
}
function scan(root) {
var scope = root || document;
scope.querySelectorAll('.aksrt-widget-video[data-video-url]').forEach(initVideo);
scope.querySelectorAll('.aksrt-widget-quote').forEach(initQuote);
scope.querySelectorAll('[data-widget-items]').forEach(initItems);
scope.querySelectorAll('.aksrt-widget-tabs, .widget-tab-post').forEach(initTabs);
scope.querySelectorAll('.aksrt-faq').forEach(initFaq);
scope.querySelectorAll('.aksrt-feature-intro').forEach(initFeatures);
scope.querySelectorAll('[data-count-up]').forEach(initCount);
scope.querySelectorAll('.widget-slider-graphic').forEach(measureSliderGraphic);
if (typeof window.VanillaTilt !== 'undefined') scope.querySelectorAll('[data-aksrt-tilt]').forEach(function (el) { if (once(el, 'Tilt')) window.VanillaTilt.init(el); });
}
function ready() {
scan(document);
if ('MutationObserver' in window) {
new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) { mutation.addedNodes.forEach(function (node) { if (node.nodeType === 1) scan(node); }); });
}).observe(document.body, { childList: true, subtree: true });
}
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ready);
else ready();
// AJAX 注入新内容后由 auto_fun 重扫(once() data 标记保证幂等)。
if (window.jQuery) window.jQuery(document).on('auto_fun', function () { scan(document); });
})();