diff --git a/public/js/post-content.js b/public/js/post-content.js new file mode 100644 index 0000000..90bbb12 --- /dev/null +++ b/public/js/post-content.js @@ -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); + }; +})(); diff --git a/src/components/post/post_content.rs b/src/components/post/post_content.rs index 3703fd0..70dc2a5 100644 --- a/src/components/post/post_content.rs +++ b/src/components/post/post_content.rs @@ -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); - 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); - copybutton.add_event_listener_with_callback("click", copy_closure.as_ref().unchecked_ref()).unwrap(); - copy_closure.forget(); - - let _ = parent.append_child(©button); - } - } - } - } - - // 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::().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::().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); - 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); - 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); - 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); - document_clone.add_event_listener_with_callback("keydown", key_handler.as_ref().unchecked_ref()).unwrap(); - key_handler.forget(); - - }) as Box); - - 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! {