Archived
Add copy buttons to markdown code blocks
This commit is contained in:
@@ -9,8 +9,55 @@ export class FormattedText extends LitElement {
|
|||||||
@property() text = "";
|
@property() text = "";
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`<div class="formatted">${unsafeHTML(toSafeMarkdownHtml(this.text))}</div>`;
|
return html`<div class="formatted" @click=${this.onFormattedClick}>${unsafeHTML(toSafeMarkdownHtml(this.text))}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
override updated(): void {
|
||||||
|
this.enhanceCodeBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enhanceCodeBlocks(): void {
|
||||||
|
this.renderRoot.querySelectorAll("pre").forEach((element) => {
|
||||||
|
if (!(element instanceof HTMLPreElement) || element.parentElement?.classList.contains("code-block-wrapper") === true) return;
|
||||||
|
const code = element.querySelector("code");
|
||||||
|
if (!(code instanceof HTMLElement)) return;
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "code-block-wrapper";
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "code-copy-button";
|
||||||
|
button.textContent = "Copy";
|
||||||
|
button.setAttribute("aria-label", "Copy code block");
|
||||||
|
element.before(wrapper);
|
||||||
|
wrapper.append(element, button);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly onFormattedClick = (event: MouseEvent): void => {
|
||||||
|
if (!(event.target instanceof HTMLButtonElement) || !event.target.classList.contains("code-copy-button")) return;
|
||||||
|
const wrapper = event.target.closest(".code-block-wrapper");
|
||||||
|
if (!(wrapper instanceof HTMLElement)) return;
|
||||||
|
const code = wrapper.querySelector("pre code");
|
||||||
|
if (!(code instanceof HTMLElement)) return;
|
||||||
|
void this.copyCode(code.textContent, event.target);
|
||||||
|
};
|
||||||
|
|
||||||
|
private async copyCode(text: string, button: HTMLButtonElement): Promise<void> {
|
||||||
|
const ok = await writeClipboard(text);
|
||||||
|
button.textContent = ok ? "Copied" : "Failed";
|
||||||
|
window.setTimeout(() => {
|
||||||
|
button.textContent = "Copy";
|
||||||
|
}, 1200);
|
||||||
}
|
}
|
||||||
|
|
||||||
static override styles = formattedTextStyles;
|
static override styles = formattedTextStyles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function writeClipboard(text: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -162,13 +162,17 @@ export const chatStyles = css`
|
|||||||
export const formattedTextStyles = css`
|
export const formattedTextStyles = css`
|
||||||
:host { display: block; }
|
:host { display: block; }
|
||||||
.formatted { white-space: normal; overflow-wrap: anywhere; line-height: 1.45; }
|
.formatted { white-space: normal; overflow-wrap: anywhere; line-height: 1.45; }
|
||||||
p, ul, ol, pre, blockquote, table { margin: 0 0 10px; }
|
p, ul, ol, pre, blockquote, table, .code-block-wrapper { margin: 0 0 10px; }
|
||||||
:is(p, ul, ol, pre, blockquote, table):last-child { margin-bottom: 0; }
|
:is(p, ul, ol, pre, blockquote, table, .code-block-wrapper):last-child { margin-bottom: 0; }
|
||||||
ul, ol { padding-left: 22px; }
|
ul, ol { padding-left: 22px; }
|
||||||
li + li { margin-top: 3px; }
|
li + li { margin-top: 3px; }
|
||||||
code { border: 1px solid #30363d; border-radius: 4px; background: #0d1117; padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
code { border: 1px solid #30363d; border-radius: 4px; background: #0d1117; padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||||
|
.code-block-wrapper { position: relative; }
|
||||||
|
.code-block-wrapper pre { margin: 0; padding-right: 58px; }
|
||||||
pre { border: 1px solid #30363d; border-radius: 8px; background: #0d1117; padding: 10px; overflow: auto; }
|
pre { border: 1px solid #30363d; border-radius: 8px; background: #0d1117; padding: 10px; overflow: auto; }
|
||||||
pre code { border: 0; padding: 0; background: transparent; }
|
pre code { border: 0; padding: 0; background: transparent; }
|
||||||
|
.code-copy-button { position: absolute; top: 6px; right: 6px; z-index: 1; border: 1px solid #30363d; border-radius: 6px; background: #161b22; color: #8b949e; padding: 3px 7px; font: 12px system-ui, sans-serif; cursor: pointer; }
|
||||||
|
.code-copy-button:hover { color: #e6edf3; border-color: #58a6ff; }
|
||||||
blockquote { border-left: 3px solid #30363d; padding-left: 10px; color: #8b949e; }
|
blockquote { border-left: 3px solid #30363d; padding-left: 10px; color: #8b949e; }
|
||||||
a { color: #58a6ff; }
|
a { color: #58a6ff; }
|
||||||
h1, h2, h3, h4 { margin: 14px 0 8px; line-height: 1.2; }
|
h1, h2, h3, h4 { margin: 14px 0 8px; line-height: 1.2; }
|
||||||
|
|||||||
Reference in New Issue
Block a user