import { Link } from '@tiptap/extension-link' import { EditorContent, useEditor } from '@tiptap/react' import { StarterKit } from '@tiptap/starter-kit' import { useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { Markdown } from 'tiptap-markdown' import { useAddFilterTag } from '#components/Map/hooks/useFilter' import { useTags } from '#components/Map/hooks/useTags' import { Hashtag } from '#components/TipTap/extensions/Hashtag' import { VideoEmbed } from '#components/TipTap/extensions/VideoEmbed' import { preprocessMarkdown, removeMarkdownSyntax, truncateMarkdown, } from '#components/TipTap/utils/preprocessMarkdown' import type { Item } from '#types/Item' /** * @category Map */ export const TextView = ({ item, text, truncate = false, rawText, }: { item?: Item text?: string | null truncate?: boolean rawText?: string }) => { if (item) { text = item.text } const tags = useTags() const addFilterTag = useAddFilterTag() const navigate = useNavigate() const containerRef = useRef(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 the markdown const processedText = innerText ? preprocessMarkdown(innerText) : '' const editor = useEditor( { extensions: [ StarterKit, Markdown.configure({ html: true, // Allow HTML in markdown (for our preprocessed tags) transformPastedText: true, }), Link.configure({ openOnClick: false, // We handle clicks ourselves HTMLAttributes: { target: '_blank', rel: 'noopener noreferrer', }, }), Hashtag.configure({ tags, onTagClick: (tag) => { addFilterTag(tag) }, }), VideoEmbed, ], content: processedText, editable: false, editorProps: { attributes: { class: 'markdown tw:text-map tw:leading-map tw:text-sm', }, }, }, [processedText, tags], ) // Update content when text changes useEffect(() => { editor.commands.setContent(processedText) }, [editor, processedText]) // Handle link clicks for internal navigation useEffect(() => { const container = containerRef.current if (!container) return const handleClick = (e: MouseEvent) => { const target = e.target as HTMLElement 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 the Link extension (target="_blank") } container.addEventListener('click', handleClick) return () => { container.removeEventListener('click', handleClick) } }, [navigate]) if (!innerText) { return null } return (
) }