fix(frontend) : copie presse-papiers fonctionnelle en HTTP via fallback execCommand
Auto Tag Develop / tag (push) Successful in 7s
Auto Tag Develop / tag (push) Successful in 7s
navigator.clipboard n'est disponible qu'en secure context (HTTPS/localhost), ce qui cassait la copie en prod HTTP. Ajout d'un utilitaire copyToClipboard avec fallback textarea + execCommand, appliqué au viewer Markdown, au token API du profil et au nom de branche Git.
This commit is contained in:
@@ -124,6 +124,7 @@
|
|||||||
import type { TaskDocument } from '~/services/dto/task-document'
|
import type { TaskDocument } from '~/services/dto/task-document'
|
||||||
import { useTaskDocumentService } from '~/services/task-documents'
|
import { useTaskDocumentService } from '~/services/task-documents'
|
||||||
import { formatFileSize } from '~/utils/format'
|
import { formatFileSize } from '~/utils/format'
|
||||||
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
document: TaskDocument | null
|
document: TaskDocument | null
|
||||||
@@ -159,13 +160,10 @@ const isPdf = computed(() => props.document?.mimeType === 'application/pdf')
|
|||||||
const isText = computed(() => isTextDocument(props.document))
|
const isText = computed(() => isTextDocument(props.document))
|
||||||
|
|
||||||
async function copyContent() {
|
async function copyContent() {
|
||||||
try {
|
if (await copyToClipboard(textContent.value)) {
|
||||||
await navigator.clipboard.writeText(textContent.value)
|
|
||||||
copied.value = true
|
copied.value = true
|
||||||
useToast().success(t('taskDocuments.copied'))
|
useToast().success(t('taskDocuments.copied'))
|
||||||
setTimeout(() => { copied.value = false }, 2000)
|
setTimeout(() => { copied.value = false }, 2000)
|
||||||
} catch {
|
|
||||||
// Clipboard unavailable
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,7 @@
|
|||||||
import type { Task } from '~/services/dto/task'
|
import type { Task } from '~/services/dto/task'
|
||||||
import type { GiteaBranch, GiteaPullRequest } from '~/services/dto/gitea'
|
import type { GiteaBranch, GiteaPullRequest } from '~/services/dto/gitea'
|
||||||
import { useGiteaService } from '~/services/gitea'
|
import { useGiteaService } from '~/services/gitea'
|
||||||
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -374,7 +375,7 @@ async function handleCreate() {
|
|||||||
async function handleCopy() {
|
async function handleCopy() {
|
||||||
try {
|
try {
|
||||||
const result = await getBranchName(props.task.id, branchForm.type)
|
const result = await getBranchName(props.task.id, branchForm.type)
|
||||||
await navigator.clipboard.writeText(result.name)
|
await copyToClipboard(result.name)
|
||||||
const { success } = useToast()
|
const { success } = useToast()
|
||||||
success(t('gitea.branch.copied'))
|
success(t('gitea.branch.copied'))
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -129,6 +129,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAvatarService } from '~/composables/useAvatarService'
|
import { useAvatarService } from '~/composables/useAvatarService'
|
||||||
import { useApiTokenService } from '~/services/api-token'
|
import { useApiTokenService } from '~/services/api-token'
|
||||||
|
import { copyToClipboard } from '~/utils/clipboard'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -181,10 +182,9 @@ async function onRemove() {
|
|||||||
|
|
||||||
async function onCopy() {
|
async function onCopy() {
|
||||||
if (!auth.user?.apiToken) return
|
if (!auth.user?.apiToken) return
|
||||||
try {
|
if (await copyToClipboard(auth.user.apiToken)) {
|
||||||
await navigator.clipboard.writeText(auth.user.apiToken)
|
|
||||||
toast.success({ message: t('profile.apiToken.copied') })
|
toast.success({ message: t('profile.apiToken.copied') })
|
||||||
} catch {
|
} else {
|
||||||
toast.error({ message: t('profile.apiToken.copyFailed') })
|
toast.error({ message: t('profile.apiToken.copyFailed') })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Copy text to the clipboard with a fallback for non-secure contexts.
|
||||||
|
*
|
||||||
|
* `navigator.clipboard` is only available in secure contexts (HTTPS or
|
||||||
|
* localhost). On a plain HTTP origin (e.g. an internal/prod server without
|
||||||
|
* TLS) the API is missing, so we fall back to the legacy
|
||||||
|
* `document.execCommand('copy')` using a temporary off-screen textarea.
|
||||||
|
*
|
||||||
|
* @returns `true` if the copy succeeded, `false` otherwise.
|
||||||
|
*/
|
||||||
|
export async function copyToClipboard(text: string): Promise<boolean> {
|
||||||
|
// Preferred path: available in secure contexts (HTTPS / localhost).
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
// Fall through to the legacy fallback below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy fallback: works on plain HTTP origins.
|
||||||
|
try {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = text
|
||||||
|
// Keep it out of view and prevent layout shift / scrolling.
|
||||||
|
textarea.style.position = 'fixed'
|
||||||
|
textarea.style.top = '-9999px'
|
||||||
|
textarea.style.left = '-9999px'
|
||||||
|
textarea.setAttribute('readonly', '')
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
textarea.setSelectionRange(0, text.length)
|
||||||
|
const ok = document.execCommand('copy')
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
return ok
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user