/** * AKSRT Runtime Infrastructure * * Unified core primitives for the entire frontend: * aksrt.tbquire(deps, cb) — lazy script/CSS loader * aksrt.confirm(msg, opts) — i18n confirm dialog * aksrt.scrollTopTo(target, off) — smart scroll with header offset * aksrt.loading(el, show) — element-level loading overlay * aksrt.route(url, title) — push/replace URL + title * * Declarative interactions (event delegation on body): * [data-close] — close target element * [data-onclick] — trigger click on another element * [data-confirm] — confirm before action * [data-action] — universal AJAX action button (like/follow/checkin) * [ajax-action] — general AJAX button with modal response * * @package AKSRT * @since 5.0.0 */ (function () { 'use strict'; var aksrt = window.aksrt || {}; window.aksrt = aksrt; // ══════════════════════════════════════════════════════════════ // §10.11 — tbquire: lazy script/CSS loader // ══════════════════════════════════════════════════════════════ var _loadedScripts = {}; var _loadingPromises = {}; function loadSingleScript(src) { if (_loadedScripts[src]) return Promise.resolve(); if (_loadingPromises[src]) return _loadingPromises[src]; _loadingPromises[src] = new Promise(function (resolve, reject) { var isCSS = /\.css(\?|$)/i.test(src); var el; if (isCSS) { el = document.createElement('link'); el.rel = 'stylesheet'; el.href = src; } else { el = document.createElement('script'); el.src = src; el.async = true; } el.onload = function () { _loadedScripts[src] = true; delete _loadingPromises[src]; resolve(); }; el.onerror = function () { delete _loadingPromises[src]; reject(new Error('Failed to load: ' + src)); }; (document.head || document.documentElement).appendChild(el); }); return _loadingPromises[src]; } aksrt.tbquire = function (deps, callback) { if (typeof deps === 'string') deps = [deps]; if (!Array.isArray(deps) || !deps.length) { if (typeof callback === 'function') callback(); return Promise.resolve(); } var resolvedDeps = deps.map(function (dep) { if (/^https?:\/\/|^\//.test(dep)) return dep; var map = (window.aksrtData && aksrtData.tbquireMap) || {}; return map[dep] || dep; }); var p = Promise.all(resolvedDeps.map(loadSingleScript)); if (typeof callback === 'function') { p.then(callback).catch(function (err) { console.error('[AKSRT tbquire]', err); }); } return p; }; // ══════════════════════════════════════════════════════════════ // §10.10 — confirm: i18n confirm dialog + [data-confirm] // ══════════════════════════════════════════════════════════════ var _i18n = (window.aksrtData && aksrtData.i18n) || {}; function __(key, fallback) { return _i18n[key] || fallback || key; } aksrt.__ = __; aksrt.confirm = function (msg, opts) { opts = opts || {}; return new Promise(function (resolve) { var overlay = document.createElement('div'); overlay.className = 'aksrt-confirm-overlay'; var dialog = document.createElement('div'); dialog.className = 'aksrt-confirm-dialog'; dialog.setAttribute('role', 'alertdialog'); dialog.setAttribute('aria-modal', 'true'); dialog.innerHTML = '
' + escapeHtml(msg) + '
' + '
' + '' + '' + '
'; overlay.appendChild(dialog); document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('is-open'); }); function close(result) { overlay.classList.remove('is-open'); setTimeout(function () { overlay.remove(); }, 200); resolve(result); } dialog.querySelector('.aksrt-confirm__cancel').addEventListener('click', function () { close(false); }); dialog.querySelector('.aksrt-confirm__ok').addEventListener('click', function () { close(true); }); overlay.addEventListener('click', function (e) { if (e.target === overlay) close(false); }); dialog.querySelector('.aksrt-confirm__ok').focus(); }); }; // ══════════════════════════════════════════════════════════════ // §10.10 — scrollTopTo: smart scroll with header offset // ══════════════════════════════════════════════════════════════ aksrt.scrollTopTo = function (target, extraOffset) { var top = 0; if (typeof target === 'number') { top = target; } else if (target && target.getBoundingClientRect) { top = target.getBoundingClientRect().top + window.pageYOffset; } var navFixed = document.querySelector('body.nav-fixed .header, .navbar.navbar-top'); var headerOffset = navFixed ? (navFixed.offsetHeight + 20) : 0; top = Math.max(0, top - headerOffset - (extraOffset || 0)); var current = window.pageYOffset; if (Math.abs(top - current) < 80) return; window.scrollTo({ top: top, behavior: 'smooth' }); }; // ══════════════════════════════════════════════════════════════ // §10.10 — loading: element-level loading overlay // ══════════════════════════════════════════════════════════════ aksrt.loading = function (el, show) { if (!el) return; if (show === false) { var mask = el.querySelector('.aksrt-loading-mask'); if (mask) { mask.classList.add('aksrt-loading-mask--out'); setTimeout(function () { mask.remove(); }, 300); } return; } if (el.querySelector('.aksrt-loading-mask')) return; var pos = getComputedStyle(el).position; if (pos === 'static') el.style.position = 'relative'; var mask = document.createElement('div'); mask.className = 'aksrt-loading-mask'; mask.innerHTML = '' + '' + '' + ''; el.appendChild(mask); }; // ══════════════════════════════════════════════════════════════ // §10.11 — route: tab route + popstate // ══════════════════════════════════════════════════════════════ aksrt.route = function (url, title) { if (!window.history || !history.pushState) return; history.pushState({ aksrt: true }, title || '', url); if (title) document.title = title; }; aksrt.routeReplace = function (url, title) { if (!window.history || !history.replaceState) return; history.replaceState({ aksrt: true }, title || '', url); if (title) document.title = title; }; // Tab route: elements with [data-route] push URL on activation window.addEventListener('popstate', function (e) { var routeTabs = document.querySelectorAll('[data-route]'); if (!routeTabs.length) return; var path = window.location.pathname + window.location.search; routeTabs.forEach(function (tab) { var tabRoute = tab.getAttribute('data-route'); var isMatch = path === tabRoute || path.indexOf(tabRoute) === 0; tab.classList.toggle('is-active', isMatch); var target = tab.getAttribute('data-route-target'); if (target) { var panel = document.querySelector(target); if (panel) panel.classList.toggle('is-active', isMatch); } }); }); // ══════════════════════════════════════════════════════════════ // §10.11 — Declarative interactions (event delegation) // ══════════════════════════════════════════════════════════════ document.addEventListener('click', function (e) { var target = e.target.closest('[data-close]'); if (target) { e.preventDefault(); var sel2 = target.getAttribute('data-close'); var el2 = sel2 ? document.querySelector(sel2) : target.parentElement; if (el2) { el2.classList.remove('is-open', 'is-active', 'is-visible'); el2.hidden = true; } return; } target = e.target.closest('[data-onclick]'); if (target) { e.preventDefault(); var sel3 = target.getAttribute('data-onclick'); var el3 = document.querySelector(sel3); if (el3) el3.click(); return; } }); // [data-confirm] — intercept submission document.addEventListener('click', function (e) { var target = e.target.closest('[data-confirm]'); if (!target) return; var msg = target.getAttribute('data-confirm'); if (!msg) return; e.preventDefault(); e.stopPropagation(); aksrt.confirm(msg).then(function (ok) { if (!ok) return; target.removeAttribute('data-confirm'); target.click(); target.setAttribute('data-confirm', msg); }); }, true); // ══════════════════════════════════════════════════════════════ // §10.11 — [data-action] / [ajax-action] universal buttons // ══════════════════════════════════════════════════════════════ // Only these data-action values are network AJAX. UI-only actions // (toggle-theme, editor toolbar, download, etc.) must not hit admin-ajax. var DATA_ACTION_AJAX = { like: true, favorite: true, follow: true, checkin: true }; document.addEventListener('click', function (e) { // [data-action] — like/follow/checkin with localStorage de-dup var btn = e.target.closest('[data-action]'); if (btn) { var dataAction = btn.getAttribute('data-action') || ''; if (!DATA_ACTION_AJAX[dataAction]) { // Leave non-AJAX data-action handlers (theme toggle, toolbar, download) alone. return; } e.preventDefault(); handleDataAction(btn); return; } // [ajax-action] — general AJAX button btn = e.target.closest('[ajax-action]'); if (btn) { e.preventDefault(); handleAjaxAction(btn); return; } }); function handleDataAction(btn) { if (btn.classList.contains('is-loading')) return; var action = btn.getAttribute('data-action'); if (!action || !DATA_ACTION_AJAX[action]) return; var id = btn.getAttribute('data-id') || ''; var storageKey = 'aksrt_action_' + action + '_' + id; if (action === 'like' || action === 'favorite') { try { if (localStorage.getItem(storageKey) && !btn.classList.contains('is-active')) { notyf(__('already_done', '已操作'), 'warning'); return; } } catch (ex) { /* storage unavailable */ } } btn.classList.add('is-loading'); var ajaxUrl = (window.aksrtData && aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php'; if (!(window.aksrtData && aksrtData.nonce)) { btn.classList.remove('is-loading'); notyf(__('action_failed', '操作失败'), 'danger'); return; } var params = new URLSearchParams(); params.set('action', 'aksrt_' + action); params.set('nonce', aksrtData.nonce); params.set('id', id); var extraData = btn.getAttribute('data-action-data'); if (extraData) { try { var ed = JSON.parse(extraData); Object.keys(ed).forEach(function (k) { params.set(k, ed[k]); }); } catch (ex) { /* ignore */ } } fetch(ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, credentials: 'same-origin', body: params.toString() }) .then(function (r) { return r.json(); }) .then(function (json) { btn.classList.remove('is-loading'); if (json && json.success) { var isActive = !!json.data && !!json.data.active; btn.classList.toggle('is-active', isActive); var countEl = btn.querySelector('[data-count]') || btn.querySelector('.action-count'); if (countEl && json.data && json.data.count !== undefined) { countEl.textContent = String(json.data.count); } try { if (isActive) localStorage.setItem(storageKey, '1'); else localStorage.removeItem(storageKey); } catch (ex) { /* */ } if (json.data && json.data.message) { notyf(json.data.message, 'success'); } if (json.data && json.data.goto) { window.location.href = json.data.goto; } } else { notyf((json.data && json.data.message) || __('action_failed', '操作失败'), 'danger'); } }) .catch(function (err) { btn.classList.remove('is-loading'); notyf(err.message || __('network_error', '网络错误'), 'danger'); }); } function handleAjaxAction(btn) { if (btn.classList.contains('is-loading')) return; btn.classList.add('is-loading'); var action = btn.getAttribute('ajax-action'); var ajaxUrl = (window.aksrtData && aksrtData.ajaxUrl) || '/wp-admin/admin-ajax.php'; var params = new URLSearchParams(); params.set('action', action); if (window.aksrtData && aksrtData.nonce) params.set('nonce', aksrtData.nonce); var form = btn.closest('form'); if (form) { new FormData(form).forEach(function (v, k) { params.set(k, v); }); } fetch(ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, credentials: 'same-origin', body: params.toString() }) .then(function (r) { return r.json(); }) .then(function (json) { btn.classList.remove('is-loading'); if (json && json.success) { if (json.data && json.data.show_modal) { window.AksrtModalRuntime.open({ html: json.data.show_modal, bottom: true, mobileBottom: true, kind: 'generic', }); } else if (json.data && json.data.message) { notyf(json.data.message, 'success'); } if (json.data && json.data.goto) { window.location.href = json.data.goto; } if (json.data && json.data.reload) { window.location.reload(); } } else { notyf((json.data && json.data.message) || __('action_failed', '操作失败'), 'danger'); } }) .catch(function (err) { btn.classList.remove('is-loading'); notyf(err.message || __('network_error', '网络错误'), 'danger'); }); } // ══════════════════════════════════════════════════════════════ // §10.11 — Ripple (broad coverage) // ══════════════════════════════════════════════════════════════ var RIPPLE_SELECTORS = '.toggle-radius,.but-ripple,.but,.btn,.btn-primary,.btn-ghost,.btn-outline,.item-thumbnail,.menu-item>a,.aksrt-ripple,.yiyan-box,.aksrt-card'; document.addEventListener('click', function (e) { var target = e.target.closest(RIPPLE_SELECTORS); if (!target) return; // Markup opt-out (header publish uses class="nowave") + nav CTA chips if (target.classList.contains('nowave') || target.closest('.nowave')) return; if (target.classList.contains('navbar-cta') || target.closest('.navbar-cta')) return; if (target.closest('.navbar-top .navbar-but, .navbar-top .navbar-vip, .navbar-but-cluster')) return; if (target.querySelector('.aksrt-wave')) return; var rect = target.getBoundingClientRect(); var size = Math.max(rect.width, rect.height) * 2; var x = e.clientX - rect.left - size / 2; var y = e.clientY - rect.top - size / 2; var wave = document.createElement('span'); wave.className = 'aksrt-wave'; // Must be absolute — without it the huge span expands the button box ("变长"). wave.style.cssText = 'position:absolute;pointer-events:none;border-radius:50%;background:rgba(255,255,255,0.35);width:' + size + 'px;height:' + size + 'px;left:' + x + 'px;top:' + y + 'px;transform:scale(0);animation:aksrtWaveRipple .6s ease-out forwards;'; var cs = getComputedStyle(target); if (cs.position === 'static') target.style.position = 'relative'; if (cs.overflow === 'visible') target.style.overflow = 'hidden'; target.appendChild(wave); setTimeout(function () { if (wave.parentNode) wave.parentNode.removeChild(wave); }, 600); }); // ══════════════════════════════════════════════════════════════ // §10.11 — auto_fun: extended rehydration coverage // ══════════════════════════════════════════════════════════════ aksrt.autoFun = function (root) { root = root || document; if (window.AksrtFrontend) { AksrtFrontend.rehydrate(root, 'ajax'); } // Tooltips root.querySelectorAll('[data-tooltip]').forEach(function (el) { if (el.dataset.tooltipBound) return; el.dataset.tooltipBound = '1'; el.addEventListener('mouseenter', function () { var tip = document.createElement('div'); tip.className = 'aksrt-tooltip'; tip.textContent = el.getAttribute('data-tooltip'); document.body.appendChild(tip); var rect = el.getBoundingClientRect(); tip.style.left = (rect.left + rect.width / 2 - tip.offsetWidth / 2) + 'px'; tip.style.top = (rect.top - tip.offsetHeight - 8 + window.pageYOffset) + 'px'; el._aksrtTooltip = tip; }); el.addEventListener('mouseleave', function () { if (el._aksrtTooltip) { el._aksrtTooltip.remove(); el._aksrtTooltip = null; } }); }); // Countdowns root.querySelectorAll('[data-countdown]').forEach(function (el) { if (el.dataset.countdownBound) return; el.dataset.countdownBound = '1'; var end = parseInt(el.getAttribute('data-countdown'), 10); if (!end) return; var iv = setInterval(function () { var left = Math.max(0, end - Math.floor(Date.now() / 1000)); if (left <= 0) { clearInterval(iv); el.textContent = '00:00'; return; } var m = Math.floor(left / 60); var s = left % 60; el.textContent = (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s; }, 1000); }); }; // ══════════════════════════════════════════════════════════════ // Helpers // ══════════════════════════════════════════════════════════════ function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // ── DOMContentLoaded ── function initRuntime() { aksrt.autoFun(document); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initRuntime); } else { initRuntime(); } })();