Files
wordpress-theme-aksrt/assets/js/core/article.js
T
kate522 5fd7c212ed fix(runtime): 按参考主题硬切换为 loader/main/auto_fun 单一 DOM 生命周期
- 删除 AksrtFrontend registry/rehydrate 双路架构及 build/public.js 拼包入口,
  前台唯一初始路径为 PHP 本地栈(jQuery 1.9.1 → Bootstrap 3.4.1)
  → loader_js(_win/aksrtData 唯一注入)→ tbquire(['main']) → runtime/main.min.js
- window.auto_fun = debounce(z_auto_fun, 100) 为唯一重扫入口;所有模块改为
  执行一次 + off/on('auto_fun.aksrt-*') 命名空间重扫,data/class 标记幂等
- 局部更新协议统一:HTML 注入 → post_ajax.ed → auto_fun();
  远程 modal → loaded.bs.modal → auto_fun();JSON 成功 → zib_ajax.success
- 灯箱恢复 Miniimgbox/imgbox 唯一 owner(imgbox-container、a[data-imgbox]、
  imgbox_data),删除 AksrtLightbox/data-aksrt-lightbox 第二实现及 CSS/overlay,
  _win.imgbox* 重新接上 post-display 设置
- 死代码清理:runtime 下 11 组被自有实现替代的旧模块副本、无消费 vendored 库、
  顶层 grade.min.js、revenue.js、无调用方的 aksrt_get_win_runtime_config(),
  以及 loader 中 16 条死 mapping
- PHP 入队改为本地栈 + loader_js,routed/widget/submit 依赖统一换绑;
  用户卡片发布按钮恢复 nav_user_newposts_btn 设置门控
- 测试对齐:新增 frontend-lifecycle-contract,重写 navigation/lazyload/toast/
  auth-source/comment/page-display/post-visual 等断言为目标架构契约

验证:node --test widget-system+navigation 223 通过 0 失败;
构建脚本重建 main/imgbox min 与 CSS bundles;改动 PHP php -l、JS node --check
全部通过;git diff --check 干净;credits 重入 harness 通过。
2026-09-03 12:44:31 +08:00

351 lines
12 KiB
JavaScript

