mirror of https://github.com/usememos/memos.git
feat(web): enhance code blocks with copy button and fix link navigation
Add custom code block renderer with language display and copy functionality. Links now open in new windows, and clicking image links no longer triggers both link navigation and image preview. - Add CodeBlock component with copy-to-clipboard button and language label - Configure all markdown links to open in new windows with target="_blank" - Fix image link behavior to prevent duplicate actions when clicked 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b00df8a9d1
commit
d693142dd4
|
|
@ -0,0 +1,55 @@
|
|||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface PreProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CodeBlock = ({ children, className, ...props }: PreProps) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Extract the code element and its props
|
||||
const codeElement = children as React.ReactElement;
|
||||
const codeClassName = codeElement?.props?.className || "";
|
||||
const codeContent = String(codeElement?.props?.children || "").replace(/\n$/, "");
|
||||
|
||||
// Extract language from className (format: language-xxx)
|
||||
const match = /language-(\w+)/.exec(codeClassName);
|
||||
const language = match ? match[1] : "";
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(codeContent);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy code:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<pre className="relative group">
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<span className="text-[10px] font-medium text-muted-foreground/60 uppercase tracking-wider select-none">{language}</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
"p-1.5 rounded-md transition-all",
|
||||
"hover:bg-accent/50",
|
||||
"focus:outline-none focus:ring-1 focus:ring-ring",
|
||||
copied ? "text-primary" : "text-muted-foreground",
|
||||
)}
|
||||
aria-label={copied ? "Copied" : "Copy code"}
|
||||
title={copied ? "Copied!" : "Copy code"}
|
||||
>
|
||||
{copied ? <CheckIcon className="w-3.5 h-3.5" /> : <CopyIcon className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
<div className={className} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
</pre>
|
||||
);
|
||||
};
|
||||
|
|
@ -10,6 +10,7 @@ import { useTranslate } from "@/utils/i18n";
|
|||
import { remarkPreserveType } from "@/utils/remark-plugins/remark-preserve-type";
|
||||
import { remarkTag } from "@/utils/remark-plugins/remark-tag";
|
||||
import { isSuperUser } from "@/utils/user";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
import { createConditionalComponent, isTagNode, isTaskListItemNode } from "./ConditionalComponent";
|
||||
import { MemoContentContext } from "./MemoContentContext";
|
||||
import { Tag } from "./Tag";
|
||||
|
|
@ -102,6 +103,12 @@ const MemoContent = observer((props: Props) => {
|
|||
// Conditionally render custom components based on AST node type
|
||||
input: createConditionalComponent(TaskListItem, "input", isTaskListItemNode),
|
||||
span: createConditionalComponent(Tag, "span", isTagNode),
|
||||
pre: CodeBlock,
|
||||
a: ({ href, children, ...props }) => (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer" {...props}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,13 @@ const MemoView: React.FC<Props> = observer((props: Props) => {
|
|||
const targetEl = e.target as HTMLElement;
|
||||
|
||||
if (targetEl.tagName === "IMG") {
|
||||
// Check if the image is inside a link
|
||||
const linkElement = targetEl.closest("a");
|
||||
if (linkElement) {
|
||||
// If image is inside a link, only navigate to the link (don't show preview)
|
||||
return;
|
||||
}
|
||||
|
||||
const imgUrl = targetEl.getAttribute("src");
|
||||
if (imgUrl) {
|
||||
setPreviewImage({ open: true, urls: [imgUrl], index: 0 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue