mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
* ✨ feat(chat-input): add category-based mention menu with keyboard navigation Replace flat mention list with a structured category menu (Agents, Members, Topics). Supports home/category/search views, Fuse.js fuzzy search, floating-ui positioning, and full keyboard navigation. * 🔧 chore: update @lobehub/editor to version 4.3.0 and refactor type definition in useMentionCategories Signed-off-by: Innei <tukon479@gmail.com> * ✨ feat(MentionMenu): enhance icon rendering logic in MenuItem component Updated the MenuItem component to improve how icons are rendered. Now, it checks if the icon is a valid React element or a function, ensuring better flexibility in icon usage. This change enhances the overall user experience in the mention menu. Signed-off-by: Innei <tukon479@gmail.com> * 🔧 chore: update @lobehub/editor to version 4.3.1 in package.json Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com>
101 lines
2.9 KiB
TypeScript
101 lines
2.9 KiB
TypeScript
import type { VirtualElement } from '@floating-ui/react';
|
|
import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/react';
|
|
import type { RefObject } from 'react';
|
|
import { useLayoutEffect, useState } from 'react';
|
|
|
|
const MENU_GAP = 4;
|
|
const VIEWPORT_MARGIN = 8;
|
|
|
|
interface MenuPosition {
|
|
visible: boolean;
|
|
}
|
|
|
|
const getSelectionRect = () => {
|
|
const selection = window.getSelection();
|
|
|
|
if (!selection?.rangeCount) return new DOMRect();
|
|
|
|
return selection.getRangeAt(0).getBoundingClientRect();
|
|
};
|
|
|
|
const hasActiveEditorSelection = () => {
|
|
const selection = window.getSelection();
|
|
if (!selection?.rangeCount) return false;
|
|
|
|
const activeElement = document.activeElement;
|
|
if (!(activeElement instanceof HTMLElement)) return false;
|
|
|
|
const editor = activeElement.closest('[data-lexical-editor="true"]');
|
|
if (!(editor instanceof HTMLElement)) return false;
|
|
|
|
const anchorNode = selection.anchorNode;
|
|
if (!anchorNode) return false;
|
|
|
|
const anchorElement = anchorNode instanceof Element ? anchorNode : anchorNode.parentElement;
|
|
|
|
return !!anchorElement && editor.contains(anchorElement);
|
|
};
|
|
|
|
const createCaretVirtualElement = (): VirtualElement => ({
|
|
getBoundingClientRect: () => getSelectionRect(),
|
|
});
|
|
|
|
export const useMenuPosition = (menuRef: RefObject<HTMLDivElement | null>, open: boolean) => {
|
|
const { refs, update, x, y } = useFloating({
|
|
middleware: [
|
|
offset(MENU_GAP),
|
|
flip({ fallbackPlacements: ['bottom-start'] }),
|
|
shift({ crossAxis: true, mainAxis: false, padding: VIEWPORT_MARGIN }),
|
|
],
|
|
placement: 'top-start',
|
|
strategy: 'fixed',
|
|
});
|
|
const [position, setPosition] = useState<MenuPosition>({ visible: false });
|
|
|
|
useLayoutEffect(() => {
|
|
if (!open || !menuRef.current) {
|
|
setPosition((prev) => ({ ...prev, visible: false }));
|
|
return;
|
|
}
|
|
|
|
const menu = menuRef.current;
|
|
const reference = createCaretVirtualElement();
|
|
refs.setFloating(menu);
|
|
refs.setPositionReference(reference);
|
|
|
|
const updatePosition = async () => {
|
|
if (!hasActiveEditorSelection()) {
|
|
setPosition({ visible: false });
|
|
return;
|
|
}
|
|
|
|
await update();
|
|
setPosition({ visible: true });
|
|
};
|
|
|
|
const scheduleUpdate = () => {
|
|
requestAnimationFrame(() => {
|
|
void updatePosition();
|
|
});
|
|
};
|
|
|
|
const cleanupAutoUpdate = autoUpdate(reference, menu, scheduleUpdate, {
|
|
animationFrame: true,
|
|
});
|
|
|
|
document.addEventListener('focusin', scheduleUpdate);
|
|
document.addEventListener('focusout', scheduleUpdate);
|
|
document.addEventListener('selectionchange', scheduleUpdate);
|
|
scheduleUpdate();
|
|
|
|
return () => {
|
|
cleanupAutoUpdate();
|
|
document.removeEventListener('focusin', scheduleUpdate);
|
|
document.removeEventListener('focusout', scheduleUpdate);
|
|
document.removeEventListener('selectionchange', scheduleUpdate);
|
|
};
|
|
}, [menuRef, open, refs, update]);
|
|
|
|
return { visible: position.visible, x: x ?? 0, y: y ?? 0 };
|
|
};
|