Files
wordpress-theme-aksrt/assets/js/aksrt-runtime.js
T
kate522 d35f0b6730 fix(frontend): 解绑 vendor main.min.js 的 body 级 ajax 委托,根除 admin-ajax 400 "0"
main.min.js 在 body 上委托 [data-action]/[ajax-action]/.wp-ajax-submit/
.initiate-checkin/.oauth-untying 的点击并向 admin-ajax 发 zibll 原生 action
(comment_like/like/follow_user…),AKSRT 无对应处理器,WP 一律返回 400 "0",
即控制台「ajax请求错误」的来源;评论点赞(data-action=comment_like)、文章
点赞/收藏(data-action=like|favorite)、VIP 购买等均会触发,且与 §10.11 自有
处理重复。main.min.js 由 loader.js 异步加载,解绑以幂等 .off 每 200ms
重试至多 50 次覆盖加载窗口。vendor 运行时仅保留 UI 行为。
2026-08-22 13:46:06 +08:00

574 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 =
'<div class="aksrt-confirm__body">' + escapeHtml(msg) + '</div>' +
'<div class="aksrt-confirm__actions">' +
'<button type="button" class="aksrt-confirm__cancel btn-ghost">' +
escapeHtml(opts.cancelText || __('cancel', '取消')) +
'</button>' +
'<button type="button" class="aksrt-confirm__ok btn-primary">' +
escapeHtml(opts.okText || __('confirm', '确定')) +
'</button>' +
'</div>';
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 = '<svg class="aksrt-loading-mask__spinner" width="36" height="36" viewBox="0 0 36 36">' +
'<circle cx="18" cy="18" r="15" fill="none" stroke="currentColor" stroke-width="3" stroke-dasharray="80 40" stroke-linecap="round">' +
'<animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="0.8s" repeatCount="indefinite"/>' +
'</circle></svg>';
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.10a — 解绑 vendor main.min.js 的 ajax 委托
// ══════════════════════════════════════════════════════════════
// main.min.js(loader.js 异步加载)在 body 上挂了 zib_ajax 系点击委托:
// [data-action] / [ajax-action] / .wp-ajax-submit / .initiate-checkin / .oauth-untying
// 它们向 admin-ajax 发 zibll 的 action(comment_like、like、follow_user…),
// AKSRT 没有这些处理器 → WP 返回 400 "0"(控制台「ajax请求错误」),
// 且与下方 §10.11 自有处理重复。vendor 运行时只保留 UI 行为,ajax 委托统一摘除。
// main.min.js 异步加载,.off 需在其绑定后执行——幂等重试覆盖加载窗口。
function unbindVendorAjaxDelegates() {
if (!window.jQuery) return;
try {
var $body = jQuery('body');
$body.off('click', '[data-action]');
$body.off('click', '[ajax-action]');
$body.off('click', '.wp-ajax-submit,[zibajax="submit"]');
$body.off('click', '.initiate-checkin');
$body.off('click', '.oauth-untying');
} catch (e) { /* vendor 运行时未就绪时忽略 */ }
}
var unbindVendorAjaxLoop = function () {
unbindVendorAjaxDelegates();
if (++unbindVendorAjaxLoop.tries >= 50) return; // ~10s @200ms 覆盖异步加载窗口
setTimeout(unbindVendorAjaxLoop, 200);
};
unbindVendorAjaxLoop.tries = 0;
unbindVendorAjaxLoop();
// ══════════════════════════════════════════════════════════════
// §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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ── DOMContentLoaded ──
function initRuntime() {
aksrt.autoFun(document);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initRuntime);
} else {
initRuntime();
}
})();