diff --git a/libs/yggdrasil-core/src/index.ts b/libs/yggdrasil-core/src/index.ts index 0ef5cdb..cf8c324 100644 --- a/libs/yggdrasil-core/src/index.ts +++ b/libs/yggdrasil-core/src/index.ts @@ -1,11 +1,11 @@ +import { initPostContent } from './post-content'; + declare global { interface Window { __initPostContent: (selector: string) => void; } } -window.__initPostContent = (_selector: string): void => { - // Task 2 实现 -}; +window.__initPostContent = initPostContent; export {}; diff --git a/libs/yggdrasil-core/src/post-content.test.ts b/libs/yggdrasil-core/src/post-content.test.ts new file mode 100644 index 0000000..38daa4b --- /dev/null +++ b/libs/yggdrasil-core/src/post-content.test.ts @@ -0,0 +1,73 @@ +/** + * post-content 测试:钉住代码块 copy 按钮的生成与复制行为。 + * 黑盒驱动:只通过 window.__initPostContent 入口。 + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import './index'; + +describe('initPostContent', () => { + beforeEach(() => { + document.body.innerHTML = ''; + vi.restoreAllMocks(); + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + document.body.innerHTML = ''; + }); + + it('为 pre>code 注入 .copy-code 按钮', () => { + const root = document.createElement('div'); + root.className = 'post-content'; + root.innerHTML = '
console.log(1)';
+ document.body.appendChild(root);
+
+ window.__initPostContent('.post-content');
+
+ const btn = root.querySelector('pre .copy-code');
+ expect(btn).not.toBeNull();
+ expect(btn?.textContent).toBe('copy');
+ });
+
+ it('已有 .copy-code 的 pre 不重复注入', () => {
+ const root = document.createElement('div');
+ root.className = 'post-content';
+ root.innerHTML =
+ 'x';
+ document.body.appendChild(root);
+
+ window.__initPostContent('.post-content');
+
+ const btns = root.querySelectorAll('pre .copy-code');
+ expect(btns.length).toBe(1);
+ });
+
+ it('点击按钮调用 clipboard.writeText 并回显 copied!', () => {
+ const writeText = vi.fn().mockResolvedValue(undefined);
+ Object.defineProperty(navigator, 'clipboard', {
+ value: { writeText },
+ configurable: true,
+ });
+
+ const root = document.createElement('div');
+ root.className = 'post-content';
+ root.innerHTML = 'hello';
+ document.body.appendChild(root);
+
+ window.__initPostContent('.post-content');
+
+ const btn = root.querySelector('.copy-code') as HTMLButtonElement;
+ btn.click();
+
+ expect(writeText).toHaveBeenCalledWith('hello');
+ expect(btn.textContent).toBe('copied!');
+
+ // 2 秒后还原回 'copy'
+ vi.advanceTimersByTime(2000);
+ expect(btn.textContent).toBe('copy');
+ });
+
+ it('selector 未命中时不报错', () => {
+ expect(() => window.__initPostContent('.not-exist')).not.toThrow();
+ });
+});
diff --git a/libs/yggdrasil-core/src/post-content.ts b/libs/yggdrasil-core/src/post-content.ts
new file mode 100644
index 0000000..4e15c29
--- /dev/null
+++ b/libs/yggdrasil-core/src/post-content.ts
@@ -0,0 +1,48 @@
+/**
+ * 代码块 copy 按钮。
+ *
+ * 为 pre>code 注入一个 .copy-code 按钮,点击复制代码文本并回显 copied!。
+ * 2 秒后还原文案。样式见 input.css 的 .copy-code 规则(全局 Tailwind 构建)。
+ */
+
+function initCopyButtons(root: Element): void {
+ const blocks = root.querySelectorAll('pre > code');
+ blocks.forEach((code) => {
+ const pre = code.parentElement;
+ if (!pre) return;
+ if (pre.querySelector('.copy-code')) return;
+
+ const btn = document.createElement('button');
+ btn.className = 'copy-code';
+ btn.textContent = 'copy';
+ btn.setAttribute('aria-label', 'Copy code');
+
+ const codeText = code.textContent || '';
+ btn.addEventListener('click', () => {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(codeText);
+ } else {
+ const ta = document.createElement('textarea');
+ ta.value = codeText;
+ ta.style.position = 'fixed';
+ ta.style.opacity = '0';
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand('copy');
+ document.body.removeChild(ta);
+ }
+ btn.textContent = 'copied!';
+ setTimeout(() => {
+ btn.textContent = 'copy';
+ }, 2000);
+ });
+
+ pre.appendChild(btn);
+ });
+}
+
+export function initPostContent(selector: string): void {
+ const root = document.querySelector(selector);
+ if (!root) return;
+ initCopyButtons(root);
+}
diff --git a/public/js/post-content.js b/public/js/post-content.js
deleted file mode 100644
index c392afe..0000000
--- a/public/js/post-content.js
+++ /dev/null
@@ -1,48 +0,0 @@
-(function () {
- "use strict";
-
- function initCopyButtons(root) {
- var blocks = root.querySelectorAll("pre > code");
- for (var i = 0; i < blocks.length; i++) {
- var code = blocks[i];
- var pre = code.parentElement;
- if (!pre) continue;
- if (pre.querySelector(".copy-code")) continue;
-
- var btn = document.createElement("button");
- btn.className = "copy-code";
- btn.textContent = "copy";
- btn.setAttribute("aria-label", "Copy code");
-
- (function (codeText) {
- btn.addEventListener("click", function () {
- var self = this;
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(codeText);
- } else {
- var ta = document.createElement("textarea");
- ta.value = codeText;
- ta.style.position = "fixed";
- ta.style.opacity = "0";
- document.body.appendChild(ta);
- ta.select();
- document.execCommand("copy");
- document.body.removeChild(ta);
- }
- self.textContent = "copied!";
- setTimeout(function () {
- self.textContent = "copy";
- }, 2000);
- });
- })(code.textContent || "");
-
- pre.appendChild(btn);
- }
- }
-
- window.__initPostContent = function (selector) {
- var root = document.querySelector(selector);
- if (!root) return;
- initCopyButtons(root);
- };
-})();
diff --git a/src/components/post/post_content.rs b/src/components/post/post_content.rs
index 157f9e4..cfc7145 100644
--- a/src/components/post/post_content.rs
+++ b/src/components/post/post_content.rs
@@ -10,14 +10,14 @@ use dioxus::prelude::*;
/// - `content_html`:服务端渲染的文章 HTML 字符串
///
/// 关键行为:
-/// - 在 `target_arch = "wasm32"` 环境下执行 `post-content.js`(代码块复制)。
+/// - 在 `target_arch = "wasm32"` 环境下调用 `window.__initPostContent` 初始化代码块
+/// 复制按钮(`yggdrasil-core.js` 已由 `Dioxus.toml` 全局注入)。
/// 灯箱(图片灯箱 + 懒加载)改由 `Dioxus.toml` 全局注入 `lightbox.js`,
/// 这里仅设置其初始化配置 `__lightboxSelectors` 并兜底调用。
#[component]
pub fn PostContent(content_html: String) -> Element {
#[cfg(target_arch = "wasm32")]
use_effect(move || {
- let _ = js_sys::eval(include_str!("../../../public/js/post-content.js"));
let _ = js_sys::eval("window.__initPostContent('.post-content')");
// lightbox 改由 Dioxus.toml 全局