diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/TextView.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/TextView.tsx index 67968ff2..7fcc58bb 100644 --- a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/TextView.tsx +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/TextView.tsx @@ -1,55 +1,53 @@ -/* eslint-disable @typescript-eslint/no-unnecessary-condition */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/restrict-plus-operands */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -import Markdown from 'react-markdown' -import { Link as RouterLink } from 'react-router-dom' -import remarkBreaks from 'remark-breaks' +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 { decodeTag } from '#utils/FormatTags' -import { hashTagRegex } from '#utils/HashTagRegex' -import { fixUrls, mailRegex } from '#utils/ReplaceURLs' +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' -import type { Tag } from '#types/Tag' /** * @category Map */ export const TextView = ({ item, - itemId, text, truncate = false, rawText, }: { item?: Item - itemId?: string text?: string | null truncate?: boolean rawText?: string }) => { if (item) { text = item.text - itemId = item.id } + const tags = useTags() const addFilterTag = useAddFilterTag() + const navigate = useNavigate() + const containerRef = useRef(null) + // Prepare the text content let innerText = '' - let replacedText = '' if (rawText) { - innerText = replacedText = rawText + innerText = rawText } else if (text === undefined) { // Field was omitted by backend (no permission) - innerText = replacedText = `[Login](/login) to see this ${ - item?.layer?.item_default_name ?? 'item' - }` + 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 = '' @@ -58,138 +56,89 @@ export const TextView = ({ innerText = text } - if (innerText && truncate) - innerText = truncateText(removeMarkdownKeepLinksAndParagraphs(innerText), 100) - - if (innerText) replacedText = fixUrls(innerText) - - if (replacedText) { - replacedText = replacedText.replace( - /(?)/g, - (url) => `[${url.replace(/https?:\/\/w{3}\./gi, '')}](${url})`, - ) + // Apply truncation if needed + if (innerText && truncate) { + innerText = truncateMarkdown(removeMarkdownSyntax(innerText), 100) } - if (replacedText) { - replacedText = replacedText.replace(mailRegex, (url) => { - return `[${url}](mailto:${url})` - }) - } + // Pre-process the markdown + const processedText = innerText ? preprocessMarkdown(innerText) : '' - if (replacedText) { - replacedText = replacedText.replace(hashTagRegex, (match) => { - return `[${match}](${match})` - }) - } + 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], + ) - const HashTag = ({ children, tag, itemId }: { children: string; tag: Tag; itemId?: string }) => { - return ( - { - e.stopPropagation() - addFilterTag(tag) - }} - > - {decodeTag(children)} - - ) - } + // Update content when text changes + useEffect(() => { + editor.commands.setContent(processedText) + }, [editor, processedText]) - const Link = ({ href, children }: { href: string; children: string }) => { - // Youtube - if (href.startsWith('https://www.youtube.com/watch?v=')) { - const videoId = href?.split('v=')[1].split('&')[0] - const youtubeEmbedUrl = `https://www.youtube-nocookie.com/embed/${videoId}` + // Handle link clicks for internal navigation + useEffect(() => { + const container = containerRef.current + if (!container) return - return ( -