mirror of
https://github.com/docmost/docmost.git
synced 2026-05-18 23:44:24 +08:00
657fdf8cb7
* Tiptap3 migration - WIP * fix collaboration * remove unused code * fix flicker * disable duplicate extensions * update tiptap version * Switch to useEditorState - Set shouldRerenderOnTransaction to false * fix editable state * add tippyoptions for reference * merge main * tiptap 3.6.1 * fix bubble menu * fix converter * fix menus * fix collaboration caret css * fix: Set `isInitialized` to force immediate react node view rendering * feat: Migrate tippy.js menus to Floating UI * feat: Update collaboration connection for HocusPocus v3 * fix: Connect/disconnect websocketProvider * cleanup * cleanup * feat: Improved placeholder and upload handling for images * feat: Improved placeholder and upload handling for videos * refactor: Image node and view clean-up * feat: Improved placeholder and upload handling for attachments * fix: Video view styles * fix: Transaction handling on asset upload * fix: Use imageDimensionsFromStream * feat: Multiple file upload, improved placeholders, local previews * fix: Drag & drop, paste upload * fix: Allow media as attachment * * add skeleton pulse animation * add translation strings * fix attachment view responsiveness * fix collab connection status display * Tiptap v3.17.0 * fix suggestion menu exit bug * fix search shortcut * fix history editor css * tiptap 3.17.1 --------- Co-authored-by: Arek Nawo <areknawo@areknawo.com>
151 lines
3.6 KiB
TypeScript
151 lines
3.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;
|
|
|
|
const destroy = () => {
|
|
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;
|
|
}
|
|
|
|
component = new ReactRenderer(MentionList, {
|
|
props,
|
|
editor: props.editor,
|
|
});
|
|
|
|
if (!props.clientRect) {
|
|
return;
|
|
}
|
|
|
|
activeClientRect = props.clientRect;
|
|
|
|
const { element } = component;
|
|
document.body.appendChild(element);
|
|
|
|
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(0), flip(), shift()],
|
|
},
|
|
).then(({ x, y }) => {
|
|
Object.assign(element.style, {
|
|
left: `${x}px`,
|
|
top: `${y}px`,
|
|
position: "absolute",
|
|
zIndex: "9999",
|
|
});
|
|
});
|
|
},
|
|
);
|
|
},
|
|
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;
|