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 {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
Loading…
Reference in a new issue