diff --git a/src/nodes/MentionNode.ts b/src/nodes/MentionNode.ts
index 04fab42..d38fdf8 100644
--- a/src/nodes/MentionNode.ts
+++ b/src/nodes/MentionNode.ts
@@ -22,17 +22,26 @@ export type SerializedMentionNode = Spread<
typeof SerializedTextNode
>;
+type PopoverCard = {
+ card: HTMLElement;
+ leftOffset: number;
+ topOffset: number;
+};
+
const mentionStyle = 'background-color: rgba(24, 119, 232, 0.2)';
+
export class MentionNode extends TextNode {
__mention: string;
+ __popoverCard: PopoverCard;
static getType(): string {
return 'mention';
}
static clone(node: MentionNode): MentionNode {
- return new MentionNode(node.__mention, node.__text, node.__key);
+ return new MentionNode(node.__mention, undefined, node.__text, node.__key);
}
+
static importJSON(serializedNode: SerializedMentionNode): MentionNode {
const node = $createMentionNode(serializedNode.mentionName);
node.setTextContent(serializedNode.text);
@@ -43,9 +52,20 @@ export class MentionNode extends TextNode {
return node;
}
- constructor(mentionName: string, text?: string, key?: NodeKey) {
+ constructor(
+ mentionName: string,
+ popover?: PopoverCard,
+ text?: string,
+ key?: NodeKey
+ ) {
super(text ?? mentionName, key);
this.__mention = mentionName;
+ if (popover !== undefined) {
+ this.__popoverCard = popover;
+ this.__popoverCard.card.id = 'verbum-mention-popover';
+ this.__popoverCard.card.style.position = 'absolute';
+ this.removePopover();
+ }
}
exportJSON(): SerializedMentionNode {
@@ -57,10 +77,29 @@ export class MentionNode extends TextNode {
};
}
+ removePopover = () => {
+ const existingPopover = document.getElementById(this.__popoverCard.card.id);
+ if (existingPopover && existingPopover.parentElement) {
+ existingPopover.parentElement.removeChild(existingPopover);
+ }
+ };
+
createDOM(config: EditorConfig): HTMLElement {
const dom = super.createDOM(config);
dom.style.cssText = mentionStyle;
dom.className = 'mention';
+
+ dom.addEventListener('pointerover', (event) => {
+ const { left, top } = dom.getBoundingClientRect();
+ this.__popoverCard.card.style.left = `${left - this.__popoverCard.leftOffset}px`;
+ this.__popoverCard.card.style.top = `${top - this.__popoverCard.topOffset}px`;
+ document.body.appendChild(this.__popoverCard.card);
+ });
+
+ dom.addEventListener('pointerout', (event) => {
+ this.removePopover();
+ });
+
return dom;
}
@@ -69,8 +108,11 @@ export class MentionNode extends TextNode {
}
}
-export function $createMentionNode(mentionName: string): MentionNode {
- const mentionNode = new MentionNode(mentionName);
+export function $createMentionNode(
+ mentionName: string,
+ popover?: PopoverCard
+): MentionNode {
+ const mentionNode = new MentionNode(mentionName, popover);
mentionNode.setMode('segmented').toggleDirectionless();
return mentionNode;
}
diff --git a/src/plugins/MentionsPlugin.tsx b/src/plugins/MentionsPlugin.tsx
index 7643d06..58c16e8 100644
--- a/src/plugins/MentionsPlugin.tsx
+++ b/src/plugins/MentionsPlugin.tsx
@@ -5,11 +5,11 @@ import {
MenuOption,
useBasicTypeaheadTriggerMatch,
} from '@lexical/react/LexicalTypeaheadMenuPlugin';
-import { $createTextNode, TextNode } from 'lexical';
+import { TextNode } from 'lexical';
import { useCallback, useEffect, useMemo, useState } from 'react';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
-
+import { renderToStaticMarkup } from 'react-dom/server';
import { $createMentionNode } from '../nodes/MentionNode';
import { $createAutoLinkNode } from '@lexical/link';
@@ -17,10 +17,26 @@ import './MentionsPlugin.css';
type SearchData = (p: string) => Promise;
+type OffsetCard = {
+ leftOffset: number;
+ topOffset: number;
+};
+
+type PopoverCard = {
+ card: (data: A) => JSX.Element;
+ offset: OffsetCard;
+};
+
+type UserCard = {
+ card: JSX.Element;
+ offset: OffsetCard;
+};
+
type GetTypeaheadValues = (result: A) => {
url: string;
value: string;
picture: JSX.Element;
+ popoverCard?: PopoverCard;
};
const PUNCTUATION =
@@ -181,12 +197,19 @@ class MentionMenuOption extends MenuOption {
name: string;
picture: JSX.Element;
url: string;
-
- constructor(name: string, picture: JSX.Element, url?: string) {
+ userCard: UserCard;
+
+ constructor(
+ name: string,
+ picture: JSX.Element,
+ url?: string,
+ userCard?: UserCard
+ ) {
super(name);
this.name = name;
this.picture = picture;
this.url = url;
+ this.userCard = userCard;
}
}
@@ -249,7 +272,16 @@ export default function MentionsPlugin(props: {
new MentionMenuOption(
getTypeaheadValues(result).value,
getTypeaheadValues(result).picture,
- getTypeaheadValues(result).url
+ getTypeaheadValues(result).url,
+ {
+ card: getTypeaheadValues(result).popoverCard.card(result),
+ offset: {
+ leftOffset:
+ getTypeaheadValues(result).popoverCard.offset.leftOffset,
+ topOffset:
+ getTypeaheadValues(result).popoverCard.offset.topOffset,
+ },
+ }
)
)
.slice(0, SUGGESTION_LIST_LENGTH_LIMIT),
@@ -264,7 +296,17 @@ export default function MentionsPlugin(props: {
) => {
editor.update(() => {
if (nodeToReplace) {
- const mentionNode = $createMentionNode(`@${selectedOption.name}`);
+ const popover = document.createElement('div');
+ const staticElement = renderToStaticMarkup(
+ selectedOption.userCard.card
+ );
+ popover.innerHTML = staticElement;
+
+ const mentionNode = $createMentionNode(`@${selectedOption.name}`, {
+ card: popover,
+ leftOffset: selectedOption.userCard.offset.leftOffset,
+ topOffset: selectedOption.userCard.offset.topOffset,
+ });
const linkNode = $createAutoLinkNode(selectedOption.url);
linkNode.append(mentionNode);
nodeToReplace.replace(linkNode);