882 lines
33 KiB
JavaScript
882 lines
33 KiB
JavaScript
/**
|
|
* AKSRT Runtime Infrastructure
|
|
*
|
|
* Unified core primitives for the entire frontend:
|
|
* aksrt.tbquire(deps, cb) — lazy script/CSS loader
|
|
* aksrt.notyf(msg, opts) — unified toast (same-ID update)
|
|
* 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.Drawer(opts) — mobile fullscreen drawer + route
|
|
* aksrt.route(url, title) — push/replace URL + title
|
|
*
|
|
* Declarative interactions (event delegation on body):
|
|
* [data-toggle-class] — toggle class on target
|
|
* [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
|
|
*
|
|
* Body scroll direction bus:
|
|
* body.scroll-down / body.scroll-up (threshold 100px)
|
|
*
|
|
* @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 — notyf: unified toast with same-ID update
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
var _notyfContainer = null;
|
|
var _notyfMap = {};
|
|
var NOTYF_DURATION = 5000;
|
|
|
|
function getNotyfContainer() {
|
|
if (_notyfContainer && _notyfContainer.parentNode) return _notyfContainer;
|
|
_notyfContainer = document.createElement('div');
|
|
_notyfContainer.className = 'aksrt-notyf';
|
|
_notyfContainer.setAttribute('role', 'alert');
|
|
_notyfContainer.setAttribute('aria-live', 'polite');
|
|
document.body.appendChild(_notyfContainer);
|
|
return _notyfContainer;
|
|
}
|
|
|
|
function createNotyfItem(msg, opts) {
|
|
var el = document.createElement('div');
|
|
el.className = 'aksrt-notyf__item aksrt-notyf--' + (opts.type || 'info');
|
|
if (opts.type === 'load') el.classList.add('aksrt-notyf--loading');
|
|
|
|
var iconMap = {
|
|
success: '<i class="fas fa-check-circle"></i>',
|
|
danger: '<i class="fas fa-exclamation-circle"></i>',
|
|
error: '<i class="fas fa-exclamation-circle"></i>',
|
|
warning: '<i class="fas fa-exclamation-triangle"></i>',
|
|
load: '<i class="fas fa-spinner fa-spin"></i>',
|
|
info: '<i class="fas fa-info-circle"></i>'
|
|
};
|
|
|
|
el.innerHTML =
|
|
'<span class="aksrt-notyf__icon">' + (iconMap[opts.type] || iconMap.info) + '</span>' +
|
|
'<span class="aksrt-notyf__text">' + escapeHtml(msg) + '</span>' +
|
|
'<button type="button" class="aksrt-notyf__close" aria-label="Close">×</button>';
|
|
|
|
el.querySelector('.aksrt-notyf__close').addEventListener('click', function () {
|
|
removeNotyf(el, opts.id);
|
|
});
|
|
|
|
return el;
|
|
}
|
|
|
|
function removeNotyf(el, id) {
|
|
el.classList.add('aksrt-notyf--out');
|
|
setTimeout(function () {
|
|
if (el.parentNode) el.parentNode.removeChild(el);
|
|
}, 300);
|
|
if (id && _notyfMap[id]) {
|
|
clearTimeout(_notyfMap[id].timer);
|
|
delete _notyfMap[id];
|
|
}
|
|
}
|
|
|
|
aksrt.notyf = function (msg, opts) {
|
|
if (typeof opts === 'string') opts = { type: opts };
|
|
opts = opts || {};
|
|
var type = opts.type || 'info';
|
|
var duration = type === 'load' ? 0 : (opts.duration || NOTYF_DURATION);
|
|
var id = opts.id || null;
|
|
|
|
var container = getNotyfContainer();
|
|
|
|
if (id && _notyfMap[id]) {
|
|
var existing = _notyfMap[id];
|
|
clearTimeout(existing.timer);
|
|
existing.el.className = 'aksrt-notyf__item aksrt-notyf--' + type;
|
|
existing.el.querySelector('.aksrt-notyf__text').textContent = msg;
|
|
var iconSpan = existing.el.querySelector('.aksrt-notyf__icon');
|
|
var iconMap2 = {
|
|
success: '<i class="fas fa-check-circle"></i>',
|
|
danger: '<i class="fas fa-exclamation-circle"></i>',
|
|
error: '<i class="fas fa-exclamation-circle"></i>',
|
|
warning: '<i class="fas fa-exclamation-triangle"></i>',
|
|
load: '<i class="fas fa-spinner fa-spin"></i>',
|
|
info: '<i class="fas fa-info-circle"></i>'
|
|
};
|
|
iconSpan.innerHTML = iconMap2[type] || iconMap2.info;
|
|
if (duration > 0) {
|
|
existing.timer = setTimeout(function () { removeNotyf(existing.el, id); }, duration);
|
|
}
|
|
return existing.el;
|
|
}
|
|
|
|
var el = createNotyfItem(msg, { type: type, id: id });
|
|
container.appendChild(el);
|
|
|
|
requestAnimationFrame(function () {
|
|
el.classList.add('aksrt-notyf--in');
|
|
});
|
|
|
|
var timer = null;
|
|
if (duration > 0) {
|
|
timer = setTimeout(function () { removeNotyf(el, id); }, duration);
|
|
}
|
|
|
|
if (id) {
|
|
_notyfMap[id] = { el: el, timer: timer };
|
|
}
|
|
|
|
return el;
|
|
};
|
|
|
|
aksrt.notyf.close = function (id) {
|
|
if (_notyfMap[id]) removeNotyf(_notyfMap[id].el, id);
|
|
};
|
|
|
|
// Backward compat: wire window.aksrtToast to notyf
|
|
window.aksrtToast = function (msg, type) {
|
|
aksrt.notyf(msg, { type: type || 'info', duration: 2200 });
|
|
};
|
|
// Alias used by several modules that call aksrt.toast(...)
|
|
aksrt.toast = function (msg, type) {
|
|
return aksrt.notyf(msg, typeof type === 'string' ? { type: type, duration: 2200 } : (type || { duration: 2200 }));
|
|
};
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// §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 — Drawer: mobile fullscreen slide-in + route
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
aksrt.Drawer = function (opts) {
|
|
opts = opts || {};
|
|
var drawerEl = document.createElement('div');
|
|
drawerEl.className = 'aksrt-drawer' + (opts.class ? ' ' + opts.class : '');
|
|
|
|
drawerEl.innerHTML =
|
|
'<div class="aksrt-drawer__header">' +
|
|
'<button type="button" class="aksrt-drawer__back"><i class="fas fa-arrow-left"></i></button>' +
|
|
'<span class="aksrt-drawer__title">' + escapeHtml(opts.title || '') + '</span>' +
|
|
'</div>' +
|
|
'<div class="aksrt-drawer__body"></div>';
|
|
|
|
var body = drawerEl.querySelector('.aksrt-drawer__body');
|
|
|
|
if (opts.content) {
|
|
body.innerHTML = opts.content;
|
|
} else if (opts.remote) {
|
|
aksrt.loading(body, true);
|
|
fetch(opts.remote, { credentials: 'same-origin' })
|
|
.then(function (r) { return r.text(); })
|
|
.then(function (html) {
|
|
aksrt.loading(body, false);
|
|
body.innerHTML = html;
|
|
if (window.AksrtFrontend) AksrtFrontend.rehydrate(body, 'drawer');
|
|
})
|
|
.catch(function () { aksrt.loading(body, false); });
|
|
}
|
|
|
|
document.body.appendChild(drawerEl);
|
|
document.body.classList.add('drawer-open');
|
|
|
|
requestAnimationFrame(function () { drawerEl.classList.add('is-open'); });
|
|
|
|
if (opts.route) {
|
|
aksrt.route(opts.route, opts.title);
|
|
}
|
|
|
|
function close() {
|
|
drawerEl.classList.remove('is-open');
|
|
document.body.classList.remove('drawer-open');
|
|
setTimeout(function () { drawerEl.remove(); }, 300);
|
|
if (opts.route) {
|
|
window.history.back();
|
|
}
|
|
if (typeof opts.onClose === 'function') opts.onClose();
|
|
}
|
|
|
|
drawerEl.querySelector('.aksrt-drawer__back').addEventListener('click', close);
|
|
|
|
// minitouch right-swipe close
|
|
if (window.aksrt.minitouch) {
|
|
aksrt.minitouch(drawerEl, {
|
|
direction: 'right',
|
|
onEnd: close
|
|
});
|
|
}
|
|
|
|
return { el: drawerEl, body: body, close: close };
|
|
};
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// §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.2 — Unified body scroll direction bus
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
var _lastScrollTop = 0;
|
|
var _scrollTicking = false;
|
|
var SCROLL_BUS_THRESHOLD = 100;
|
|
|
|
function updateScrollBus() {
|
|
var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
var body = document.body;
|
|
|
|
if (scrollTop > SCROLL_BUS_THRESHOLD) {
|
|
body.classList.add('body-scroll');
|
|
} else {
|
|
body.classList.remove('body-scroll');
|
|
}
|
|
|
|
if (scrollTop > _lastScrollTop + 10) {
|
|
body.classList.add('scroll-down');
|
|
body.classList.remove('scroll-up');
|
|
} else if (scrollTop < _lastScrollTop - 10) {
|
|
body.classList.remove('scroll-down');
|
|
body.classList.add('scroll-up');
|
|
}
|
|
|
|
_lastScrollTop = Math.max(0, scrollTop);
|
|
_scrollTicking = false;
|
|
}
|
|
|
|
window.addEventListener('scroll', function () {
|
|
if (!_scrollTicking) {
|
|
_scrollTicking = true;
|
|
requestAnimationFrame(updateScrollBus);
|
|
}
|
|
}, { passive: true });
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// §10.11 — Declarative interactions (event delegation)
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
document.addEventListener('click', function (e) {
|
|
var target = e.target.closest('[data-toggle-class]');
|
|
if (target) {
|
|
e.preventDefault();
|
|
var cls = target.getAttribute('data-toggle-class');
|
|
var sel = target.getAttribute('data-target') || target.getAttribute('data-toggle-target');
|
|
var el = sel ? document.querySelector(sel) : target;
|
|
if (el && cls) el.classList.toggle(cls);
|
|
return;
|
|
}
|
|
|
|
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')) {
|
|
aksrt.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');
|
|
aksrt.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) {
|
|
aksrt.notyf(json.data.message, 'success');
|
|
}
|
|
|
|
if (json.data && json.data.goto) {
|
|
window.location.href = json.data.goto;
|
|
}
|
|
} else {
|
|
aksrt.notyf((json.data && json.data.message) || __('action_failed', '操作失败'), 'danger');
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
btn.classList.remove('is-loading');
|
|
aksrt.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) {
|
|
aksrt.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 {
|
|
aksrt.notyf((json.data && json.data.message) || __('action_failed', '操作失败'), 'danger');
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
btn.classList.remove('is-loading');
|
|
aksrt.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; }
|
|
});
|
|
});
|
|
|
|
// Lazy images
|
|
root.querySelectorAll('img.aksrt-lazy[data-src]').forEach(function (img) {
|
|
if (img.classList.contains('aksrt-loaded')) return;
|
|
if ('IntersectionObserver' in window) {
|
|
var obs = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (!entry.isIntersecting) return;
|
|
img.src = img.dataset.src;
|
|
img.classList.remove('aksrt-lazy');
|
|
img.classList.add('aksrt-loaded');
|
|
obs.unobserve(img);
|
|
});
|
|
}, { rootMargin: '200px' });
|
|
obs.observe(img);
|
|
} else {
|
|
img.src = img.dataset.src;
|
|
img.classList.remove('aksrt-lazy');
|
|
img.classList.add('aksrt-loaded');
|
|
}
|
|
});
|
|
|
|
// Swiper re-init
|
|
if (typeof Swiper !== 'undefined') {
|
|
root.querySelectorAll('[data-swiper-config]:not(.swiper-initialized)').forEach(function (el) {
|
|
try {
|
|
var cfg = JSON.parse(el.dataset.swiperConfig || '{}');
|
|
new Swiper(el, cfg);
|
|
} catch (ex) { /* */ }
|
|
});
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
};
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// §10.11 — minitouch: general direction gesture
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
aksrt.minitouch = function (el, opts) {
|
|
opts = opts || {};
|
|
var direction = opts.direction || 'any';
|
|
var startX = 0, startY = 0, currentX = 0, currentY = 0;
|
|
var active = false;
|
|
|
|
el.addEventListener('touchstart', function (e) {
|
|
if (e.touches.length !== 1) return;
|
|
startX = e.touches[0].clientX;
|
|
startY = e.touches[0].clientY;
|
|
currentX = startX;
|
|
currentY = startY;
|
|
active = true;
|
|
if (typeof opts.onStart === 'function') {
|
|
var stop = opts.onStart({ x: startX, y: startY, el: el });
|
|
if (stop === false) active = false;
|
|
}
|
|
}, { passive: true });
|
|
|
|
el.addEventListener('touchmove', function (e) {
|
|
if (!active) return;
|
|
currentX = e.touches[0].clientX;
|
|
currentY = e.touches[0].clientY;
|
|
var dx = currentX - startX;
|
|
var dy = currentY - startY;
|
|
var dist = Math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (dist < 10) return;
|
|
|
|
var angle = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
var dir = getDirection(angle);
|
|
|
|
if (direction !== 'any' && dir !== direction) {
|
|
if (typeof opts.stop === 'function' && opts.stop({ direction: dir, dx: dx, dy: dy })) {
|
|
active = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (typeof opts.onIng === 'function') {
|
|
opts.onIng({ dx: dx, dy: dy, direction: dir, distance: dist, el: el });
|
|
}
|
|
}, { passive: true });
|
|
|
|
el.addEventListener('touchend', function () {
|
|
if (!active) return;
|
|
active = false;
|
|
var dx = currentX - startX;
|
|
var dy = currentY - startY;
|
|
var dist = Math.sqrt(dx * dx + dy * dy);
|
|
var angle = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
var dir = getDirection(angle);
|
|
|
|
if (typeof opts.onEnd === 'function') {
|
|
opts.onEnd({ dx: dx, dy: dy, direction: dir, distance: dist, el: el });
|
|
}
|
|
}, { passive: true });
|
|
};
|
|
|
|
function getDirection(angle) {
|
|
if (angle >= -45 && angle < 45) return 'right';
|
|
if (angle >= 45 && angle < 135) return 'bottom';
|
|
if (angle >= -135 && angle < -45) return 'top';
|
|
return 'left';
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// Helpers
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
// ── DOMContentLoaded ──
|
|
|
|
function initRuntime() {
|
|
updateScrollBus();
|
|
aksrt.autoFun(document);
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initRuntime);
|
|
} else {
|
|
initRuntime();
|
|
}
|
|
|
|
})();
|
|
|