refactor: extract post content JS into standalone script

This commit is contained in:
xfy 2026-06-09 13:18:40 +08:00
parent 0345fa70f2
commit 44c1358da5
2 changed files with 120 additions and 170 deletions

118
public/js/post-content.js Normal file
View File

@ -0,0 +1,118 @@
(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);
}
}
function closeLightbox(overlay) {
if (overlay && overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
document.body.style.overflow = "";
}
}
function initImageZoom(root) {
var images = root.querySelectorAll("img");
for (var i = 0; i < images.length; i++) {
var img = images[i];
if (img.getAttribute("data-zoom-enabled")) continue;
var src = img.getAttribute("src") || img.src || "";
if (src.indexOf("data:") === 0) continue;
img.setAttribute("data-zoom-enabled", "true");
var originalSrc = img.src;
var sep = originalSrc.indexOf("?") !== -1 ? "&" : "?";
img.src = originalSrc + sep + "w=800";
img.classList.add("md-content-img-zoomable");
(function (origSrc, altText) {
img.addEventListener("click", function (e) {
e.preventDefault();
var overlay = document.createElement("div");
overlay.className = "md-image-lightbox-overlay";
var container = document.createElement("div");
container.className = "md-image-lightbox-content";
var fullImg = document.createElement("img");
fullImg.src = origSrc;
fullImg.alt = altText;
var closeBtn = document.createElement("button");
closeBtn.className = "md-image-lightbox-close";
closeBtn.textContent = "\u2715";
container.appendChild(fullImg);
container.appendChild(closeBtn);
overlay.appendChild(container);
document.body.appendChild(overlay);
document.body.style.overflow = "hidden";
var onKey = function (ev) {
if (ev.key === "Escape") {
cleanup(overlay, onKey);
}
};
var cleanup = function (ol, kh) {
closeLightbox(ol);
document.removeEventListener("keydown", kh);
};
overlay.addEventListener("click", function () {
cleanup(overlay, onKey);
});
container.addEventListener("click", function (ev) {
ev.stopPropagation();
});
closeBtn.addEventListener("click", function () {
cleanup(overlay, onKey);
});
document.addEventListener("keydown", onKey);
});
})(originalSrc, img.alt || "");
}
}
window.__initPostContent = function (selector) {
var root = document.querySelector(selector);
if (!root) return;
initCopyButtons(root);
initImageZoom(root);
};
})();

View File

