-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #117 from game-node-app/dev
Dev
- Loading branch information
Showing
10 changed files
with
335 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import React, { | ||
forwardRef, | ||
useEffect, | ||
useImperativeHandle, | ||
useState, | ||
} from "react"; | ||
import { SuggestionOptions, SuggestionProps } from "@tiptap/suggestion"; | ||
import { List, Paper } from "@mantine/core"; | ||
|
||
export type SuggestionListRef = { | ||
// For convenience using this SuggestionList from within the | ||
// mentionSuggestionOptions, we'll match the signature of SuggestionOptions's | ||
// `onKeyDown` returned in its `render` function | ||
onKeyDown: NonNullable< | ||
ReturnType< | ||
NonNullable<SuggestionOptions<string>["render"]> | ||
>["onKeyDown"] | ||
>; | ||
}; | ||
|
||
export interface MentionSuggestion { | ||
id: string; | ||
label: string; | ||
} | ||
|
||
const EditorMentionList = forwardRef< | ||
SuggestionListRef, | ||
SuggestionProps<MentionSuggestion> | ||
>((props, ref) => { | ||
const [selectedIndex, setSelectedIndex] = useState(0); | ||
|
||
const selectItem = (index: number) => { | ||
const item = props.items[index]; | ||
|
||
if (item) { | ||
props.command(item); | ||
} | ||
}; | ||
|
||
const upHandler = () => { | ||
setSelectedIndex( | ||
(selectedIndex + props.items.length - 1) % props.items.length, | ||
); | ||
}; | ||
|
||
const downHandler = () => { | ||
setSelectedIndex((selectedIndex + 1) % props.items.length); | ||
}; | ||
|
||
const enterHandler = () => { | ||
selectItem(selectedIndex); | ||
}; | ||
|
||
useEffect(() => setSelectedIndex(0), [props.items]); | ||
|
||
// @ts-ignore | ||
useImperativeHandle(ref, () => ({ | ||
onKeyDown: ({ event }: any) => { | ||
if (event.key === "ArrowUp") { | ||
upHandler(); | ||
return true; | ||
} | ||
|
||
if (event.key === "ArrowDown") { | ||
downHandler(); | ||
return true; | ||
} | ||
|
||
if (event.key === "Enter") { | ||
enterHandler(); | ||
return true; | ||
} | ||
|
||
return false; | ||
}, | ||
})); | ||
|
||
return ( | ||
<Paper> | ||
<List> | ||
{props.items?.map((item, index) => { | ||
return ( | ||
<List.Item | ||
key={item.id} | ||
onClick={() => selectItem(index)} | ||
className={ | ||
index === selectedIndex | ||
? "bg-brand-5 rounded-xs px-1" | ||
: undefined | ||
} | ||
> | ||
{item.label} | ||
</List.Item> | ||
); | ||
})} | ||
</List> | ||
</Paper> | ||
// <div className="dropdown-menu"> | ||
// {props.items.length ? ( | ||
// props.items.map((item, index) => ( | ||
// <button | ||
// className={index === selectedIndex ? "is-selected" : ""} | ||
// key={index} | ||
// onClick={() => selectItem(index)} | ||
// > | ||
// {item.label} | ||
// </button> | ||
// )) | ||
// ) : ( | ||
// <div className="item">No result</div> | ||
// )} | ||
// </div> | ||
); | ||
}); | ||
|
||
EditorMentionList.displayName = "EditorMentionList"; | ||
|
||
export default EditorMentionList; |
114 changes: 114 additions & 0 deletions
114
src/components/general/editor/util/getEditorMentionConfig.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { Mention } from "@tiptap/extension-mention"; | ||
import { ReactRenderer } from "@tiptap/react"; | ||
import EditorMentionList, { | ||
MentionSuggestion, | ||
SuggestionListRef, | ||
} from "@/components/general/editor/EditorMentionList"; | ||
import tippy, { type Instance as TippyInstance } from "tippy.js"; | ||
import { SearchService } from "@/wrapper/search"; | ||
import { Profile, ProfileService } from "@/wrapper/server"; | ||
|
||
export default function getEditorMentionConfig() { | ||
// Poor man's cache | ||
let profiles: Profile[] | undefined = undefined; | ||
|
||
return Mention.configure({ | ||
HTMLAttributes: { | ||
class: "mention", | ||
}, | ||
suggestion: { | ||
items: async ({ query }): Promise<MentionSuggestion[]> => { | ||
try { | ||
if (profiles == undefined) { | ||
profiles = | ||
await ProfileService.profileControllerFindAll(); | ||
} | ||
|
||
return profiles | ||
.map((profile) => ({ | ||
id: profile.userId, | ||
label: profile.username, | ||
})) | ||
.filter((item) => { | ||
return item.label | ||
.toLowerCase() | ||
.startsWith(query.toLowerCase()); | ||
}) | ||
.slice(0, 5); | ||
} catch (err) { | ||
console.error(err); | ||
} | ||
|
||
return []; | ||
}, | ||
render: () => { | ||
let component: ReactRenderer<SuggestionListRef> | undefined = | ||
undefined; | ||
let popup: TippyInstance | undefined; | ||
|
||
return { | ||
onStart: (props) => { | ||
component = new ReactRenderer(EditorMentionList, { | ||
props, | ||
editor: props.editor, | ||
}); | ||
|
||
if (!props.clientRect) { | ||
return; | ||
} | ||
|
||
popup = tippy("body", { | ||
getReferenceClientRect: | ||
props.clientRect as () => DOMRect, | ||
appendTo: () => document.body, | ||
content: component.element, | ||
showOnCreate: true, | ||
interactive: true, | ||
trigger: "manual", | ||
placement: "bottom-start", | ||
})[0]; | ||
}, | ||
|
||
onUpdate(props) { | ||
component?.updateProps(props); | ||
|
||
if (!props.clientRect) { | ||
return; | ||
} | ||
|
||
popup?.setProps({ | ||
getReferenceClientRect: | ||
props.clientRect as () => DOMRect, | ||
}); | ||
}, | ||
|
||
onKeyDown(props) { | ||
if (props.event.key === "Escape") { | ||
popup?.hide(); | ||
|
||
return true; | ||
} | ||
if (component?.ref?.onKeyDown == undefined) { | ||
return false; | ||
} | ||
|
||
return component?.ref?.onKeyDown(props); | ||
}, | ||
|
||
onExit() { | ||
popup?.destroy(); | ||
component?.destroy(); | ||
|
||
// Remove references to the old popup and component upon destruction/exit. | ||
// (This should prevent redundant calls to `popup.destroy()`, which Tippy | ||
// warns in the console is a sign of a memory leak, as the `suggestion` | ||
// plugin seems to call `onExit` both when a suggestion menu is closed after | ||
// a user chooses an option, *and* when the editor itself is destroyed.) | ||
popup = undefined; | ||
component = undefined; | ||
}, | ||
}; | ||
}, | ||
}, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { JSONContent } from "@tiptap/react"; | ||
|
||
function getMentionNodes(item: JSONContent[] | JSONContent): JSONContent[] { | ||
const mentionNodes: JSONContent[] = []; | ||
|
||
if (Array.isArray(item)) { | ||
for (const node of item) { | ||
if (node.type === "mention") { | ||
mentionNodes.push(node); | ||
} else if (Array.isArray(node.content)) { | ||
mentionNodes.push(...getMentionNodes(node.content)); | ||
} | ||
} | ||
} else if (!Array.isArray(item) && Array.isArray(item.content)) { | ||
mentionNodes.push(...getMentionNodes(item.content)); | ||
} | ||
|
||
return mentionNodes; | ||
} | ||
|
||
/** | ||
* Returns the list of possible user ids mentioned in a editor's content. | ||
*/ | ||
export default function getEditorMentions(jsonContent: JSONContent) { | ||
const mentionedUsers: string[] = []; | ||
if (!jsonContent.content) { | ||
return []; | ||
} | ||
|
||
const mentionNodes = getMentionNodes(jsonContent); | ||
|
||
for (const node of mentionNodes) { | ||
if (node && node.attrs && Object.hasOwn(node.attrs, "id")) { | ||
mentionedUsers.push(node.attrs.id); | ||
} | ||
} | ||
|
||
return mentionedUsers; | ||
} |
Oops, something went wrong.