Adding new clipboard funtionality and panel

This commit is contained in:
Julio Ruiz 2026-05-25 21:04:05 -05:00
parent d7f5612954
commit e2deaa8066
2 changed files with 165 additions and 25 deletions

View File

@ -6,6 +6,7 @@ import { useHistoryStore } from '~/stores/history'
import { storeToRefs } from 'pinia'
import { useSettingsStore } from '~/stores/settings'
import type { SearchHit } from '~/types'
import { select } from '#build/ui'
interface TypesenseHighlight {
field?: string
@ -344,7 +345,7 @@ const currentMatchIdx = ref(0)
function clearMatchMarks() {
const container = paragraphsContainer.value
if (!container) return
container.querySelectorAll('mark.search-match').forEach(m => {
container.querySelectorAll('mark.search-match').forEach((m) => {
const parent = m.parentNode
if (parent) {
parent.replaceChild(document.createTextNode(m.textContent || ''), m)
@ -753,13 +754,43 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
}
return matches.length
}
const isPopOverOpen = ref(false)
const anchor = ref({ x: 0, y: 0 })
const virtualElement = computed(() => ({
getBoundingClientRect: () =>
({
width: 0,
height: 0,
left: anchor.value.x,
right: anchor.value.x,
top: anchor.value.y,
bottom: anchor.value.y,
...anchor.value
} as DOMRect)
}))
function handleSelection(event: MouseEvent) {
const selection = window.getSelection()
if (selection && selection.toString().length > 0) {
anchor.value = { x: event.clientX, y: event.clientY }
isPopOverOpen.value = true
} else {
isPopOverOpen.value = false
}
}
</script>
<template>
<UDashboardPanel id="estudios-ts-detail">
<UDashboardNavbar :toggle="false">
<template #leading>
<UButton icon="i-lucide-arrow-left" color="neutral" variant="ghost" class="-ms-1.5" @click="emits('close')" />
<UButton
icon="i-lucide-arrow-left"
color="neutral"
variant="ghost"
class="-ms-1.5"
@click="emits('close')"
/>
</template>
<template #title>
@ -770,9 +801,14 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
<template #right>
<UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
<UButton :icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
:color="isFav ? 'primary' : 'neutral'" variant="ghost" :disabled="!document"
:aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'" @click="onToggleFavorite" />
<UButton
:icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
:color="isFav ? 'primary' : 'neutral'"
variant="ghost"
:disabled="!document"
:aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'"
@click="onToggleFavorite"
/>
</UTooltip>
</template>
</UDashboardNavbar>
@ -798,39 +834,91 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
<span class="truncate max-w-55">{{ safeLocation() }}</span>
</p>
</div>
<UButton v-if="matchUrl" :to="matchUrl" target="_blank" icon="i-lucide-external-link" label="Ver en sitio"
color="primary" variant="soft" size="xs" class="shrink-0" />
<UButton
v-if="matchUrl"
:to="matchUrl"
target="_blank"
icon="i-lucide-external-link"
label="Ver en sitio"
color="primary"
variant="soft"
size="xs"
class="shrink-0"
/>
</div>
<div v-if="fileLinks.length" class="flex flex-wrap gap-2">
<UButton v-for="(f, idx) in fileLinks" :key="idx" :to="f.to" :target="f.target" :icon="f.icon!" :label="f.label"
color="neutral" variant="subtle" size="xs" external />
<UButton
v-for="(f, idx) in fileLinks"
:key="idx"
:to="f.to"
:target="f.target"
:icon="f.icon!"
:label="f.label"
color="neutral"
variant="subtle"
size="xs"
external
/>
</div>
<!-- Buscador interno del documento -->
<div v-if="paragraphs.length" class="flex items-center gap-2">
<UInput v-model="localQuery" icon="i-lucide-search" :placeholder="props.query || 'Buscar en este documento...'"
class="flex-1 min-w-0" :ui="{ trailing: 'pe-1' }" @keydown="onInputKey">
<UInput
v-model="localQuery"
icon="i-lucide-search"
:placeholder="props.query || 'Buscar en este documento...'"
class="flex-1 min-w-0"
:ui="{ trailing: 'pe-1' }"
@keydown="onInputKey"
>
<template #trailing>
<UButton v-if="localQuery" icon="i-lucide-x" variant="link" aria-label="Limpiar" @click="clearLocalQuery" />
<UButton
v-if="localQuery"
icon="i-lucide-x"
variant="link"
aria-label="Limpiar"
@click="clearLocalQuery"
/>
</template>
</UInput>
<template v-if="matchElements.length || searchMode === 'local'">
<UBadge v-if="searchMode === 'typesense' && matchElements.length" label="Typesense" size="xs" variant="subtle"
color="warning" class="shrink-0" />
<UBadge
v-if="searchMode === 'typesense' && matchElements.length"
label="Typesense"
size="xs"
variant="subtle"
color="warning"
class="shrink-0"
/>
<span
class="text-xs tabular-nums whitespace-nowrap shrink-0 px-2 py-1 rounded-md bg-elevated border border-default"
:class="matchElements.length ? 'text-toned font-medium' : 'text-dimmed'">
:class="matchElements.length ? 'text-toned font-medium' : 'text-dimmed'"
>
{{ matchElements.length ? `${currentMatchIdx + 1} / ${matchElements.length}` : '0 / 0' }}
</span>
<div class="flex items-center gap-1 shrink-0">
<UTooltip text="Anterior (Shift+Enter)">
<UButton icon="i-lucide-chevron-up" :disabled="!matchElements.length" aria-label="Anterior"
color="neutral" variant="ghost" size="sm" @click="prevMatch" />
<UButton
icon="i-lucide-chevron-up"
:disabled="!matchElements.length"
aria-label="Anterior"
color="neutral"
variant="ghost"
size="sm"
@click="prevMatch"
/>
</UTooltip>
<UTooltip text="Siguiente (Enter)">
<UButton icon="i-lucide-chevron-down" :disabled="!matchElements.length" aria-label="Siguiente"
color="neutral" variant="ghost" size="sm" @click="nextMatch" />
<UButton
icon="i-lucide-chevron-down"
:disabled="!matchElements.length"
aria-label="Siguiente"
color="neutral"
variant="ghost"
size="sm"
@click="nextMatch"
/>
</UTooltip>
</div>
</template>
@ -846,6 +934,42 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
<!-- Todos los párrafos -->
<div v-else-if="paragraphs.length" ref="paragraphsContainer" class="p-1 sm:p-4 max-w-4xl m-4 sm:mx-auto sm:my-6">
<UPopover
v-model:open="isPopOverOpen"
:reference="virtualElement"
class="absolute top-[90vh] left-[900px]"
>
<!-- We use the slot to bind to our moving anchor -->
<template #anchor>
<div class="hidden" />
</template>
<template #content>
<UFieldGroup size="sm" orientation="horizontal" class="bg-white p-0 rounded-md shadow-md">
<UButton
icon="ph-note-pencil"
variant="ghost"
color="neutral"
size="lg"
@click="addNote()"
></UButton>
<UButton
icon="ph-highlighter"
variant="ghost"
color="neutral"
size="lg"
@click="highlightParagraph()"
></UButton>
<UButton
icon="ph-copy"
variant="ghost"
color="neutral"
size="lg"
@click="copyToClipboard()"
></UButton>
</UFieldGroup>
</template>
</UPopover>
<div class="bg-white rounded-lg shadow-md p-4 pl-2 sm:p-8 sm:pl-4 mb-4 last:mb-0">
<div v-for="(hit, idx) in paragraphs" :key="idx" :data-paragraph-number="hit.document.number">
<div class="grid grid-cols-1fr items-start gap-2 mb-2" :class="(showParagraphNumbers && 'grid-cols-[32px_1fr]')">
@ -855,15 +979,16 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
:label="`${hit.document.number}`"
size="md"
variant="ghost"
@click="copyToClipboard(hit.document.text, hit.document.type)"
class="text-gray-300 hover:text-white"
:class="accentColor === 'blue' ? 'hover:bg-carpablue' : 'hover:bg-carpagreen'"
:class="(hit.document.type=='activities'?'hover:bg-carpagreen':'hover:bg-carpablue')"
@click="isPopOverOpen = !isPopOverOpen"
/>
</div>
<div class="">
<div
class="paragraph-html text-sm leading-relaxed text-gray-800 dark:text-gray-200"
v-html="hit.document.raw || hit.document.text"
@mouseup="handleSelection"
v-html="hit.document.html || hit.document.text"
/>
</div>
</div>

View File

@ -2,15 +2,30 @@
export async function copyToClipboard(textToCopy: string, type: string) {
const toast = useToast()
const selection = window.getSelection()
const container = document.createElement('div')
const range = selection?.getRangeAt(0)
container.appendChild(range.cloneContents())
const htmlOutput = container.innerHTML
const textOutput = selection?.toString()
try {
await navigator.clipboard.writeText(textToCopy)
// Modern Clipboard API requires ClipboardItem
const clipboardItem = new ClipboardItem({
'text/html': new Blob([htmlOutput], { type: 'text/html' }),
'text/plain': new Blob([textOutput], { type: 'text/plain' })
})
await navigator.clipboard.write([clipboardItem])
toast.add({
title: 'Texto copiado!',
description: `${textToCopy}`,
description: `${textOutput}`,
icon: 'ph-clipboard-text',
color: type == 'activities' ? 'success' : 'info'
})
} catch (err) {
console.error('Failed to copy: ', err)
}
}
}