refactor(core): 迁移 post-content 到 yggdrasil-core,删除 public/js
逐行 TypeScript 化(逻辑不变),改走 window.__initPostContent 全局入口。 post-content.js 已迁移进 libs/yggdrasil-core/,public/js/ 目录随之删除。
This commit is contained in:
parent
afabdf4655
commit
ac1f92d816
@ -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 {};
|
||||
|
||||
73
libs/yggdrasil-core/src/post-content.test.ts
Normal file
73
libs/yggdrasil-core/src/post-content.test.ts
Normal file
@ -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 = '<pre><code>console.log(1)</code></pre>';
|
||||
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 =
|
||||
'<pre><code>x</code><button class="copy-code">copy</button></pre>';
|
||||
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 = '<pre><code>hello</code></pre>';
|
||||
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();
|
||||
});
|
||||
});
|
||||
48
libs/yggdrasil-core/src/post-content.ts
Normal file
48
libs/yggdrasil-core/src/post-content.ts
Normal file
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
})();
|
||||
@ -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 全局 <script src> 加载(不再 include_str!)。
|
||||
// 双保险契约:先设配置,若 lightbox.js 已加载则立即调用;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user