import { useEffect, useRef, useState } from 'react' import { useEditor, EditorContent } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import Link from '@tiptap/extension-link' import Image from '@tiptap/extension-image' import { api } from '../api/client.js' // Toolbar button. function Btn({ onClick, active, disabled, title, children }) { return ( ) } function escapeHtml(s) { return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]) } export default function RichTextEditor({ value, onChange, pages = [] }) { const fileRef = useRef(null) const [uploading, setUploading] = useState(false) const [linkMenu, setLinkMenu] = useState(false) const [linkFilter, setLinkFilter] = useState('') const editor = useEditor({ extensions: [ StarterKit.configure({ heading: { levels: [2, 3] } }), Link.configure({ openOnClick: false, autolink: true }), Image.configure({ inline: false }), ], content: value || '', onUpdate: ({ editor }) => onChange(editor.getHTML()), }) // Safety net: sync if the parent resets `value` externally (won't fire during // normal typing because the parent value equals what the editor just emitted). useEffect(() => { if (!editor) return if (value != null && value !== editor.getHTML()) { editor.commands.setContent(value, false) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, editor]) if (!editor) return null function setLink() { const prev = editor.getAttributes('link').href || '' const url = window.prompt('Link URL (leave blank to remove)', prev) if (url === null) return if (url === '') return editor.chain().focus().extendMarkRange('link').unsetLink().run() editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run() } function insertInternalLink(page) { const { from, to } = editor.state.selection if (from === to) { editor.chain().focus().insertContent(`${escapeHtml(page.title)} `).run() } else { editor.chain().focus().extendMarkRange('link').setLink({ href: `/wiki/${page.slug}` }).run() } setLinkMenu(false) setLinkFilter('') } async function onPickImage(e) { const file = e.target.files?.[0] e.target.value = '' // allow re-selecting the same file if (!file) return setUploading(true) try { const { url } = await api.admin.upload(file) editor.chain().focus().setImage({ src: url, alt: file.name }).run() } catch (err) { alert(err.message || 'Image upload failed.') } finally { setUploading(false) } } return (