hensei-web/utils/mentionSuggestions.tsx
Justin Edmund d950d3a935 Add mention components
This adds the code required for us to mention objects in rich text fields like team descriptions.

The mentionSuggestion util fetches data from the server and serves it to MentionList for the user to select, then inserts it into the Editor as a token.
2023-07-05 18:56:32 -07:00

124 lines
3.1 KiB
TypeScript

import { ReactRenderer } from '@tiptap/react'
import { MentionOptions } from '@tiptap/extension-mention'
import { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion'
import tippy, { Instance as TippyInstance } from 'tippy.js'
import {
MentionList,
MentionRef,
MentionSuggestion,
} from '~components/MentionList'
import api from '~utils/api'
import { numberToElement } from '~utils/elements'
interface RawSearchResponse {
searchable_type: string
granblue_id: string
name_en: string
name_jp: string
element: number
}
interface SearchResponse {
name: {
[key: string]: string
en: string
ja: string
}
type: string
granblue_id: string
element: GranblueElement
}
function transform(object: RawSearchResponse) {
const result: SearchResponse = {
name: {
en: object.name_en,
ja: object.name_jp,
},
type: object.searchable_type.toLowerCase(),
granblue_id: object.granblue_id,
element: numberToElement(object.element),
}
return result
}
export const mentionSuggestionOptions: MentionOptions['suggestion'] = {
items: async ({ query }): Promise<MentionSuggestion[]> => {
const response = await api.searchAll(query)
const results = response.data.results
return results
.map((rawObject: RawSearchResponse, index: number) => {
const object = transform(rawObject)
return {
granblue_id: object.granblue_id,
element: object.element,
type: object.type,
name: {
en: object.name.en,
ja: object.name.ja,
},
}
})
.slice(0, 7)
},
render: () => {
let component: ReactRenderer<MentionRef> | undefined
let popup: TippyInstance | undefined
return {
onStart: (props) => {
component = new ReactRenderer(MentionList, {
props,
editor: props.editor,
})
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
content: component.element,
showOnCreate: true,
interactive: true,
trigger: 'manual',
placement: 'bottom-start',
})[0]
},
onUpdate(props) {
component?.updateProps(props)
popup?.setProps({
getReferenceClientRect: props.clientRect,
})
},
onKeyDown(props) {
if (props.event.key === 'Escape') {
popup?.hide()
return true
}
if (!component?.ref) {
return false
}
return component?.ref.onKeyDown(props)
},
onExit() {
popup?.destroy()
component?.destroy()
// Remove references to the old popup and component upon destruction/exit.
// (This should prevent redundant calls to `popup.destroy()`, which Tippy
// warns in the console is a sign of a memory leak, as the `suggestion`
// plugin seems to call `onExit` both when a suggestion menu is closed after
// a user chooses an option, *and* when the editor itself is destroyed.)
popup = undefined
component = undefined
},
}
},
}