mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
web/composer: refactor autocompleter to be generic
This commit is contained in:
parent
d31b0905ed
commit
0696a43208
2 changed files with 34 additions and 14 deletions
|
@ -13,9 +13,9 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// 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 { RoomStateStore } from "@/api/statestore"
|
||||||
import { useFilteredEmojis } from "@/util/emoji"
|
import { Emoji, useFilteredEmojis } from "@/util/emoji"
|
||||||
import useEvent from "@/util/useEvent.ts"
|
import useEvent from "@/util/useEvent.ts"
|
||||||
import type { ComposerState } from "./MessageComposer.tsx"
|
import type { ComposerState } from "./MessageComposer.tsx"
|
||||||
import "./Autocompleter.css"
|
import "./Autocompleter.css"
|
||||||
|
@ -39,20 +39,29 @@ export interface AutocompleterProps {
|
||||||
|
|
||||||
const positiveMod = (val: number, div: number) => (val % div + div) % div
|
const positiveMod = (val: number, div: number) => (val % div + div) % div
|
||||||
|
|
||||||
export const EmojiAutocompleter = ({ params, state, setState, setAutocomplete }: AutocompleterProps) => {
|
interface InnerAutocompleterProps<T> extends AutocompleterProps {
|
||||||
const emojis = useFilteredEmojis((params.frozenQuery ?? params.query).slice(1), true)
|
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) => {
|
const onSelect = useEvent((index: number) => {
|
||||||
if (emojis.length === 0) {
|
if (items.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
index = positiveMod(index, emojis.length)
|
index = positiveMod(index, items.length)
|
||||||
const emoji = emojis[index]
|
const replacementText = getText(items[index])
|
||||||
setState({
|
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({
|
setAutocomplete({
|
||||||
...params,
|
...params,
|
||||||
endPos: params.startPos + emoji.u.length,
|
endPos: params.startPos + replacementText.length,
|
||||||
frozenQuery: params.frozenQuery ?? params.query,
|
frozenQuery: params.frozenQuery ?? params.query,
|
||||||
})
|
})
|
||||||
document.querySelector(`div.autocompletion-item[data-index='${index}']`)?.scrollIntoView({ block: "nearest" })
|
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)
|
||||||
}
|
}
|
||||||
}, [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">
|
return <div className="autocompletions">
|
||||||
{emojis.map((emoji, i) => <div
|
{items.map((item, i) => <div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
data-index={i}
|
data-index={i}
|
||||||
className={`autocompletion-item ${selected === i ? "selected" : ""}`}
|
className={`autocompletion-item ${selected === i ? "selected" : ""}`}
|
||||||
key={emoji.u}
|
key={getKey(item)}
|
||||||
>{emoji.u} :{emoji.n}:</div>)}
|
>{render(item)}</div>)}
|
||||||
</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) => {
|
export const UserAutocompleter = ({ params }: AutocompleterProps) => {
|
||||||
return <div className="autocompletions">
|
return <div className="autocompletions">
|
||||||
Autocomplete {params.type} {params.query}
|
Autocomplete {params.type} {params.query}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
import { useRef } from "react"
|
import { useRef } from "react"
|
||||||
import data from "./data.json"
|
import data from "./data.json"
|
||||||
|
|
||||||
interface Emoji {
|
export interface Emoji {
|
||||||
u: string // Unicode codepoint
|
u: string // Unicode codepoint
|
||||||
c: number // Category number
|
c: number // Category number
|
||||||
t: string // Emoji title
|
t: string // Emoji title
|
||||||
|
|
Loading…
Add table
Reference in a new issue