/**
* AKSRT Article Tools — password toggles, search clear, article actions, interaction bar
*/
(function () {
"use strict";
function initPasswordToggles(root) {
(root || document).querySelectorAll(".toggle-pwd").forEach(function (btn) {
if (btn.dataset.pwdToggleBound) return;
btn.dataset.pwdToggleBound = "1";
btn.addEventListener("click", function () {
var id = btn.getAttribute("data-target");
var input = id ? document.getElementById(id) : null;
if (!input) return;
var show = input.getAttribute("type") === "password";
input.setAttribute("type", show ? "text" : "password");
btn.setAttribute("aria-label", show ? "隐藏密码" : "显示密码");
});
});
}
function initSearchClear() {
var btn = document.getElementById("search-clear-btn");
var input = document.getElementById("search-page-input");
if (btn && input && !btn.dataset.aksrtSearchClearBound) {
btn.dataset.aksrtSearchClearBound = "1";
var syncClearButton = function () {
var hasValue = input.value.trim().length > 0;
btn.classList.toggle("is-visible", hasValue);
btn.hidden = !hasValue;
};
btn.addEventListener("click", function () {
input.value = "";
input.focus();
syncClearButton();
});
input.addEventListener("input", syncClearButton);
syncClearButton();
}
var categorySelect = document.getElementById("search-category-filter");
if (categorySelect) {
categorySelect.addEventListener("change", function () {
var url = new URL(window.location.href);
if (this.value) {
url.searchParams.set("aksrt_category", this.value);
} else {
url.searchParams.delete("aksrt_category");
}
if (url.searchParams.get("aksrt_search_type") && url.searchParams.get("aksrt_search_type") !== "post") {
url.searchParams.delete("aksrt_category");
}
url.searchParams.delete("paged");
window.location.href = url.toString();
});
}
}
function initArticleTools() {
var buttons = document.querySelectorAll("[data-article-tool]");
if (!buttons.length) return;
buttons.forEach(function (button) {
if (button.dataset.aksrtArticleToolBound) return;
button.dataset.aksrtArticleToolBound = "1";
button.addEventListener("click", function () {
var tool = button.getAttribute("data-article-tool");
if (tool === "print") {
window.print();
return;
}
if (tool === "pdf") {
window.print();
notyf("请在打印对话框中选择“另存为 PDF”", "info");
}
});
});
}
var postMetaDocumentEventsBound = false;
function setPostMetaMenuState(menuRoot, open) {
var toggle = menuRoot.querySelector(".post-drop-meta__toggle");
var menu = menuRoot.querySelector(".post-drop-meta__menu");
if (!toggle || !menu) return;
menuRoot.classList.toggle("is-open", open);
toggle.setAttribute("aria-expanded", open ? "true" : "false");
menu.setAttribute("aria-hidden", open ? "false" : "true");
}
function closePostMetaMenus(except, restoreFocus) {
document.querySelectorAll(".post-drop-meta.is-open").forEach(function (menuRoot) {
if (menuRoot === except) return;
var toggle = menuRoot.querySelector(".post-drop-meta__toggle");
setPostMetaMenuState(menuRoot, false);
if (restoreFocus && toggle) toggle.focus();
});
}
function initPostMetaMenus(root) {
var scope = root || document;
var menuRoots = [];
if (scope.nodeType === 1 && scope.matches(".post-drop-meta")) {
menuRoots.push(scope);
}
scope.querySelectorAll(".post-drop-meta").forEach(function (menuRoot) {
menuRoots.push(menuRoot);
});
menuRoots.forEach(function (menuRoot) {
var toggle = menuRoot.querySelector(".post-drop-meta__toggle");
var menu = menuRoot.querySelector(".post-drop-meta__menu");
if (!toggle || !menu || toggle.dataset.postMetaMenuBound) return;
toggle.dataset.postMetaMenuBound = "1";
setPostMetaMenuState(menuRoot, false);
toggle.addEventListener("click", function () {
var open = !menuRoot.classList.contains("is-open");
closePostMetaMenus(menuRoot, false);
setPostMetaMenuState(menuRoot, open);
});
menu.addEventListener("click", function (event) {
if (event.target.closest(".post-drop-meta__item")) {
setPostMetaMenuState(menuRoot, false);
}
});
});
if (menuRoots.length && !postMetaDocumentEventsBound) {
postMetaDocumentEventsBound = true;
document.addEventListener("click", function (event) {
if (!event.target.closest(".post-drop-meta")) {
closePostMetaMenus(null, false);
}
});
document.addEventListener("keydown", function (event) {
if (event.key === "Escape") {
closePostMetaMenus(null, true);
}
});
}
}
function getBaseStatCount(statName) {
var statEl = document.querySelector('[data-stat-count="' + statName + '"][data-base-count]');
return statEl ? (parseInt(statEl.dataset.baseCount, 10) || 0) : 0;
}
function updateStatCounts(statName, count) {
var safeCount = Math.max(0, count);
document.querySelectorAll('[data-stat-count="' + statName + '"]').forEach(function (statEl) {
statEl.textContent = String(safeCount);
});
}
function setBaseStatCount(statName, count) {
var safeCount = Math.max(0, count);
document.querySelectorAll('[data-stat-count="' + statName + '"][data-base-count]').forEach(function (statEl) {
statEl.dataset.baseCount = String(safeCount);
});
updateStatCounts(statName, safeCount);
}
function requestInteraction(actionName, postId, active) {
if (!window.aksrtData || !aksrtData.ajaxUrl || !aksrtData.nonce) {
return Promise.reject(new Error("互动服务未配置"));
}
var body = new URLSearchParams();
body.set("action", actionName);
body.set("nonce", aksrtData.nonce);
body.set("postId", String(postId));
body.set("active", active ? "1" : "0");
return fetch(aksrtData.ajaxUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
credentials: "same-origin",
body: body.toString()
}).then(function (response) {
return response.json().catch(function () {
throw new Error("服务器返回无效响应");
});
}).then(function (payload) {
if (!payload || !payload.success) {
var message = payload && payload.data && payload.data.message ? payload.data.message : "请求失败";
throw new Error(message);
}
return payload.data || {};
});
}
function setButtonBusy(button, busy) {
button.disabled = !!busy;
button.classList.toggle("is-loading", !!busy);
}
function findInScope(scope, id) {
scope = scope || document;
if (!scope) return null;
if (scope.nodeType === 9) {
return scope.getElementById(id);
}
if (scope.nodeType === 1) {
if (scope.id === id) return scope;
return typeof scope.querySelector === "function"
? scope.querySelector("#" + id)
: null;
}
return null;
}
function collectActionButtons(scope, selector) {
scope = scope || document;
var list = [];
if (!scope) return list;
if (scope.nodeType === 1 && scope.matches && scope.matches(selector)) {
list.push(scope);
}
if (typeof scope.querySelectorAll === "function") {
var found = scope.querySelectorAll(selector);
for (var i = 0; i < found.length; i++) list.push(found[i]);
} else if (scope.nodeType === 9) {
var all = document.querySelectorAll(selector);
for (var j = 0; j < all.length; j++) list.push(all[j]);
}
// also keep legacy id hooks
var legacyId = selector.indexOf("action-like") >= 0 ? "action-like" : (selector.indexOf("action-favorite") >= 0 ? "action-favorite" : "");
if (legacyId) {
var legacy = findInScope(scope, legacyId);
if (legacy && list.indexOf(legacy) === -1) list.push(legacy);
}
return list;
}
function initInteractionBar(root) {
var scope = root || document;
// Bind all like/favorite controls (in-content + footer tabbar extend).
var likeBtns = collectActionButtons(scope, ".action-like[data-post-id]");
var favBtns = collectActionButtons(scope, ".action-favorite[data-post-id]");
function paintLike(btn, isActive) {
btn.dataset.initialActive = isActive ? "1" : "0";
btn.classList.toggle("is-active", isActive);
var icon = btn.querySelector("i");
if (icon) icon.className = isActive ? "fa fas fa-heart" : "fa fas fa-heart";
}
function paintFavorite(btn, isActive) {
btn.dataset.initialActive = isActive ? "1" : "0";
btn.classList.toggle("is-active", isActive);
var icon = btn.querySelector("i");
if (icon) icon.className = isActive ? "fa fas fa-star" : "fa fas fa-star";
}
function applyLikeState(isActive, count) {
for (var i = 0; i < likeBtns.length; i++) paintLike(likeBtns[i], isActive);
if ("number" === typeof count && !isNaN(count)) {
setBaseStatCount("likes", count);
} else {
updateStatCounts("likes", getBaseStatCount("likes"));
}
}
function applyFavoriteState(isActive, count) {
for (var i = 0; i < favBtns.length; i++) paintFavorite(favBtns[i], isActive);
if ("number" === typeof count && !isNaN(count)) {
setBaseStatCount("favorites", count);
} else {
updateStatCounts("favorites", getBaseStatCount("favorites"));
}
}
if (likeBtns.length) {
var seedLike = likeBtns[0];
if (!seedLike.dataset.aksrtInteractionBound) {
applyLikeState(seedLike.dataset.initialActive === "1", getBaseStatCount("likes"));
likeBtns.forEach(function (likeBtn) {
if (likeBtn.dataset.aksrtInteractionBound) return;
likeBtn.dataset.aksrtInteractionBound = "1";
likeBtn.addEventListener("click", function () {
var postId = likeBtn.dataset.postId ? String(likeBtn.dataset.postId) : "";
if (!postId || likeBtn.disabled) return;
var nextState = likeBtn.dataset.initialActive !== "1";
setButtonBusy(likeBtn, true);
requestInteraction("aksrt_toggle_like", postId, nextState).then(function (data) {
applyLikeState(!!data.active, parseInt(data.count, 10) || 0);
notyf(data.active ? "感谢你的点赞" : "已取消点赞", "success");
}).catch(function (error) {
notyf(error && error.message ? error.message : "点赞失败", "danger");
}).finally(function () {
setButtonBusy(likeBtn, false);
});
});
});
}
}
if (favBtns.length) {
var seedFav = favBtns[0];
if (!seedFav.dataset.aksrtInteractionBound) {
applyFavoriteState(seedFav.dataset.initialActive === "1", getBaseStatCount("favorites"));
favBtns.forEach(function (favBtn) {
if (favBtn.dataset.aksrtInteractionBound) return;
favBtn.dataset.aksrtInteractionBound = "1";
favBtn.addEventListener("click", function () {
var postId = favBtn.dataset.postId ? String(favBtn.dataset.postId) : "";
if (!postId || favBtn.disabled) return;
var nextState = favBtn.dataset.initialActive !== "1";
setButtonBusy(favBtn, true);
requestInteraction("aksrt_toggle_favorite", postId, nextState).then(function (data) {
applyFavoriteState(!!data.active, parseInt(data.count, 10) || 0);
notyf(data.active ? "收藏成功" : "已取消收藏", "success");
}).catch(function (error) {
notyf(error && error.message ? error.message : "收藏失败", "danger");
}).finally(function () {
setButtonBusy(favBtn, false);
});
});
});
}
}
}
function init(root) {
initSearchClear();
initArticleTools();
initPostMetaMenus(root);
initPasswordToggles(root);
initInteractionBar(root);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () { init(); });
} else {
init();
}
// AJAX 注入新内容后由 auto_fun 重扫(各 init 均带 data 标记,重复调用幂等)。
if (window.jQuery) {
window.jQuery(document).off("auto_fun.aksrt-article").on("auto_fun.aksrt-article", function () { init(); });
}
})();