v3: added emoji picker, various bug and layout fixes
This commit is contained in:
79
src/hooks/useEmojiTrigger.ts
Normal file
79
src/hooks/useEmojiTrigger.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { type RefObject, useCallback, useRef, useState } from "react";
|
||||
|
||||
// Trigger: ':' not preceded by a word character, followed by 1+ word chars
|
||||
const TRIGGER_RE = /(?<![A-Za-z0-9_]):([A-Za-z0-9_+\-]{1,})$/;
|
||||
|
||||
interface EmojiTriggerState {
|
||||
open: boolean;
|
||||
query: string;
|
||||
}
|
||||
|
||||
export function useEmojiTrigger(
|
||||
value: string,
|
||||
onChange: (value: string) => void,
|
||||
textareaRef: RefObject<HTMLTextAreaElement | null>,
|
||||
) {
|
||||
const [state, setState] = useState<EmojiTriggerState>({
|
||||
open: false,
|
||||
query: "",
|
||||
});
|
||||
// Saved positions at the moment the trigger was detected
|
||||
const triggerStartRef = useRef(0);
|
||||
const triggerCursorRef = useRef(0);
|
||||
|
||||
/**
|
||||
* Call this from the textarea's onChange (alongside the mention handler).
|
||||
* It receives the new value and cursor position already read from the event.
|
||||
*/
|
||||
const detectEmojiTrigger = useCallback(
|
||||
(newValue: string, cursor: number) => {
|
||||
const textBefore = newValue.slice(0, cursor);
|
||||
const match = TRIGGER_RE.exec(textBefore);
|
||||
if (match) {
|
||||
triggerStartRef.current = match.index;
|
||||
triggerCursorRef.current = cursor;
|
||||
setState({ open: true, query: match[1] });
|
||||
} else {
|
||||
setState((s) => s.open ? { open: false, query: "" } : s);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Called when frimousse fires onEmojiSelect.
|
||||
* Replaces the ':query' text with the selected emoji.
|
||||
*/
|
||||
const handleEmojiSelect = useCallback(
|
||||
(native: string) => {
|
||||
const start = triggerStartRef.current;
|
||||
const cursor = triggerCursorRef.current;
|
||||
const newValue = value.slice(0, start) + native + " " +
|
||||
value.slice(cursor);
|
||||
onChange(newValue);
|
||||
setState({ open: false, query: "" });
|
||||
const newPos = start + native.length + 1; // +1 for trailing space
|
||||
requestAnimationFrame(() => {
|
||||
const el = textareaRef.current;
|
||||
if (el) {
|
||||
el.focus();
|
||||
el.setSelectionRange(newPos, newPos);
|
||||
}
|
||||
});
|
||||
},
|
||||
[value, onChange, textareaRef],
|
||||
);
|
||||
|
||||
const closeEmoji = useCallback(() => {
|
||||
setState({ open: false, query: "" });
|
||||
requestAnimationFrame(() => textareaRef.current?.focus());
|
||||
}, [textareaRef]);
|
||||
|
||||
return {
|
||||
emojiOpen: state.open,
|
||||
emojiQuery: state.query,
|
||||
detectEmojiTrigger,
|
||||
handleEmojiSelect,
|
||||
closeEmoji,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user