diff --git a/components/common/MentionEditor/index.module.scss b/components/common/MentionEditor/index.module.scss new file mode 100644 index 00000000..0ab74f54 --- /dev/null +++ b/components/common/MentionEditor/index.module.scss @@ -0,0 +1,152 @@ +.wrapper { + border-radius: $input-corner; + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: hidden; + + .editor { + -webkit-font-smoothing: antialiased; + box-sizing: border-box; + color: var(--text-primary); + display: block; + flex-grow: 1; + font-family: system-ui, -apple-system, 'Helvetica Neue', Helvetica, Arial, + sans-serif; + font-size: $font-regular; + overflow: scroll; + padding: ($unit * 1.5) $unit-2x; + white-space: pre-wrap; + width: 100%; + + &:focus { + // border: 2px solid $blue; + outline: none; + } + + p { + line-height: 1.5; + } + + p.empty:first-child::before { + color: var(--text-tertiary); + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + } + + &.bound { + background-color: var(--input-bound-bg); + + &:hover { + background-color: var(--input-bound-bg-hover); + } + } + + .mention { + border-radius: $item-corner-small; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--null-shadow); + background: var(--null-bg); + display: inline-flex; + color: var(--text-primary); + font-weight: $medium; + font-size: 15px; + padding: 1px $unit-half; + margin: $unit-fourth; + transition: all 0.1s ease-out; + + &:hover { + background: var(--null-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--null-shadow-hover); + text-decoration: none; + cursor: pointer; + } + + &[data-element='fire'] { + background: var(--fire-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--fire-shadow); + color: var(--fire-text); + + &:hover { + background: var(--fire-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--fire-shadow-hover); + color: var(--fire-text-hover); + } + } + + &[data-element='water'] { + background: var(--water-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--water-shadow); + color: var(--water-text); + + &:hover { + background: var(--water-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--water-shadow-hover); + color: var(--water-text-hover); + } + } + + &[data-element='earth'] { + background: var(--earth-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--earth-shadow); + color: var(--earth-text); + + &:hover { + background: var(--earth-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--earth-shadow-hover); + color: var(--earth-text-hover); + } + } + + &[data-element='wind'] { + background: var(--wind-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--wind-shadow); + color: var(--wind-text); + + &:hover { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--wind-shadow-hover); + color: var(--wind-text-hover); + } + } + + &[data-element='dark'] { + background: var(--dark-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--dark-shadow); + color: var(--dark-text); + + &:hover { + background: var(--dark-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--dark-shadow-hover); + color: var(--dark-text-hover); + } + } + + &[data-element='light'] { + background: var(--light-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--light-shadow); + color: var(--light-text); + + &:hover { + background: var(--light-bg-hover); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0px var(--light-shadow-hover); + color: var(--light-text-hover); + } + } + } + } +} diff --git a/components/common/MentionEditor/index.tsx b/components/common/MentionEditor/index.tsx new file mode 100644 index 00000000..6bb0358e --- /dev/null +++ b/components/common/MentionEditor/index.tsx @@ -0,0 +1,88 @@ +import { ComponentProps, useCallback, useEffect } from 'react' +import { useRouter } from 'next/router' +import type { JSONContent } from '@tiptap/core' +import { useEditor, EditorContent } from '@tiptap/react' +import { useTranslation } from 'next-i18next' + +import Placeholder from '@tiptap/extension-placeholder' +import FilterMention from '~extensions/FilterMention' +import NoNewLine from '~extensions/NoNewLine' +import classNames from 'classnames' + +import { mentionSuggestionOptions } from '~utils/mentionSuggestions' + +import styles from './index.module.scss' +import StarterKit from '@tiptap/starter-kit' + +interface Props extends ComponentProps<'div'> { + bound?: boolean + placeholder?: string + onUpdate?: (content: string[]) => void +} + +const MentionEditor = ({ bound, placeholder, onUpdate, ...props }: Props) => { + const locale = useRouter().locale || 'en' + const { t } = useTranslation('common') + + // Setup: Editor + const editor = useEditor({ + content: '', + editable: true, + editorProps: { + attributes: { + class: classNames({ + [styles.editor]: true, + [styles.bound]: bound, + }), + }, + }, + extensions: [ + StarterKit, + Placeholder.configure({ + emptyEditorClass: styles.empty, + placeholder: placeholder, + }), + NoNewLine, + FilterMention.configure({ + renderLabel({ options, node }) { + return `${node.attrs.id.name[locale] ?? node.attrs.id.granblue_en}` + }, + suggestion: mentionSuggestionOptions, + HTMLAttributes: { + class: classNames({ + [styles.mention]: true, + }), + }, + }), + ], + onFocus: ({ editor }) => { + console.log('Editor reporting that is focused') + }, + onUpdate: ({ editor }) => { + const mentions = parseMentions(editor.getJSON()) + if (onUpdate) onUpdate(mentions) + }, + }) + + return ( +