Adding new clipboard funtionality and panel
This commit is contained in:
parent
d7f5612954
commit
e2deaa8066
|
|
@ -6,6 +6,7 @@ import { useHistoryStore } from '~/stores/history'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useSettingsStore } from '~/stores/settings'
|
import { useSettingsStore } from '~/stores/settings'
|
||||||
import type { SearchHit } from '~/types'
|
import type { SearchHit } from '~/types'
|
||||||
|
import { select } from '#build/ui'
|
||||||
|
|
||||||
interface TypesenseHighlight {
|
interface TypesenseHighlight {
|
||||||
field?: string
|
field?: string
|
||||||
|
|
@ -344,7 +345,7 @@ const currentMatchIdx = ref(0)
|
||||||
function clearMatchMarks() {
|
function clearMatchMarks() {
|
||||||
const container = paragraphsContainer.value
|
const container = paragraphsContainer.value
|
||||||
if (!container) return
|
if (!container) return
|
||||||
container.querySelectorAll('mark.search-match').forEach(m => {
|
container.querySelectorAll('mark.search-match').forEach((m) => {
|
||||||
const parent = m.parentNode
|
const parent = m.parentNode
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent.replaceChild(document.createTextNode(m.textContent || ''), m)
|
parent.replaceChild(document.createTextNode(m.textContent || ''), m)
|
||||||
|
|
@ -753,13 +754,43 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
|
||||||
}
|
}
|
||||||
return matches.length
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UDashboardPanel id="estudios-ts-detail">
|
<UDashboardPanel id="estudios-ts-detail">
|
||||||
<UDashboardNavbar :toggle="false">
|
<UDashboardNavbar :toggle="false">
|
||||||
<template #leading>
|
<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>
|
||||||
|
|
||||||
<template #title>
|
<template #title>
|
||||||
|
|
@ -770,9 +801,14 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
|
||||||
|
|
||||||
<template #right>
|
<template #right>
|
||||||
<UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
|
<UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
|
||||||
<UButton :icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
|
<UButton
|
||||||
:color="isFav ? 'primary' : 'neutral'" variant="ghost" :disabled="!document"
|
:icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
|
||||||
:aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'" @click="onToggleFavorite" />
|
:color="isFav ? 'primary' : 'neutral'"
|
||||||
|
variant="ghost"
|
||||||
|
:disabled="!document"
|
||||||
|
:aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'"
|
||||||
|
@click="onToggleFavorite"
|
||||||
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
|
|
@ -798,39 +834,91 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
|
||||||
<span class="truncate max-w-55">{{ safeLocation() }}</span>
|
<span class="truncate max-w-55">{{ safeLocation() }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<UButton v-if="matchUrl" :to="matchUrl" target="_blank" icon="i-lucide-external-link" label="Ver en sitio"
|
<UButton
|
||||||
color="primary" variant="soft" size="xs" class="shrink-0" />
|
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>
|
||||||
|
|
||||||
<div v-if="fileLinks.length" class="flex flex-wrap gap-2">
|
<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"
|
<UButton
|
||||||
color="neutral" variant="subtle" size="xs" external />
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Buscador interno del documento -->
|
<!-- Buscador interno del documento -->
|
||||||
<div v-if="paragraphs.length" class="flex items-center gap-2">
|
<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...'"
|
<UInput
|
||||||
class="flex-1 min-w-0" :ui="{ trailing: 'pe-1' }" @keydown="onInputKey">
|
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>
|
<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>
|
</template>
|
||||||
</UInput>
|
</UInput>
|
||||||
<template v-if="matchElements.length || searchMode === 'local'">
|
<template v-if="matchElements.length || searchMode === 'local'">
|
||||||
<UBadge v-if="searchMode === 'typesense' && matchElements.length" label="Typesense" size="xs" variant="subtle"
|
<UBadge
|
||||||
color="warning" class="shrink-0" />
|
v-if="searchMode === 'typesense' && matchElements.length"
|
||||||
|
label="Typesense"
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
color="warning"
|
||||||
|
class="shrink-0"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
class="text-xs tabular-nums whitespace-nowrap shrink-0 px-2 py-1 rounded-md bg-elevated border border-default"
|
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' }}
|
{{ matchElements.length ? `${currentMatchIdx + 1} / ${matchElements.length}` : '0 / 0' }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<UTooltip text="Anterior (Shift+Enter)">
|
<UTooltip text="Anterior (Shift+Enter)">
|
||||||
<UButton icon="i-lucide-chevron-up" :disabled="!matchElements.length" aria-label="Anterior"
|
<UButton
|
||||||
color="neutral" variant="ghost" size="sm" @click="prevMatch" />
|
icon="i-lucide-chevron-up"
|
||||||
|
:disabled="!matchElements.length"
|
||||||
|
aria-label="Anterior"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click="prevMatch"
|
||||||
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<UTooltip text="Siguiente (Enter)">
|
<UTooltip text="Siguiente (Enter)">
|
||||||
<UButton icon="i-lucide-chevron-down" :disabled="!matchElements.length" aria-label="Siguiente"
|
<UButton
|
||||||
color="neutral" variant="ghost" size="sm" @click="nextMatch" />
|
icon="i-lucide-chevron-down"
|
||||||
|
:disabled="!matchElements.length"
|
||||||
|
aria-label="Siguiente"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click="nextMatch"
|
||||||
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -846,6 +934,42 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
|
||||||
|
|
||||||
<!-- Todos los párrafos -->
|
<!-- 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">
|
<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 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 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]')">
|
<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}`"
|
:label="`${hit.document.number}`"
|
||||||
size="md"
|
size="md"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@click="copyToClipboard(hit.document.text, hit.document.type)"
|
|
||||||
class="text-gray-300 hover:text-white"
|
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>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div
|
<div
|
||||||
class="paragraph-html text-sm leading-relaxed text-gray-800 dark:text-gray-200"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,26 @@
|
||||||
export async function copyToClipboard(textToCopy: string, type: string) {
|
export async function copyToClipboard(textToCopy: string, type: string) {
|
||||||
const toast = useToast()
|
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 {
|
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({
|
toast.add({
|
||||||
title: 'Texto copiado!',
|
title: 'Texto copiado!',
|
||||||
description: `${textToCopy}`,
|
description: `${textOutput}`,
|
||||||
icon: 'ph-clipboard-text',
|
icon: 'ph-clipboard-text',
|
||||||
color: type == 'activities' ? 'success' : 'info'
|
color: type == 'activities' ? 'success' : 'info'
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue