Implement MentionEditor and MentionTableField

This commit is contained in:
Justin Edmund 2023-07-09 22:47:34 -07:00
parent 1d4f1b6211
commit db2fe44204
6 changed files with 315 additions and 2 deletions

View file

@ -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);
}
}
}
}
}

View file

@ -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 (
<div className={styles.wrapper}>
<EditorContent editor={editor} />
</div>
)
}
function parseMentions(data: JSONContent) {
const mentions: string[] = (data.content || []).flatMap(parseMentions)
if (data.type === 'mention') {
const granblueId = data.attrs?.id.granblue_id
mentions.push(granblueId)
}
return [...new Set(mentions)]
}
MentionEditor.defaultProps = {
bound: false,
}
export default MentionEditor

View file

@ -0,0 +1,40 @@
import { useEffect, useState } from 'react'
import MentionEditor from '~components/common/MentionEditor'
import TableField from '~components/common/TableField'
import styles from './index.module.scss'
interface Props
extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
label: string
description?: string
placeholder?: string
onUpdate: (content: string[]) => void
}
const MentionTableField = ({
label,
description,
placeholder,
...props
}: Props) => {
return (
<TableField
{...props}
name={props.name || ''}
className="mention"
label={label}
>
<MentionEditor
bound={true}
placeholder={placeholder}
onUpdate={props.onUpdate}
/>
</TableField>
)
}
export default MentionTableField

View file

@ -44,6 +44,10 @@
}
}
&.mention {
grid-template-columns: 1fr 2fr;
}
.left {
align-items: center;
display: flex;

View file

@ -18,6 +18,7 @@ import SelectItem from '~components/common/SelectItem'
import type { DialogProps } from '@radix-ui/react-dialog'
import styles from './index.module.scss'
import MentionTableField from '~components/common/MentionTableField'
interface Props extends DialogProps {
defaultFilterSet: FilterSet
@ -47,6 +48,8 @@ const FilterModal = (props: Props) => {
const [chargeAttackOpen, setChargeAttackOpen] = useState(false)
const [fullAutoOpen, setFullAutoOpen] = useState(false)
const [autoGuardOpen, setAutoGuardOpen] = useState(false)
const [inclusions, setInclusions] = useState<string[]>([])
const [exclusions, setExclusions] = useState<string[]>([])
const [filterSet, setFilterSet] = useState<FilterSet>({})
// Filter states
@ -131,6 +134,9 @@ const FilterModal = (props: Props) => {
setCookie('filters', filters, { path: '/' })
}
if (inclusions.length > 0) filters.includes = inclusions.join(',')
if (exclusions.length > 0) filters.excludes = exclusions.join(',')
props.sendAdvancedFilters(filters)
openChange()
}
@ -384,6 +390,27 @@ const FilterModal = (props: Props) => {
/>
)
// Inclusions and exclusions
const inclusionField = (
<MentionTableField
name="inclusion"
description={t('modals.filters.descriptions.inclusion')}
placeholder={t('modals.filters.placeholders.included')}
label={t('modals.filters.labels.inclusion')}
onUpdate={(value) => setInclusions(value)}
/>
)
const exclusionField = (
<MentionTableField
name="exclusion"
description={t('modals.filters.descriptions.exclusion')}
placeholder={t('modals.filters.placeholders.excluded')}
label={t('modals.filters.labels.exclusion')}
onUpdate={(value) => setExclusions(value)}
/>
)
const filterNotice = () => {
if (props.persistFilters) return null
return (
@ -404,14 +431,16 @@ const FilterModal = (props: Props) => {
<DialogContent
className="filter"
wrapperClassName="filter"
headerref={headerRef}
footerref={footerRef}
headerRef={headerRef}
footerRef={footerRef}
onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus}
>
<DialogHeader title={t('modals.filters.title')} />
<div className={styles.fields}>
{filterNotice()}
{inclusionField}
{exclusionField}
{chargeAttackField()}
{fullAutoField()}
{autoGuardField()}