mirror of
https://github.com/docmost/docmost.git
synced 2026-05-18 15:34:05 +08:00
57efb91bd3
* feat: ai chat * feat: ai chat * sync * cleanup * view space button
174 lines
4.6 KiB
TypeScript
174 lines
4.6 KiB
TypeScript
import { ReactRenderer, useEditor } from "@tiptap/react";
|
|
import {
|
|
autoUpdate,
|
|
computePosition,
|
|
flip,
|
|
offset,
|
|
shift,
|
|
} from "@floating-ui/dom";
|
|
import MentionList from "@/features/editor/components/mention/mention-list.tsx";
|
|
|
|
function getWhitespaceCount(query: string) {
|
|
const matches = query?.match(/([\s]+)/g);
|
|
return matches?.length || 0;
|
|
}
|
|
|
|
const mentionRenderItems = () => {
|
|
let component: ReactRenderer | null = null;
|
|
let activeClientRect: (() => DOMRect) | null = null;
|
|
let updatePositionCleanup: (() => void) | null = null;
|
|
let outsideClickHandler: ((e: MouseEvent) => void) | null = null;
|
|
|
|
const destroy = () => {
|
|
if (outsideClickHandler) {
|
|
document.removeEventListener("pointerdown", outsideClickHandler);
|
|
outsideClickHandler = null;
|
|
}
|
|
updatePositionCleanup?.();
|
|
updatePositionCleanup = null;
|
|
component?.destroy();
|
|
if (component?.element?.parentNode) {
|
|
component.element.parentNode.removeChild(component.element);
|
|
}
|
|
component = null;
|
|
};
|
|
|
|
return {
|
|
onStart: (props: {
|
|
editor: ReturnType<typeof useEditor>;
|
|
clientRect: () => DOMRect;
|
|
query: string;
|
|
}) => {
|
|
// query must not start with a whitespace
|
|
if (props.query.charAt(0) === " ") {
|
|
return;
|
|
}
|
|
|
|
// don't render component if space between the search query words is greater than 4
|
|
const whitespaceCount = getWhitespaceCount(props.query);
|
|
if (whitespaceCount > 4) {
|
|
return;
|
|
}
|
|
|
|
const editorDom = props.editor?.view?.dom;
|
|
const asideEl = editorDom?.closest(".mantine-AppShell-aside");
|
|
const dialogEl = editorDom?.closest("[data-comment-dialog]");
|
|
const chatInput = editorDom?.closest("[data-chat-input]");
|
|
const isInCommentContext = !!(asideEl || dialogEl || chatInput);
|
|
|
|
component = new ReactRenderer(MentionList, {
|
|
props: { ...props, isInCommentContext },
|
|
editor: props.editor,
|
|
});
|
|
|
|
if (!props.clientRect) {
|
|
return;
|
|
}
|
|
|
|
activeClientRect = props.clientRect;
|
|
|
|
const { element } = component;
|
|
document.body.appendChild(element);
|
|
|
|
outsideClickHandler = (e: MouseEvent) => {
|
|
const target = e.target as Node;
|
|
if (element && !element.contains(target)) {
|
|
destroy();
|
|
}
|
|
};
|
|
document.addEventListener("pointerdown", outsideClickHandler);
|
|
|
|
const shiftMiddleware = asideEl
|
|
? shift({ boundary: asideEl, crossAxis: true, padding: 8 })
|
|
: shift();
|
|
|
|
updatePositionCleanup = autoUpdate(
|
|
{
|
|
getBoundingClientRect: () =>
|
|
activeClientRect ? activeClientRect() : new DOMRect(),
|
|
},
|
|
element,
|
|
() => {
|
|
if (!component?.element) return;
|
|
computePosition(
|
|
{
|
|
getBoundingClientRect: () => {
|
|
return activeClientRect ? activeClientRect() : new DOMRect();
|
|
},
|
|
},
|
|
element,
|
|
{
|
|
placement: "bottom-start",
|
|
middleware: [offset(4), flip(), shiftMiddleware],
|
|
},
|
|
).then(({ x, y }) => {
|
|
Object.assign(element.style, {
|
|
left: `${x}px`,
|
|
top: `${y}px`,
|
|
position: "absolute",
|
|
zIndex: "190",
|
|
});
|
|
});
|
|
},
|
|
);
|
|
},
|
|
onUpdate: (props: {
|
|
editor: ReturnType<typeof useEditor>;
|
|
clientRect: () => DOMRect;
|
|
query: string;
|
|
}) => {
|
|
// query must not start with a whitespace
|
|
if (props.query.charAt(0) === " ") {
|
|
destroy();
|
|
return;
|
|
}
|
|
|
|
// only update component if popup is not destroyed
|
|
if (component) {
|
|
component.updateProps(props);
|
|
}
|
|
|
|
if (!props || !props.clientRect) {
|
|
return;
|
|
}
|
|
|
|
activeClientRect = props.clientRect;
|
|
|
|
const whitespaceCount = getWhitespaceCount(props.query);
|
|
|
|
// destroy component if space is greater 3 without a match
|
|
if (
|
|
whitespaceCount > 4 &&
|
|
//@ts-ignore
|
|
props.editor.storage.mentionItems.length === 1
|
|
) {
|
|
destroy();
|
|
return;
|
|
}
|
|
// fallback exit
|
|
if (whitespaceCount > 7) {
|
|
destroy();
|
|
return;
|
|
}
|
|
},
|
|
onKeyDown: (props: { event: KeyboardEvent }) => {
|
|
if (props.event.key === "Escape") {
|
|
destroy();
|
|
return true;
|
|
}
|
|
|
|
if (props.event.key === "Enter" && !component) {
|
|
destroy();
|
|
return false;
|
|
}
|
|
|
|
return (component?.ref as any)?.onKeyDown(props);
|
|
},
|
|
onExit: () => {
|
|
destroy();
|
|
},
|
|
};
|
|
};
|
|
|
|
export default mentionRenderItems;
|