1
0
Fork 0
forked from Mirrors/gomuks

web/composer: refactor autocompleter to be generic

This commit is contained in:
Tulir Asokan 2024-10-24 01:48:12 +03:00
parent d31b0905ed
commit 0696a43208
2 changed files with 34 additions and 14 deletions

View file

@ -13,9 +13,9 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { useEffect } from "react"
import { JSX, useEffect } from "react"
import { RoomStateStore } from "@/api/statestore"
import { useFilteredEmojis } from "@/util/emoji"
import { Emoji, useFilteredEmojis } from "@/util/emoji"
import useEvent from "@/util/useEvent.ts"
import type { ComposerState } from "./MessageComposer.tsx"
import "./Autocompleter.css"
@ -39,20 +39,29 @@ export interface AutocompleterProps {
const positiveMod = (val: number, div: number) => (val % div + div) % div
export const EmojiAutocompleter = ({ params, state, setState, setAutocomplete }: AutocompleterProps) => {
const emojis = useFilteredEmojis((params.frozenQuery ?? params.query).slice(1), true)
interface InnerAutocompleterProps<T> extends AutocompleterProps {
items: T[]
getText: (item: T) => string
getKey: (item: T) => string
render: (item: T) => JSX.Element
}
function useAutocompleter<T>({
params, state, setState, setAutocomplete,
items, getText, getKey, render,
}: InnerAutocompleterProps<T>) {
const onSelect = useEvent((index: number) => {
if (emojis.length === 0) {
if (items.length === 0) {
return
}
index = positiveMod(index, emojis.length)
const emoji = emojis[index]
index = positiveMod(index, items.length)
const replacementText = getText(items[index])
setState({
text: state.text.slice(0, params.startPos) + emoji.u + state.text.slice(params.endPos),
text: state.text.slice(0, params.startPos) + replacementText + state.text.slice(params.endPos),
})
setAutocomplete({
...params,
endPos: params.startPos + emoji.u.length,
endPos: params.startPos + replacementText.length,
frozenQuery: params.frozenQuery ?? params.query,
})
document.querySelector(`div.autocompletion-item[data-index='${index}']`)?.scrollIntoView({ block: "nearest" })
@ -69,17 +78,28 @@ export const EmojiAutocompleter = ({ params, state, setState, setAutocomplete }:
onSelect(params.selected)
}
}, [onSelect, params.selected])
const selected = params.selected !== undefined ? positiveMod(params.selected, emojis.length) : -1
const selected = params.selected !== undefined ? positiveMod(params.selected, items.length) : -1
return <div className="autocompletions">
{emojis.map((emoji, i) => <div
{items.map((item, i) => <div
onClick={onClick}
data-index={i}
className={`autocompletion-item ${selected === i ? "selected" : ""}`}
key={emoji.u}
>{emoji.u} :{emoji.n}:</div>)}
key={getKey(item)}
>{render(item)}</div>)}
</div>
}
const emojiFuncs = {
getText: (emoji: Emoji) => emoji.u,
getKey: (emoji: Emoji) => emoji.u,
render: (emoji: Emoji) => <>{emoji.u} :{emoji.n}:</>,
}
export const EmojiAutocompleter = ({ params, ...rest }: AutocompleterProps) => {
const items = useFilteredEmojis((params.frozenQuery ?? params.query).slice(1), true)
return useAutocompleter({ params, ...rest, items, ...emojiFuncs })
}
export const UserAutocompleter = ({ params }: AutocompleterProps) => {
return <div className="autocompletions">
Autocomplete {params.type} {params.query}

View file

@ -16,7 +16,7 @@
import { useRef } from "react"
import data from "./data.json"
interface Emoji {
export interface Emoji {
u: string // Unicode codepoint
c: number // Category number
t: string // Emoji title