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);