Browse Source

add buildSkeletonContent

visuddhinanda 6 days ago
parent
commit
48969c6b99
1 changed files with 73 additions and 43 deletions
  1. 73 43
      api-v12/resources/js/term-tooltip.js

+ 73 - 43
api-v12/resources/js/term-tooltip.js

@@ -1,10 +1,7 @@
 /**
  * resources/js/term-tooltip.js
- * WikiPāli 术语 tooltip(桌面 Popover)+ 抽屉(移动端 Offcanvas)
- *
- * 依赖:Bootstrap 5(bootstrap.Popover / bootstrap.Offcanvas)
- *       已由 Tabler 全局引入,无需重复 import
  */
+import * as bootstrap from "bootstrap";
 
 (function () {
     "use strict";
@@ -24,6 +21,17 @@
     // ── 设备判断 ──────────────────────────────────────────────────────
     const isMobile = () => window.innerWidth < 768;
 
+    // ── Skeleton 模板 ─────────────────────────────────────────────────
+    function buildSkeletonContent() {
+        return `
+            <div class="wiki-term-skeleton">
+                <div class="wiki-term-skeleton-word"></div>
+                <div class="wiki-term-skeleton-line"></div>
+                <div class="wiki-term-skeleton-line short"></div>
+            </div>
+        `;
+    }
+
     // ── Popover 内容模板 ──────────────────────────────────────────────
     function buildPopoverContent(data) {
         const meaning = (data.meaning || "").trim();
@@ -49,19 +57,16 @@
     }
 
     // ── 桌面端:Bootstrap Popover ─────────────────────────────────────
-    function initDesktopPopover(el, data) {
-        // 防止重复初始化
+    function initDesktopPopover(el, content) {
         if (el._wikiPopover) return el._wikiPopover;
 
         const popover = new bootstrap.Popover(el, {
             trigger: "manual",
             html: true,
-            placement: "bottom", // flip 会自动在空间不足时翻转为 top
+            placement: "bottom",
             fallbackPlacements: ["top", "bottom"],
             customClass: "wiki-term-popover",
-            content: buildPopoverContent(data),
-            // 让 popover 自身也可以接收鼠标事件(解决移入气泡消失问题)
-            // Bootstrap 5.2+ 支持 sanitize: false 保留自定义 html
+            content: content,
             sanitize: false,
         });
 
@@ -71,7 +76,6 @@
 
         function scheduleHide() {
             hideTimer = setTimeout(() => {
-                // 如果鼠标此刻在 popover 内,不关闭
                 const tipEl = document.querySelector(".wiki-term-popover.show");
                 if (tipEl && tipEl.matches(":hover")) return;
                 popover.hide();
@@ -82,7 +86,6 @@
             clearTimeout(hideTimer);
         }
 
-        // 触发元素
         el.addEventListener("mouseenter", () => {
             cancelHide();
             popover.show();
@@ -90,7 +93,6 @@
 
         el.addEventListener("mouseleave", scheduleHide);
 
-        // Popover 显示后,给气泡本身绑定 mouseenter / mouseleave
         el.addEventListener("shown.bs.popover", () => {
             const tipEl = document.querySelector(".wiki-term-popover.show");
             if (!tipEl) return;
@@ -101,7 +103,20 @@
         return popover;
     }
 
-    // ── 移动端:Bootstrap Offcanvas(全局共享一个) ────────────────────
+    function updateDesktopPopover(el, data) {
+        const popover = el._wikiPopover;
+        if (!popover) return;
+        // 更新内容
+        const tip = document.querySelector(".wiki-term-popover.show");
+        if (tip) {
+            tip.querySelector(".popover-body").innerHTML =
+                buildPopoverContent(data);
+        }
+        // 同步 popover 内部 config,供下次 show 使用
+        popover._config.content = buildPopoverContent(data);
+    }
+
+    // ── 移动端:Bootstrap Offcanvas ───────────────────────────────────
     let offcanvasInstance = null;
 
     function getOffcanvas() {
@@ -109,37 +124,49 @@
         const el = document.getElementById("wikiTermDrawer");
         if (!el) return null;
         offcanvasInstance = new bootstrap.Offcanvas(el, { scroll: false });
+
+        el.addEventListener("hidden.bs.offcanvas", () => {
+            document
+                .querySelectorAll(".offcanvas-backdrop")
+                .forEach((b) => b.remove());
+            document.body.classList.remove("modal-open");
+            document.body.style.removeProperty("overflow");
+            document.body.style.removeProperty("padding-right");
+        });
+
         return offcanvasInstance;
     }
 
-    function showMobileDrawer(data) {
+    function showMobileDrawerSkeleton() {
         const oc = getOffcanvas();
         if (!oc) return;
 
+        document.getElementById("wikiTermDrawerWord").innerHTML =
+            '<div class="wiki-term-skeleton-word"></div>';
+        document.getElementById("wikiTermDrawerMeaning").innerHTML =
+            '<div class="wiki-term-skeleton-line short"></div>';
+        document.getElementById(
+            "wikiTermCardSlot"
+        ).innerHTML = `<div class="wiki-term-skeleton">
+                <div class="wiki-term-skeleton-line"></div>
+                <div class="wiki-term-skeleton-line short"></div>
+            </div>`;
+        document.getElementById("wikiTermDrawerLink").style.display = "none";
+
+        oc.show();
+    }
+
+    function fillMobileDrawer(data) {
         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>
-        `;
-
-        document.getElementById("wikiTermDrawerLink").style.display = "none";
-        oc.show();
+        document.getElementById("wikiTermDrawerWord").textContent =
+            data.word || "";
+        document.getElementById("wikiTermDrawerMeaning").textContent = meaning;
+        document.getElementById("wikiTermCardSlot").innerHTML = showSummary
+            ? `<div class="wiki-term-card-summary">${summary}</div>`
+            : "";
     }
 
     // ── 入口:扫描所有 .term-ref ──────────────────────────────────────
@@ -148,35 +175,39 @@
         if (!refs.length) return;
 
         refs.forEach((el) => {
-            // 桌面:mouseenter 时懒加载并初始化 Popover
+            // 桌面:mouseenter 时先显示 skeleton,数据回来后更新
             el.addEventListener("mouseenter", async function onFirstEnter() {
                 if (isMobile()) return;
 
+                // 立即显示 skeleton popover
+                const popover = initDesktopPopover(el, buildSkeletonContent());
+                popover.show();
+
                 try {
                     const data = await fetchTerm(el.dataset.id);
-                    const popover = initDesktopPopover(el, data);
-                    // 首次加载后直接 show(mouseenter 已触发)
-                    popover.show();
+                    updateDesktopPopover(el, data);
                 } catch (e) {
                     console.warn(
                         "[WikiPāli] term fetch failed",
                         el.dataset.id,
                         e
                     );
+                    popover.hide();
                 }
 
-                // 移除首次监听,后续由 Popover 自身管理
                 el.removeEventListener("mouseenter", onFirstEnter);
             });
 
-            // 移动端:点击触发抽屉
+            // 移动端:点击立即弹出 skeleton,数据回来后填充
             el.addEventListener("click", async (e) => {
                 if (!isMobile()) return;
                 e.preventDefault();
 
+                showMobileDrawerSkeleton();
+
                 try {
                     const data = await fetchTerm(el.dataset.id);
-                    showMobileDrawer(data);
+                    fillMobileDrawer(data);
                 } catch (err) {
                     console.warn(
                         "[WikiPāli] term fetch failed",
@@ -188,7 +219,6 @@
         });
     }
 
-    // DOM 就绪后执行
     if (document.readyState === "loading") {
         document.addEventListener("DOMContentLoaded", init);
     } else {