Implement MentionEditor and MentionTableField
This commit is contained in:
parent
1d4f1b6211
commit
db2fe44204
6 changed files with 315 additions and 2 deletions
152
components/common/MentionEditor/index.module.scss
Normal file
152
components/common/MentionEditor/index.module.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
components/common/MentionEditor/index.tsx
Normal file
88
components/common/MentionEditor/index.tsx
Normal 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
|
||||||
0
components/common/MentionTableField/index.module.scss
Normal file
0
components/common/MentionTableField/index.module.scss
Normal file
40
components/common/MentionTableField/index.tsx
Normal file
40
components/common/MentionTableField/index.tsx
Normal 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
|
||||||
|
|
@ -44,6 +44,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mention {
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
}
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import SelectItem from '~components/common/SelectItem'
|
||||||
import type { DialogProps } from '@radix-ui/react-dialog'
|
import type { DialogProps } from '@radix-ui/react-dialog'
|
||||||
|
|
||||||
import styles from './index.module.scss'
|
import styles from './index.module.scss'
|
||||||
|
import MentionTableField from '~components/common/MentionTableField'
|
||||||
|
|
||||||
interface Props extends DialogProps {
|
interface Props extends DialogProps {
|
||||||
defaultFilterSet: FilterSet
|
defaultFilterSet: FilterSet
|
||||||
|
|
@ -47,6 +48,8 @@ const FilterModal = (props: Props) => {
|
||||||
const [chargeAttackOpen, setChargeAttackOpen] = useState(false)
|
const [chargeAttackOpen, setChargeAttackOpen] = useState(false)
|
||||||
const [fullAutoOpen, setFullAutoOpen] = useState(false)
|
const [fullAutoOpen, setFullAutoOpen] = useState(false)
|
||||||
const [autoGuardOpen, setAutoGuardOpen] = useState(false)
|
const [autoGuardOpen, setAutoGuardOpen] = useState(false)
|
||||||
|
const [inclusions, setInclusions] = useState<string[]>([])
|
||||||
|
const [exclusions, setExclusions] = useState<string[]>([])
|
||||||
const [filterSet, setFilterSet] = useState<FilterSet>({})
|
const [filterSet, setFilterSet] = useState<FilterSet>({})
|
||||||
|
|
||||||
// Filter states
|
// Filter states
|
||||||
|
|
@ -131,6 +134,9 @@ const FilterModal = (props: Props) => {
|
||||||
setCookie('filters', filters, { path: '/' })
|
setCookie('filters', filters, { path: '/' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inclusions.length > 0) filters.includes = inclusions.join(',')
|
||||||
|
if (exclusions.length > 0) filters.excludes = exclusions.join(',')
|
||||||
|
|
||||||
props.sendAdvancedFilters(filters)
|
props.sendAdvancedFilters(filters)
|
||||||
openChange()
|
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 = () => {
|
const filterNotice = () => {
|
||||||
if (props.persistFilters) return null
|
if (props.persistFilters) return null
|
||||||
return (
|
return (
|
||||||
|
|
@ -404,14 +431,16 @@ const FilterModal = (props: Props) => {
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="filter"
|
className="filter"
|
||||||
wrapperClassName="filter"
|
wrapperClassName="filter"
|
||||||
headerref={headerRef}
|
headerRef={headerRef}
|
||||||
footerref={footerRef}
|
footerRef={footerRef}
|
||||||
onEscapeKeyDown={onEscapeKeyDown}
|
onEscapeKeyDown={onEscapeKeyDown}
|
||||||
onOpenAutoFocus={onOpenAutoFocus}
|
onOpenAutoFocus={onOpenAutoFocus}
|
||||||
>
|
>
|
||||||
<DialogHeader title={t('modals.filters.title')} />
|
<DialogHeader title={t('modals.filters.title')} />
|
||||||
<div className={styles.fields}>
|
<div className={styles.fields}>
|
||||||
{filterNotice()}
|
{filterNotice()}
|
||||||
|
{inclusionField}
|
||||||
|
{exclusionField}
|
||||||
{chargeAttackField()}
|
{chargeAttackField()}
|
||||||
{fullAutoField()}
|
{fullAutoField()}
|
||||||
{autoGuardField()}
|
{autoGuardField()}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue