You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
164 lines
5.0 KiB
164 lines
5.0 KiB
<script lang="ts"> |
|
import ProfileBadge from '../layout/ProfileBadge.svelte'; |
|
import type { Highlight } from '../../services/nostr/highlight-service.js'; |
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
import { goto } from '$app/navigation'; |
|
|
|
interface Props { |
|
highlights: Array<{ start: number; end: number; highlight: Highlight }>; |
|
content: string; |
|
event: NostrEvent; |
|
children: import('svelte').Snippet; |
|
} |
|
|
|
let { highlights, content, event, children }: Props = $props(); |
|
|
|
let containerRef = $state<HTMLElement | null>(null); |
|
let hoveredHighlight = $state<Highlight | null>(null); |
|
let tooltipPosition = $state({ top: 0, left: 0 }); |
|
|
|
function openHighlight(highlight: Highlight) { |
|
// Store event in sessionStorage so the event page can use it without re-fetching |
|
sessionStorage.setItem('aitherboard_preloadedEvent', JSON.stringify(highlight.event)); |
|
goto(`/event/${highlight.event.id}`); |
|
} |
|
|
|
// Apply highlights to rendered HTML content |
|
// This runs after the HTML is rendered |
|
$effect(() => { |
|
if (!containerRef || highlights.length === 0) return; |
|
|
|
// Wait for content to be rendered |
|
const timeoutId = setTimeout(() => { |
|
if (!containerRef) return; |
|
|
|
// For each highlight, try to find and wrap the text in the rendered HTML |
|
for (const { start, end, highlight } of highlights) { |
|
const highlightText = content.substring(start, end); |
|
if (!highlightText) continue; |
|
|
|
// Search for this text in the rendered HTML |
|
const walker = document.createTreeWalker( |
|
containerRef, |
|
NodeFilter.SHOW_TEXT, |
|
null |
|
); |
|
|
|
let node; |
|
while ((node = walker.nextNode())) { |
|
const text = node.textContent || ''; |
|
const index = text.indexOf(highlightText); |
|
|
|
if (index !== -1) { |
|
// Found the text, wrap it |
|
const span = document.createElement('span'); |
|
span.className = 'highlight-span'; |
|
span.setAttribute('data-highlight-id', highlight.event.id); |
|
span.setAttribute('data-pubkey', highlight.pubkey); |
|
span.textContent = highlightText; |
|
|
|
// Add event listeners |
|
span.addEventListener('mouseenter', (e) => { |
|
const rect = (e.target as HTMLElement).getBoundingClientRect(); |
|
tooltipPosition = { top: rect.top - 40, left: rect.left }; |
|
hoveredHighlight = highlight; |
|
}); |
|
|
|
span.addEventListener('mouseleave', () => { |
|
hoveredHighlight = null; |
|
}); |
|
|
|
span.addEventListener('click', () => { |
|
openHighlight(highlight); |
|
}); |
|
|
|
// Replace text node |
|
const beforeText = text.substring(0, index); |
|
const afterText = text.substring(index + highlightText.length); |
|
|
|
if (beforeText) { |
|
node.parentNode?.insertBefore(document.createTextNode(beforeText), node); |
|
} |
|
|
|
node.parentNode?.insertBefore(span, node); |
|
|
|
if (afterText) { |
|
node.parentNode?.insertBefore(document.createTextNode(afterText), node); |
|
} |
|
|
|
node.parentNode?.removeChild(node); |
|
break; // Only wrap first occurrence |
|
} |
|
} |
|
} |
|
}, 500); |
|
|
|
return () => clearTimeout(timeoutId); |
|
}); |
|
</script> |
|
|
|
<div class="highlight-overlay" bind:this={containerRef}> |
|
{@render children()} |
|
|
|
{#if hoveredHighlight} |
|
<div |
|
class="highlight-tooltip" |
|
style="top: {tooltipPosition.top}px; left: {tooltipPosition.left}px;" |
|
> |
|
<ProfileBadge pubkey={hoveredHighlight.pubkey} /> |
|
<button class="view-highlight-button" onclick={() => hoveredHighlight && openHighlight(hoveredHighlight)}> |
|
View the highlight |
|
</button> |
|
</div> |
|
{/if} |
|
</div> |
|
|
|
<style> |
|
:global(.highlight-span) { |
|
background: rgba(255, 255, 0, 0.3); |
|
cursor: pointer; |
|
transition: background 0.2s; |
|
} |
|
|
|
:global(.highlight-span:hover) { |
|
background: rgba(255, 255, 0, 0.5); |
|
} |
|
|
|
.highlight-tooltip { |
|
position: fixed; |
|
background: var(--fog-post, #ffffff); |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.5rem; |
|
padding: 0.75rem; |
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
|
z-index: 1000; |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.5rem; |
|
min-width: 200px; |
|
} |
|
|
|
:global(.dark) .highlight-tooltip { |
|
background: var(--fog-dark-post, #1f2937); |
|
border-color: var(--fog-dark-border, #374151); |
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); |
|
} |
|
|
|
.view-highlight-button { |
|
padding: 0.5rem; |
|
background: var(--fog-accent, #64748b); |
|
color: white; |
|
border: none; |
|
border-radius: 0.25rem; |
|
cursor: pointer; |
|
font-size: 0.875rem; |
|
} |
|
|
|
:global(.dark) .view-highlight-button { |
|
background: var(--fog-dark-accent, #94a3b8); |
|
} |
|
|
|
.view-highlight-button:hover { |
|
opacity: 0.9; |
|
} |
|
</style>
|
|
|