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 {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__initPostContent: (selector: string) => void;
|
__initPostContent: (selector: string) => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.__initPostContent = (_selector: string): void => {
|
window.__initPostContent = initPostContent;
|
||||||
// Task 2 实现
|
|
||||||
};
|
|
||||||
|
|
||||||
export {};
|
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 字符串
|
/// - `content_html`:服务端渲染的文章 HTML 字符串
|
||||||
///
|
///
|
||||||
/// 关键行为:
|
/// 关键行为:
|
||||||
/// - 在 `target_arch = "wasm32"` 环境下执行 `post-content.js`(代码块复制)。
|
/// - 在 `target_arch = "wasm32"` 环境下调用 `window.__initPostContent` 初始化代码块
|
||||||
|
/// 复制按钮(`yggdrasil-core.js` 已由 `Dioxus.toml` 全局注入)。
|
||||||
/// 灯箱(图片灯箱 + 懒加载)改由 `Dioxus.toml` 全局注入 `lightbox.js`,
|
/// 灯箱(图片灯箱 + 懒加载)改由 `Dioxus.toml` 全局注入 `lightbox.js`,
|
||||||
/// 这里仅设置其初始化配置 `__lightboxSelectors` 并兜底调用。
|
/// 这里仅设置其初始化配置 `__lightboxSelectors` 并兜底调用。
|
||||||
#[component]
|
#[component]
|
||||||
pub fn PostContent(content_html: String) -> Element {
|
pub fn PostContent(content_html: String) -> Element {
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use_effect(move || {
|
use_effect(move || {
|
||||||
let _ = js_sys::eval(include_str!("../../../public/js/post-content.js"));
|
|
||||||
let _ = js_sys::eval("window.__initPostContent('.post-content')");
|
let _ = js_sys::eval("window.__initPostContent('.post-content')");
|
||||||
// lightbox 改由 Dioxus.toml 全局 <script src> 加载(不再 include_str!)。
|
// lightbox 改由 Dioxus.toml 全局 <script src> 加载(不再 include_str!)。
|
||||||
// 双保险契约:先设配置,若 lightbox.js 已加载则立即调用;
|
// 双保险契约:先设配置,若 lightbox.js 已加载则立即调用;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user