mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix(file-manager): restore text selection and copying in Markdown previews (#1518)
This commit is contained in:
1 parent
a1f72b9e53
commit
64b9b18fe6
2 files changed
+123
-68
No files matched your search
@@ -48,75 +48,87 @@ export function MarkdownRenderer({
|
|||||||
: "px-4 py-2 text-foreground";
|
: "px-4 py-2 text-foreground";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactMarkdown
|
<div
|
||||||
remarkPlugins={[remarkGfm]}
|
className="select-text"
|
||||||
components={{
|
tabIndex={0}
|
||||||
code({ className, children, ...props }) {
|
onKeyDown={(event) => {
|
||||||
const match = /language-(\w+)/.exec(className || "");
|
// Keep native text copying out of the file grid's global shortcuts.
|
||||||
// v9 removed the `inline` flag; a fenced block always carries a
|
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "c")
|
||||||
// language- class, an inline span never does.
|
event.stopPropagation();
|
||||||
const inline = !className;
|
|
||||||
return !inline && match ? (
|
|
||||||
<SyntaxHighlighter
|
|
||||||
style={syntaxTheme}
|
|
||||||
language={match[1]}
|
|
||||||
PreTag="div"
|
|
||||||
className="rounded-lg"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{String(children).replace(/\n$/, "")}
|
|
||||||
</SyntaxHighlighter>
|
|
||||||
) : (
|
|
||||||
<code
|
|
||||||
className="bg-muted px-1 py-0.5 rounded text-sm font-mono"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
h1: ({ children }) => <h1 className={h1Class}>{children}</h1>,
|
|
||||||
h2: ({ children }) => <h2 className={h2Class}>{children}</h2>,
|
|
||||||
h3: ({ children }) => <h3 className={h3Class}>{children}</h3>,
|
|
||||||
h4: ({ children }) => <h4 className={h4Class}>{children}</h4>,
|
|
||||||
p: ({ children }) => <p className={pClass}>{children}</p>,
|
|
||||||
ul: ({ children }) => (
|
|
||||||
<ul className={`${listClass} list-disc`}>{children}</ul>
|
|
||||||
),
|
|
||||||
ol: ({ children }) => (
|
|
||||||
<ol className={`${listClass} list-decimal`}>{children}</ol>
|
|
||||||
),
|
|
||||||
li: ({ children }) => (
|
|
||||||
<li className="mb-1 text-foreground">{children}</li>
|
|
||||||
),
|
|
||||||
blockquote: ({ children }) => (
|
|
||||||
<blockquote className={quoteClass}>{children}</blockquote>
|
|
||||||
),
|
|
||||||
table: ({ children }) => (
|
|
||||||
<div className={tableWrapClass}>
|
|
||||||
<table className={tableClass}>{children}</table>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
thead: ({ children }) => <thead className="bg-muted">{children}</thead>,
|
|
||||||
tbody: ({ children }) => <tbody>{children}</tbody>,
|
|
||||||
tr: ({ children }) => (
|
|
||||||
<tr className="border-b border-border">{children}</tr>
|
|
||||||
),
|
|
||||||
th: ({ children }) => <th className={thClass}>{children}</th>,
|
|
||||||
td: ({ children }) => <td className={tdClass}>{children}</td>,
|
|
||||||
a: ({ href, children }) => (
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:text-blue-800 underline"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{content}
|
<ReactMarkdown
|
||||||
</ReactMarkdown>
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
code({ className, children, ...props }) {
|
||||||
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
// v9 removed the `inline` flag; a fenced block always carries a
|
||||||
|
// language- class, an inline span never does.
|
||||||
|
const inline = !className;
|
||||||
|
return !inline && match ? (
|
||||||
|
<SyntaxHighlighter
|
||||||
|
style={syntaxTheme}
|
||||||
|
language={match[1]}
|
||||||
|
PreTag="div"
|
||||||
|
className="rounded-lg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{String(children).replace(/\n$/, "")}
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
) : (
|
||||||
|
<code
|
||||||
|
className="bg-muted px-1 py-0.5 rounded text-sm font-mono"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
h1: ({ children }) => <h1 className={h1Class}>{children}</h1>,
|
||||||
|
h2: ({ children }) => <h2 className={h2Class}>{children}</h2>,
|
||||||
|
h3: ({ children }) => <h3 className={h3Class}>{children}</h3>,
|
||||||
|
h4: ({ children }) => <h4 className={h4Class}>{children}</h4>,
|
||||||
|
p: ({ children }) => <p className={pClass}>{children}</p>,
|
||||||
|
ul: ({ children }) => (
|
||||||
|
<ul className={`${listClass} list-disc`}>{children}</ul>
|
||||||
|
),
|
||||||
|
ol: ({ children }) => (
|
||||||
|
<ol className={`${listClass} list-decimal`}>{children}</ol>
|
||||||
|
),
|
||||||
|
li: ({ children }) => (
|
||||||
|
<li className="mb-1 text-foreground">{children}</li>
|
||||||
|
),
|
||||||
|
blockquote: ({ children }) => (
|
||||||
|
<blockquote className={quoteClass}>{children}</blockquote>
|
||||||
|
),
|
||||||
|
table: ({ children }) => (
|
||||||
|
<div className={tableWrapClass}>
|
||||||
|
<table className={tableClass}>{children}</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ children }) => (
|
||||||
|
<thead className="bg-muted">{children}</thead>
|
||||||
|
),
|
||||||
|
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||||
|
tr: ({ children }) => (
|
||||||
|
<tr className="border-b border-border">{children}</tr>
|
||||||
|
),
|
||||||
|
th: ({ children }) => <th className={thClass}>{children}</th>,
|
||||||
|
td: ({ children }) => <td className={tdClass}>{children}</td>,
|
||||||
|
a: ({ href, children }) => (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-600 hover:text-blue-800 underline"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
|
import { MarkdownRenderer } from "../../../src/frontend/components/MarkdownRenderer";
|
||||||
|
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
|
describe("Markdown preview text copying", () => {
|
||||||
|
it.each([false, true])(
|
||||||
|
"supports copying in compact=%s previews",
|
||||||
|
(compact) => {
|
||||||
|
const fileShortcut = vi.fn();
|
||||||
|
document.addEventListener("keydown", fileShortcut);
|
||||||
|
try {
|
||||||
|
const { container } = render(
|
||||||
|
<div className="select-none">
|
||||||
|
<MarkdownRenderer
|
||||||
|
compact={compact}
|
||||||
|
content={"# Preview\n\nCopy this text."}
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
|
);
|
||||||
|
const preview = container.querySelector<HTMLElement>(".select-text")!;
|
||||||
|
preview.focus();
|
||||||
|
expect(preview).toHaveFocus();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("heading", { name: "Preview" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
for (const modifier of [{ ctrlKey: true }, { metaKey: true }]) {
|
||||||
|
expect(fireEvent.keyDown(preview, { key: "c", ...modifier })).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(fileShortcut).not.toHaveBeenCalled();
|
||||||
|
fireEvent.keyDown(preview, { key: "Escape" });
|
||||||
|
expect(fileShortcut).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fireEvent.contextMenu(preview)).toBe(true);
|
||||||
|
} finally {
|
||||||
|
document.removeEventListener("keydown", fileShortcut);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user