From e2bde300229e53f8ef405df5c56551736ff570c2 Mon Sep 17 00:00:00 2001 From: Michael Geers Date: Fri, 22 Aug 2025 18:32:39 +0200 Subject: [PATCH] Optimizer UI: copy fallback for non-secure context (#23176) --- assets/js/components/Optimize/CopyButton.vue | 59 +++++++++++++++---- .../js/components/Savings/Sponsor.stories.ts | 8 +++ assets/js/components/Savings/Sponsor.vue | 1 + 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/assets/js/components/Optimize/CopyButton.vue b/assets/js/components/Optimize/CopyButton.vue index 50b56bfac..268183313 100644 --- a/assets/js/components/Optimize/CopyButton.vue +++ b/assets/js/components/Optimize/CopyButton.vue @@ -52,17 +52,54 @@ export default defineComponent({ }, methods: { handleCopy() { - navigator.clipboard - .writeText(this.content) - .then(() => { - this.copied = true; - setTimeout(() => { - this.copied = false; - }, 2000); // Reset after 2 seconds - }) - .catch((err) => { - console.error("Failed to copy to clipboard:", err); - }); + // Modern browser API, requires secure context (localhost or https) + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard + .writeText(this.content) + .then(() => { + this.showCopiedFeedback(); + }) + .catch((err) => { + console.error("Failed to copy to clipboard:", err); + this.fallbackCopy(); + }); + } else { + // Fallback method for HTTP/non-secure contexts + this.fallbackCopy(); + } + }, + fallbackCopy() { + try { + // Create a temporary textarea element + const textarea = document.createElement("textarea"); + textarea.value = this.content; + textarea.style.position = "fixed"; + textarea.style.left = "-999999px"; + textarea.style.top = "-999999px"; + document.body.appendChild(textarea); + + // Select and copy the text + textarea.focus(); + textarea.select(); + const successful = document.execCommand("copy"); + document.body.removeChild(textarea); + + if (successful) { + this.showCopiedFeedback(); + } else { + alert("Copy failed. Please copy manually."); + } + } catch (err) { + console.error("Fallback copy failed:", err); + // Show user feedback that copy failed + alert("Copy failed. Please copy manually."); + } + }, + showCopiedFeedback() { + this.copied = true; + setTimeout(() => { + this.copied = false; + }, 2000); }, }, }); diff --git a/assets/js/components/Savings/Sponsor.stories.ts b/assets/js/components/Savings/Sponsor.stories.ts index 5c97d1594..344b84f7f 100644 --- a/assets/js/components/Savings/Sponsor.stories.ts +++ b/assets/js/components/Savings/Sponsor.stories.ts @@ -20,6 +20,14 @@ export default { control: "boolean", description: "Whether the sponsorship is expiring soon", }, + token: { + control: "text", + description: "Sponsor token (optional)", + }, + fromYaml: { + control: "boolean", + description: "Whether the sponsor config comes from YAML", + }, }, } as Meta; diff --git a/assets/js/components/Savings/Sponsor.vue b/assets/js/components/Savings/Sponsor.vue index 6a31afb92..3b15a4599 100644 --- a/assets/js/components/Savings/Sponsor.vue +++ b/assets/js/components/Savings/Sponsor.vue @@ -79,6 +79,7 @@ export default defineComponent({ expiresAt: String, expiresSoon: Boolean, fromYaml: Boolean, + token: String, }, computed: { isTrial() {