hensei-web/components/common/Editor/index.tsx
Justin Edmund b1c8fb1a76 Add Editor component
This commit adds the bulk of the code for our new rich-text editor. The Editor component will be used to edit and display rich text via Tiptap.
2023-07-05 18:53:15 -07:00

151 lines
3.7 KiB
TypeScript

import { ComponentProps, useCallback } from 'react'
import { useRouter } from 'next/router'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
import Youtube from '@tiptap/extension-youtube'
import CustomMention from '~extensions/CustomMention'
import classNames from 'classnames'
import type { JSONContent } from '@tiptap/core'
import styles from './index.module.scss'
import { mentionSuggestionOptions } from '~components/Suggestion'
interface Props extends ComponentProps<'div'> {
bound: boolean
editable?: boolean
content?: string
onUpdate?: (content: JSONContent) => void
}
const Editor = ({
bound,
className,
content,
editable,
onUpdate,
...props
}: Props) => {
const router = useRouter()
const locale = router.locale || 'en'
const editor = useEditor({
editable: editable,
editorProps: {
attributes: {
class: classNames(
{
[styles.editor]: true,
[styles.bound]: bound,
},
className?.split(' ').map((c) => styles[c])
),
},
},
extensions: [
StarterKit,
Link,
CustomMention.configure({
renderLabel({ options, node }) {
return `${node.attrs.id.name[locale] ?? node.attrs.id.granblue_en}`
},
suggestion: mentionSuggestionOptions,
HTMLAttributes: {
class: classNames({
[styles.mention]: true,
}),
},
}),
Youtube.configure({
inline: false,
modestBranding: true,
interfaceLanguage: locale,
}),
],
content: content ? JSON.parse(content) : '',
onUpdate: ({ editor }) => {
const json = editor.getJSON()
if (onUpdate) onUpdate(json)
},
})
const setLink = useCallback(() => {
const previousUrl = editor?.getAttributes('link').href
const url = window.prompt('URL', previousUrl)
// cancelled
if (url === null) {
return
}
// empty
if (url === '') {
editor?.chain().focus().extendMarkRange('link').unsetLink().run()
return
}
// update link
editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
}, [editor])
const addYoutubeVideo = () => {
const url = prompt('Enter YouTube URL')
if (editor && url) {
editor.commands.setYoutubeVideo({
src: url,
width: 320,
height: 180,
})
}
}
if (!editor) {
return null
}
return (
<section className={styles.wrapper}>
{editor && editable === true && (
<nav className={styles.toolbar}>
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? styles.active : ''}
>
bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive('italic') ? styles.active : ''}
>
italic
</button>
<button
onClick={() => editor.chain().focus().toggleStrike().run()}
className={editor.isActive('strike') ? styles.active : ''}
>
strike
</button>
<div className={styles.divider} />
<button
onClick={setLink}
className={editor.isActive('link') ? styles.active : ''}
>
+ link
</button>
<button onClick={addYoutubeVideo}>+ youtube</button>
</nav>
)}
<EditorContent editor={editor} />
</section>
)
}
Editor.defaultProps = {
bound: false,
editable: false,
}
export default Editor