Selaa lähdekoodia

改成新的样式

visuddhinanda 1 päivä sitten
vanhempi
sitoutus
17c9994c8c
1 muutettua tiedostoa jossa 173 lisäystä ja 59 poistoa
  1. 173 59
      api-v12/resources/js/term-tooltip.js

+ 173 - 59
api-v12/resources/js/term-tooltip.js

@@ -1,83 +1,197 @@
 /**
- * <span
-    class="term-ref"
-    data-id="b45e7b10-2b75-4f5f-ac63-be686116043c"
-    data-term="anicca"
->
-    无常
-</span>
+ * resources/js/term-tooltip.js
+ * WikiPāli 术语 tooltip(桌面 Popover)+ 抽屉(移动端 Offcanvas)
+ *
+ * 依赖:Bootstrap 5(bootstrap.Popover / bootstrap.Offcanvas)
+ *       已由 Tabler 全局引入,无需重复 import
  */
-document.addEventListener("DOMContentLoaded", () => {
-    const cache = {};
-
-    const drawerEl = document.getElementById("termDrawer");
-
-    const drawer = new bootstrap.Offcanvas(drawerEl);
 
-    const drawerTitle = document.getElementById("termDrawerTitle");
+(function () {
+    "use strict";
 
-    const drawerBody = document.getElementById("termDrawerBody");
+    // ── 缓存层 ────────────────────────────────────────────────────────
+    const cache = {};
 
-    function isMobile() {
-        return window.innerWidth < 768;
+    async function fetchTerm(id) {
+        if (cache[id]) return cache[id];
+        const res = await fetch(`/api/v2/terms/${id}`);
+        if (!res.ok) throw new Error(`fetchTerm ${id} failed: ${res.status}`);
+        const json = await res.json();
+        cache[id] = json.data;
+        return json.data;
     }
 
-    async function fetchTerm(term) {
-        if (cache[term]) return cache[term];
-        console.info("term", term);
-        const res = await fetch(`/api/v2/terms/${term}`);
-
-        const data = await res.json();
-
-        cache[term] = data.data;
-
-        return data.data;
+    // ── 设备判断 ──────────────────────────────────────────────────────
+    const isMobile = () => window.innerWidth < 768;
+
+    // ── Popover 内容模板 ──────────────────────────────────────────────
+    function buildPopoverContent(data) {
+        const meaning = (data.meaning || "").trim();
+        const summary = (data.summary || "").trim();
+        const showSummary = summary && summary !== meaning;
+
+        return `
+            <div class="wiki-term-card-word">${data.word || ""}</div>
+            <div class="wiki-term-card-body" style="padding: 10px 14px 12px;">
+                ${
+                    meaning
+                        ? `<div class="wiki-term-card-meaning">${meaning}</div>`
+                        : ""
+                }
+                ${
+                    showSummary
+                        ? `<div class="wiki-term-card-summary">${summary}</div>`
+                        : ""
+                }
+            </div>
+            <div><a>查看完整条目</a></div>
+        `;
     }
 
-    document.querySelectorAll(".term-ref").forEach((el) => {
-        let popover = null;
+    // ── 桌面端:Bootstrap Popover ─────────────────────────────────────
+    function initDesktopPopover(el, data) {
+        // 防止重复初始化
+        if (el._wikiPopover) return el._wikiPopover;
+
+        const popover = new bootstrap.Popover(el, {
+            trigger: "manual",
+            html: true,
+            placement: "bottom", // flip 会自动在空间不足时翻转为 top
+            fallbackPlacements: ["top", "bottom"],
+            customClass: "wiki-term-popover",
+            content: buildPopoverContent(data),
+            // 让 popover 自身也可以接收鼠标事件(解决移入气泡消失问题)
+            // Bootstrap 5.2+ 支持 sanitize: false 保留自定义 html
+            sanitize: false,
+        });
 
-        el.addEventListener("mouseenter", async () => {
-            console.info("mouseenter");
-            if (isMobile()) return;
+        el._wikiPopover = popover;
 
-            if (popover) return;
-            const pali = el.dataset.term;
-            const data = await fetchTerm(el.dataset.id);
+        let hideTimer = null;
 
-            popover = new bootstrap.Popover(el, {
-                trigger: "manual",
-                html: true,
-                placement: "bottom",
+        function scheduleHide() {
+            hideTimer = setTimeout(() => {
+                // 如果鼠标此刻在 popover 内,不关闭
+                const tipEl = document.querySelector(".wiki-term-popover.show");
+                if (tipEl && tipEl.matches(":hover")) return;
+                popover.hide();
+            }, 120);
+        }
 
-                content: `
-            <div style="max-width:300px">
-                <h4>${data.word}</h4>
-                <div>${data.summary}</div>
-            </div>
-        `,
-            });
+        function cancelHide() {
+            clearTimeout(hideTimer);
+        }
 
+        // 触发元素
+        el.addEventListener("mouseenter", () => {
+            cancelHide();
             popover.show();
         });
 
-        el.addEventListener("mouseleave", () => {
-            if (popover) {
-                popover.dispose();
-                popover = null;
-            }
+        el.addEventListener("mouseleave", scheduleHide);
+
+        // Popover 显示后,给气泡本身绑定 mouseenter / mouseleave
+        el.addEventListener("shown.bs.popover", () => {
+            const tipEl = document.querySelector(".wiki-term-popover.show");
+            if (!tipEl) return;
+            tipEl.addEventListener("mouseenter", cancelHide);
+            tipEl.addEventListener("mouseleave", scheduleHide);
         });
 
-        el.addEventListener("click", async () => {
-            if (!isMobile()) return;
+        return popover;
+    }
+
+    // ── 移动端:Bootstrap Offcanvas(全局共享一个) ────────────────────
+    let offcanvasInstance = null;
 
-            const data = await fetchTerm(el.dataset.id);
+    function getOffcanvas() {
+        if (offcanvasInstance) return offcanvasInstance;
+        const el = document.getElementById("wikiTermDrawer");
+        if (!el) return null;
+        offcanvasInstance = new bootstrap.Offcanvas(el, { scroll: false });
+        return offcanvasInstance;
+    }
 
-            drawerTitle.innerHTML = data.word;
+    function showMobileDrawer(data) {
+        const oc = getOffcanvas();
+        if (!oc) return;
+
+        const meaning = (data.meaning || "").trim();
+        const summary = (data.summary || "").trim();
+        const showSummary = summary && summary !== meaning;
+
+        document.getElementById("wikiTermCardSlot").innerHTML = `
+            <div class="wiki-term-card">
+                <div class="wiki-term-card-word">${data.word || ""}</div>
+                <div class="wiki-term-card-body">
+                    ${
+                        meaning
+                            ? `<div class="wiki-term-card-meaning">${meaning}</div>`
+                            : ""
+                    }
+                    ${
+                        showSummary
+                            ? `<div class="wiki-term-card-summary">${summary}</div>`
+                            : ""
+                    }
+                </div>
+            </div>
+        `;
 
-            drawerBody.innerHTML = data.summary;
+        document.getElementById("wikiTermDrawerLink").style.display = "none";
+        oc.show();
+    }
 
-            drawer.show();
+    // ── 入口:扫描所有 .term-ref ──────────────────────────────────────
+    function init() {
+        const refs = document.querySelectorAll(".term-ref[data-id]");
+        if (!refs.length) return;
+
+        refs.forEach((el) => {
+            // 桌面:mouseenter 时懒加载并初始化 Popover
+            el.addEventListener("mouseenter", async function onFirstEnter() {
+                if (isMobile()) return;
+
+                try {
+                    const data = await fetchTerm(el.dataset.id);
+                    const popover = initDesktopPopover(el, data);
+                    // 首次加载后直接 show(mouseenter 已触发)
+                    popover.show();
+                } catch (e) {
+                    console.warn(
+                        "[WikiPāli] term fetch failed",
+                        el.dataset.id,
+                        e
+                    );
+                }
+
+                // 移除首次监听,后续由 Popover 自身管理
+                el.removeEventListener("mouseenter", onFirstEnter);
+            });
+
+            // 移动端:点击触发抽屉
+            el.addEventListener("click", async (e) => {
+                if (!isMobile()) return;
+                e.preventDefault();
+
+                try {
+                    const data = await fetchTerm(el.dataset.id);
+                    showMobileDrawer(data);
+                } catch (err) {
+                    console.warn(
+                        "[WikiPāli] term fetch failed",
+                        el.dataset.id,
+                        err
+                    );
+                }
+            });
         });
-    });
-});
+    }
+
+    // DOM 就绪后执行
+    if (document.readyState === "loading") {
+        document.addEventListener("DOMContentLoaded", init);
+    } else {
+        init();
+    }
+})();