sketch: optimize code copy button performance
There was a setTimeout(..., 100) for no good reason.
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s1d721e710776c3eck
diff --git a/webui/src/web-components/sketch-timeline-message.ts b/webui/src/web-components/sketch-timeline-message.ts
index 8385b66..eab1867 100644
--- a/webui/src/web-components/sketch-timeline-message.ts
+++ b/webui/src/web-components/sketch-timeline-message.ts
@@ -387,7 +387,6 @@
updated(changedProperties: Map<string, unknown>) {
super.updated(changedProperties);
this.renderMermaidDiagrams();
- this.setupCodeBlockCopyButtons();
}
// Render mermaid diagrams after the component is updated
@@ -449,60 +448,65 @@
}, 100); // Small delay to ensure DOM is ready
}
- // Setup code block copy buttons after component is updated
- setupCodeBlockCopyButtons() {
- setTimeout(() => {
- // Find all copy buttons in code blocks
- const copyButtons = this.querySelectorAll(".code-copy-button");
- if (!copyButtons || copyButtons.length === 0) return;
-
- // Add click event listener to each button
- copyButtons.forEach((button) => {
- button.addEventListener("click", (e) => {
- e.stopPropagation();
- const codeId = (button as HTMLElement).dataset.codeId;
- if (!codeId) return;
-
- const codeElement = this.querySelector(`#${codeId}`);
- if (!codeElement) return;
-
- const codeText = codeElement.textContent || "";
- const buttonRect = button.getBoundingClientRect();
-
- // Copy code to clipboard
- navigator.clipboard
- .writeText(codeText)
- .then(() => {
- // Show success indicator
- const originalHTML = button.innerHTML;
- button.innerHTML = `
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <path d="M20 6L9 17l-5-5"></path>
- </svg>
- `;
-
- // Display floating message
- this.showFloatingMessage("Copied!", buttonRect, "success");
-
- // Reset button after delay
- setTimeout(() => {
- button.innerHTML = originalHTML;
- }, 2000);
- })
- .catch((err) => {
- console.error("Failed to copy code:", err);
- this.showFloatingMessage("Failed to copy!", buttonRect, "error");
- });
- });
- });
- }, 100); // Small delay to ensure DOM is ready
- }
-
// See https://lit.dev/docs/components/lifecycle/
disconnectedCallback() {
super.disconnectedCallback();
}
+ // Add post-sanitization button replacement
+ private addCopyButtons(html: string): string {
+ return html.replace(
+ /<span class="copy-button-placeholder"><\/span>/g,
+ `<button class="code-copy-button" title="Copy code">
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
+ </svg>
+ </button>`,
+ );
+ }
+
+ // Event delegation handler for code copy functionality
+ private handleCodeCopy(event: Event) {
+ const button = event.target as HTMLElement;
+ if (!button.classList.contains("code-copy-button")) return;
+
+ event.stopPropagation();
+
+ // Find the code element using DOM traversal
+ const header = button.closest(".code-block-header");
+ const codeElement = header?.nextElementSibling?.querySelector("code");
+ if (!codeElement) return;
+
+ // Read the text directly from DOM (automatically unescapes HTML)
+ const codeText = codeElement.textContent || "";
+
+ // Copy to clipboard with visual feedback
+ navigator.clipboard
+ .writeText(codeText)
+ .then(() => {
+ // Show success feedback (icon change + floating message)
+ const originalHTML = button.innerHTML;
+ button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M20 6L9 17l-5-5"></path>
+ </svg>`;
+ this.showFloatingMessage(
+ "Copied!",
+ button.getBoundingClientRect(),
+ "success",
+ );
+ setTimeout(() => (button.innerHTML = originalHTML), 2000);
+ })
+ .catch((err) => {
+ console.error("Failed to copy code:", err);
+ this.showFloatingMessage(
+ "Failed to copy!",
+ button.getBoundingClientRect(),
+ "error",
+ );
+ });
+ }
+
renderMarkdown(markdownContent: string): string {
try {
// Create a custom renderer
@@ -535,20 +539,15 @@
}
const escapedText = codeMatch[1];
- const id = `code-block-${Math.random().toString(36).substring(2, 10)}`;
const langClass = lang ? ` class="language-${lang}"` : "";
+ // Use placeholder instead of actual button - will be replaced after sanitization
return `<div class="code-block-container">
<div class="code-block-header">
${lang ? `<span class="code-language">${lang}</span>` : ""}
- <button class="code-copy-button" title="Copy code" data-code-id="${id}">
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
- </svg>
- </button>
+ <span class="copy-button-placeholder"></span>
</div>
- <pre><code id="${id}"${langClass}>${escapedText}</code></pre>
+ <pre><code${langClass}>${escapedText}</code></pre>
</div>`;
};
@@ -562,7 +561,7 @@
// Parse markdown and sanitize the output HTML with DOMPurify
const htmlOutput = marked.parse(markdownContent, markedOptions) as string;
- return DOMPurify.sanitize(htmlOutput, {
+ const sanitizedOutput = DOMPurify.sanitize(htmlOutput, {
// Allow common HTML elements that are safe
ALLOWED_TAGS: [
"p",
@@ -637,6 +636,9 @@
// Keep whitespace for code formatting
KEEP_CONTENT: true,
});
+
+ // Add copy buttons after sanitization
+ return this.addCopyButtons(sanitizedOutput);
} catch (error) {
console.error("Error rendering markdown:", error);
// Fallback to sanitized plain text if markdown parsing fails
@@ -917,6 +919,7 @@
? html`
<div
class="overflow-x-auto mb-0 font-sans py-0.5 select-text cursor-text text-sm leading-relaxed text-left min-w-[200px] box-border mx-auto markdown-content"
+ @click=${this.handleCodeCopy}
>
${unsafeHTML(
this.renderMarkdown(this.message?.content),