@ -4,176 +4,8 @@ use dioxus::prelude::*;
pub fn PostContent(content_html: String) -> Element {
#[cfg(target_arch = "wasm32")]
use_effect(move || {
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
// Add copy buttons to all code blocks
if let Ok(elements) = document.query_selector_all("pre > code") {
for i in 0..elements.length() {
if let Some(codeblock) = elements.item(i) {
if let Some(parent) = codeblock.parent_element() {
// Check if button already exists
if parent.query_selector(".copy-code").ok().flatten().is_some() {
continue;
}
let copybutton = document.create_element("button").unwrap();
copybutton.set_class_name("copy-code");
copybutton.set_text_content(Some("copy"));
copybutton.set_attribute("aria-label", "Copy code").unwrap();
let code_text = codeblock.text_content().unwrap_or_default();
let btn = copybutton.clone();
let copy_closure = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| {
let has_clipboard = web_sys::window()
.map(|w| {
let clip = js_sys::Reflect::get(&w.navigator(), &js_sys::JsString::from("clipboard")).unwrap_or(wasm_bindgen::JsValue::UNDEFINED);
!clip.is_undefined()
})
.unwrap_or(false);
if has_clipboard {
let _ = js_sys::eval(&format!(
"navigator.clipboard.writeText({})",
serde_json::to_string(&code_text).unwrap_or_default()
));
} else {
let _ = js_sys::eval(&format!(
"((t)=>{{const e=document.createElement('textarea');e.value=t;e.style.position='fixed';e.style.opacity='0';document.body.appendChild(e);e.select();document.execCommand('copy');document.body.removeChild(e)}})({})",
serde_json::to_string(&code_text).unwrap_or_default()
));
}
btn.set_text_content(Some("copied!"));
let btn_clone = btn.clone();
let reset_closure = Closure::wrap(Box::new(move || {
btn_clone.set_text_content(Some("copy"));
}) as Box<dyn FnMut()>);
let _ = web_sys::window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(
reset_closure.as_ref().unchecked_ref(),
2000,
);
reset_closure.forget();
}) as Box<dyn FnMut(_)>);
copybutton.add_event_listener_with_callback("click", copy_closure.as_ref().unchecked_ref()).unwrap();
copy_closure.forget();
let _ = parent.append_child(&copybutton);
}
}
}
}
// Add click-to-zoom for images in post content
if let Ok(images) = document.query_selector_all(".md-content img") {
for i in 0..images.length() {
if let Some(img) = images.item(i) {
let img_element = img.clone().dyn_into::<web_sys::HtmlImageElement>().unwrap();
// Skip if already processed
if img_element.get_attribute("data-zoom-enabled").is_some() {
continue;
}
img_element.set_attribute("data-zoom-enabled", "true").unwrap();
let document_clone = document.clone();
let original_src = img_element.src();
let alt = img_element.alt();
if original_src.starts_with("data:") {
continue;
}
// Replace src with thumbnail version (add ?w=800)
let thumb_src = if original_src.contains('?') {
format!("{}&w=800", original_src)
} else {
format!("{}?w=800", original_src)
};
img_element.set_src(&thumb_src);
img_element.set_class_name("md-content-img-zoomable");
let closure = Closure::wrap(Box::new(move |_event: web_sys::MouseEvent| {
// Create overlay
let overlay = document_clone.create_element("div").unwrap();
overlay.set_class_name("md-image-lightbox-overlay");
// Create image container
let container = document_clone.create_element("div").unwrap();
container.set_class_name("md-image-lightbox-content");
// Create full-size image
let full_img = document_clone.create_element("img").unwrap();
let full_img_el = full_img.dyn_ref::<web_sys::HtmlImageElement>().unwrap();
full_img_el.set_src(&original_src);
full_img_el.set_alt(&alt);
// Create close button
let close_btn = document_clone.create_element("button").unwrap();
close_btn.set_class_name("md-image-lightbox-close");
close_btn.set_text_content(Some(""));
// Assemble
let _ = container.append_child(&full_img);
let _ = container.append_child(&close_btn);
let _ = overlay.append_child(&container);
let _ = document_clone.body().unwrap().append_child(&overlay);
// Prevent body scroll
document_clone.body().unwrap().set_attribute("style", "overflow: hidden;").unwrap();
// Click overlay background to close
let overlay_for_bg = overlay.clone();
let close_bg = Closure::wrap(Box::new(move |_evt: web_sys::MouseEvent| {
if let Some(parent) = overlay_for_bg.parent_node() {
let _ = parent.remove_child(&overlay_for_bg);
}
}) as Box<dyn FnMut(_)>);
overlay.add_event_listener_with_callback("click", close_bg.as_ref().unchecked_ref()).unwrap();
close_bg.forget();
// Click container stops propagation (so clicking image doesn't close)
let stop_prop = Closure::wrap(Box::new(move |evt: web_sys::MouseEvent| {
evt.stop_propagation();
}) as Box<dyn FnMut(_)>);
container.add_event_listener_with_callback("click", stop_prop.as_ref().unchecked_ref()).unwrap();
stop_prop.forget();
// Click close button to close
let overlay_for_btn = overlay.clone();
let close_btn_handler = Closure::wrap(Box::new(move |_evt: web_sys::MouseEvent| {
if let Some(parent) = overlay_for_btn.parent_node() {
let _ = parent.remove_child(&overlay_for_btn);
}
}) as Box<dyn FnMut(_)>);
close_btn.add_event_listener_with_callback("click", close_btn_handler.as_ref().unchecked_ref()).unwrap();
close_btn_handler.forget();
// Escape key to close
let overlay_for_key = overlay.clone();
let key_handler = Closure::wrap(Box::new(move |evt: web_sys::KeyboardEvent| {
if evt.key() == "Escape" {
if let Some(parent) = overlay_for_key.parent_node() {
let _ = parent.remove_child(&overlay_for_key);
}
}
}) as Box<dyn FnMut(_)>);
document_clone.add_event_listener_with_callback("keydown", key_handler.as_ref().unchecked_ref()).unwrap();
key_handler.forget();
}) as Box<dyn FnMut(_)>);
img.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref()).unwrap();
closure.forget();
}
}
}
}
}
let _ = js_sys::eval(include_str!("../../../public/js/post-content.js"));
let _ = js_sys::eval("window.__initPostContent('.post-content')");
});
rsx! {