Anton Tranelis b944a7f401 feat(TextViewStatic): add item colors to @mentions
- Pass items and getItemColor to simpleMarkdownToHtml
- Item mentions now display with correct colors (item.color → tag color → layer default)
- Add font-weight: bold to hashtags and item mentions for consistency

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 16:54:39 +01:00

133 lines
3.5 KiB
TypeScript

import { useEffect, useMemo, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAddFilterTag } from '#components/Map/hooks/useFilter'
import { useGetItemColor } from '#components/Map/hooks/useItemColor'
import { useItems } from '#components/Map/hooks/useItems'
import { useTags } from '#components/Map/hooks/useTags'
import {
preprocessMarkdown,
removeMarkdownSyntax,
truncateMarkdown,
} from '#components/TipTap/utils/preprocessMarkdown'
import { simpleMarkdownToHtml } from '#components/TipTap/utils/simpleMarkdownToHtml'
import type { Item } from '#types/Item'
import type { Tag } from '#types/Tag'
/**
* Lightweight static text renderer for popups and card previews.
* Uses simple HTML rendering instead of TipTap for better performance
* and compatibility with Leaflet popups (no contenteditable issues).
*
* @category Map
*/
export const TextViewStatic = ({
item,
text,
truncate = false,
rawText,
}: {
item?: Item
text?: string | null
truncate?: boolean
rawText?: string
}) => {
if (item) {
text = item.text
}
const tags = useTags()
const items = useItems()
const getItemColor = useGetItemColor()
const addFilterTag = useAddFilterTag()
const navigate = useNavigate()
const containerRef = useRef<HTMLDivElement>(null)
// Prepare the text content
let innerText = ''
if (rawText) {
innerText = rawText
} else if (text === undefined) {
// Field was omitted by backend (no permission)
innerText = `[Login](/login) to see this ${item?.layer?.item_default_name ?? 'item'}`
} else if (text === null || text === '') {
// Field is not set or empty - show nothing
innerText = ''
} else {
// Field has a value
innerText = text
}
// Apply truncation if needed
if (innerText && truncate) {
innerText = truncateMarkdown(removeMarkdownSyntax(innerText), 100)
}
// Pre-process and convert to HTML
const html = useMemo(() => {
if (!innerText) return ''
const processed = preprocessMarkdown(innerText)
return simpleMarkdownToHtml(processed, tags, { items, getItemColor })
}, [innerText, tags, items, getItemColor])
// Handle clicks for internal navigation and hashtags
useEffect(() => {
const container = containerRef.current
if (!container) return
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLElement
// Handle hashtag clicks
const hashtag = target.closest('[data-hashtag]')
if (hashtag) {
e.preventDefault()
e.stopPropagation()
const label = hashtag.getAttribute('data-label')
if (label) {
const tag = tags.find((t: Tag) => t.name.toLowerCase() === label.toLowerCase())
if (tag) {
addFilterTag(tag)
}
}
return
}
// Handle link clicks
const link = target.closest('a')
if (!link) return
const href = link.getAttribute('href')
if (!href) return
// Internal links → React Router navigation
if (href.startsWith('/')) {
e.preventDefault()
e.stopPropagation()
void navigate(href)
}
// External links are handled by target="_blank"
}
container.addEventListener('click', handleClick)
return () => {
container.removeEventListener('click', handleClick)
}
}, [navigate, tags, addFilterTag])
if (!innerText) {
return null
}
return (
<div
translate='no'
ref={containerRef}
className='markdown tw:text-map tw:leading-map tw:text-sm'
dangerouslySetInnerHTML={{ __html: html }}
/>
)
}