/** * Jio Article Tools — password toggles, 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 initArticleTools() { var buttons = document.querySelectorAll("[data-article-tool]"); if (!buttons.length) return; buttons.forEach(function (button) { if (button.dataset.jioArticleToolBound) return; button.dataset.jioArticleToolBound = "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 formatStatCount(count) { // 与 PHP jio_format_number() 同一套缩写:≥10000 → 1.2W+,≥1000 → 1.5K。 var safeCount = Math.max(0, count); if (safeCount >= 10000) { return Math.round((safeCount / 10000) * 10) / 10 + "W+"; } if (safeCount >= 1000) { return Math.round((safeCount / 1000) * 10) / 10 + "K"; } return String(safeCount); } 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 label = formatStatCount(count); document.querySelectorAll('[data-stat-count="' + statName + '"]').forEach(function (statEl) { statEl.textContent = label; }); } 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.jioData || !jioData.ajaxUrl || !jioData.nonce) { return Promise.reject(new Error("互动服务未配置")); } var body = new URLSearchParams(); body.set("action", actionName); body.set("nonce", jioData.nonce); body.set("postId", String(postId)); body.set("active", active ? "1" : "0"); return fetch(jioData.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 data = (payload && payload.data) || {}; var error = new Error(data.message || "请求失败"); // 参考主题:游客操作受限时直接拉起登录弹窗,而不是只弹一条错误提示。 error.needsLogin = !!data.login; throw error; } return payload.data || {}; }); } /** 游客被拦下时,复用站点的登录弹窗触发器。 */ function openLoginModal() { var trigger = document.querySelector(".signin-loader") || document.querySelector("[data-jio-auth-required]") || document.querySelector("#nav-login-btn"); if (trigger) { trigger.click(); } } /** * 参考主题的待发送态:计数被行内加载点替换,按钮不可重复提交。 * @returns {Function} 还原计数内容的函数 */ function showPending(button) { var counts = button.querySelectorAll("count"); var previous = []; for (var i = 0; i < counts.length; i++) { previous.push(counts[i].innerHTML); counts[i].innerHTML = ''; } button.disabled = true; return function restore() { for (var j = 0; j < counts.length; j++) { if (previous[j] === undefined) continue; counts[j].innerHTML = previous[j]; } }; } /** 参考主题只在点亮时播放 lovestar;先摘类再于下一帧加回,重复点赞也会重放。 */ function paintActive(button, isActive, animate) { button.dataset.initialActive = isActive ? "1" : "0"; button.classList.toggle("actived", isActive); button.classList.remove("action-animation"); if (!isActive || !animate) return; // 读取 offsetWidth 强制重排,使动画类移除→添加之间重新触发关键帧。 void button.offsetWidth; button.classList.add("action-animation"); } 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 applyState(buttons, statName, isActive, count, animate) { for (var i = 0; i < buttons.length; i++) paintActive(buttons[i], isActive, animate); if ("number" === typeof count && !isNaN(count)) { setBaseStatCount(statName, count); } else { updateStatCounts(statName, getBaseStatCount(statName)); } } /** * 参考主题的点击流程:立即还原计数 → 提交 → 按服务端结果重绘。 * 服务端返回未变化(例如访客重复记录)时不播放动画,只提示。 */ function bindToggle(buttons, statName, actionName, messages) { if (!buttons.length) return; if (buttons[0].dataset.jioInteractionBound) return; var seed = buttons[0]; applyState(buttons, statName, seed.dataset.initialActive === "1", getBaseStatCount(statName), false); buttons.forEach(function (button) { if (button.dataset.jioInteractionBound) return; button.dataset.jioInteractionBound = "1"; button.addEventListener("click", function () { var postId = button.dataset.postId ? String(button.dataset.postId) : ""; if (!postId || button.disabled) return; // 点击前的快照:用来判断服务端是否真的改变了状态。 var wasActive = button.dataset.initialActive === "1"; var previousCount = getBaseStatCount(statName); var restoreCount = showPending(button); requestInteraction(actionName, postId, !wasActive).then(function (data) { var active = !!data.active; var count = parseInt(data.count, 10); if (isNaN(count)) count = previousCount; restoreCount(); var unchanged = active === wasActive && count === previousCount; applyState(buttons, statName, active, count, !unchanged); if (unchanged) { notyf(messages.duplicate, "warning"); } else { notyf(active ? messages.added : messages.removed, active ? "success" : "warning"); } }).catch(function (error) { restoreCount(); notyf(error && error.message ? error.message : messages.failed, "danger"); if (error && error.needsLogin) openLoginModal(); }).finally(function () { button.disabled = false; }); }); }); } bindToggle(likeBtns, "likes", "jio_toggle_like", { added: "已赞!感谢您的支持", removed: "点赞已取消", duplicate: "已赞过此文章了!", failed: "点赞失败" }); bindToggle(favBtns, "favorites", "jio_toggle_favorite", { added: "已收藏此文章", removed: "已取消收藏", duplicate: "已经收藏过此文章了!", failed: "收藏失败" }); } function init(root) { 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.jio-article").on("auto_fun.jio-article", function () { init(); }); } })();