Skip to content

Commit 50540e2

Browse files
committed
Lexical: Created mention node, started mention service, split comment editor out
1 parent 1ee5711 commit 50540e2

File tree

6 files changed

+180
-5
lines changed

6 files changed

+180
-5
lines changed

resources/js/components/page-comment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {Component} from './component';
22
import {getLoading, htmlToDom} from '../services/dom';
33
import {PageCommentReference} from "./page-comment-reference";
44
import {HttpError} from "../services/http";
5-
import {SimpleWysiwygEditorInterface} from "../wysiwyg";
5+
import {createCommentEditorInstance, SimpleWysiwygEditorInterface} from "../wysiwyg";
66
import {el} from "../wysiwyg/utils/dom";
77

88
export interface PageCommentReplyEventData {
@@ -104,7 +104,7 @@ export class PageComment extends Component {
104104
this.input.parentElement?.appendChild(container);
105105
this.input.hidden = true;
106106

107-
this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, editorContent, {
107+
this.wysiwygEditor = wysiwygModule.createCommentEditorInstance(container as HTMLElement, editorContent, {
108108
darkMode: document.documentElement.classList.contains('dark-mode'),
109109
textDirection: this.$opts.textDirection,
110110
translations: (window as unknown as Record<string, Object>).editor_translations,

resources/js/components/page-comments.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {PageCommentReference} from "./page-comment-reference";
55
import {scrollAndHighlightElement} from "../services/util";
66
import {PageCommentArchiveEventData, PageCommentReplyEventData} from "./page-comment";
77
import {el} from "../wysiwyg/utils/dom";
8-
import {SimpleWysiwygEditorInterface} from "../wysiwyg";
8+
import {createCommentEditorInstance, SimpleWysiwygEditorInterface} from "../wysiwyg";
99

1010
export class PageComments extends Component {
1111

@@ -200,7 +200,7 @@ export class PageComments extends Component {
200200
this.formInput.parentElement?.appendChild(container);
201201
this.formInput.hidden = true;
202202

203-
this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, '<p></p>', {
203+
this.wysiwygEditor = wysiwygModule.createCommentEditorInstance(container as HTMLElement, '<p></p>', {
204204
darkMode: document.documentElement.classList.contains('dark-mode'),
205205
textDirection: this.wysiwygTextDirection,
206206
translations: (window as unknown as Record<string, Object>).editor_translations,

resources/js/wysiwyg/index.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import {createEditor} from 'lexical';
22
import {createEmptyHistoryState, registerHistory} from '@lexical/history';
33
import {registerRichText} from '@lexical/rich-text';
44
import {mergeRegister} from '@lexical/utils';
5-
import {getNodesForBasicEditor, getNodesForPageEditor, registerCommonNodeMutationListeners} from './nodes';
5+
import {
6+
getNodesForBasicEditor,
7+
getNodesForCommentEditor,
8+
getNodesForPageEditor,
9+
registerCommonNodeMutationListeners
10+
} from './nodes';
611
import {buildEditorUI} from "./ui";
712
import {focusEditor, getEditorContentAsHtml, setEditorContentFromHtml} from "./utils/actions";
813
import {registerTableResizer} from "./ui/framework/helpers/table-resizer";
@@ -22,6 +27,7 @@ import {DiagramDecorator} from "./ui/decorators/diagram";
2227
import {registerMouseHandling} from "./services/mouse-handling";
2328
import {registerSelectionHandling} from "./services/selection-handling";
2429
import {EditorApi} from "./api/api";
30+
import {registerMentions} from "./services/mentions";
2531

2632
const theme = {
2733
text: {
@@ -136,6 +142,43 @@ export function createBasicEditorInstance(container: HTMLElement, htmlContent: s
136142
return new SimpleWysiwygEditorInterface(context);
137143
}
138144

145+
export function createCommentEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
146+
const editor = createEditor({
147+
namespace: 'BookStackCommentEditor',
148+
nodes: getNodesForCommentEditor(),
149+
onError: console.error,
150+
theme: theme,
151+
});
152+
153+
// TODO - Dedupe this with the basic editor instance
154+
// Changed elements: namespace, registerMentions, toolbar, public event usage
155+
const context: EditorUiContext = buildEditorUI(container, editor, options);
156+
editor.setRootElement(context.editorDOM);
157+
158+
const editorTeardown = mergeRegister(
159+
registerRichText(editor),
160+
registerHistory(editor, createEmptyHistoryState(), 300),
161+
registerShortcuts(context),
162+
registerAutoLinks(editor),
163+
registerMentions(editor),
164+
);
165+
166+
// Register toolbars, modals & decorators
167+
context.manager.setToolbar(getBasicEditorToolbar(context));
168+
context.manager.registerContextToolbar('link', contextToolbars.link);
169+
context.manager.registerModal('link', modals.link);
170+
context.manager.onTeardown(editorTeardown);
171+
172+
setEditorContentFromHtml(editor, htmlContent);
173+
174+
window.$events.emitPublic(container, 'editor-wysiwyg::post-init', {
175+
usage: 'comment-editor',
176+
api: new EditorApi(context),
177+
});
178+
179+
return new SimpleWysiwygEditorInterface(context);
180+
}
181+
139182
export class SimpleWysiwygEditorInterface {
140183
protected context: EditorUiContext;
141184
protected onChangeListeners: (() => void)[] = [];
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {
2+
DOMConversion,
3+
DOMConversionMap, DOMConversionOutput,
4+
type EditorConfig,
5+
ElementNode,
6+
LexicalEditor, LexicalNode,
7+
SerializedElementNode,
8+
Spread
9+
} from "lexical";
10+
11+
export type SerializedMentionNode = Spread<{
12+
user_id: number;
13+
user_name: string;
14+
user_slug: string;
15+
}, SerializedElementNode>
16+
17+
export class MentionNode extends ElementNode {
18+
__user_id: number = 0;
19+
__user_name: string = '';
20+
__user_slug: string = '';
21+
22+
static getType(): string {
23+
return 'mention';
24+
}
25+
26+
static clone(node: MentionNode): MentionNode {
27+
const newNode = new MentionNode(node.__key);
28+
newNode.__user_id = node.__user_id;
29+
newNode.__user_name = node.__user_name;
30+
newNode.__user_slug = node.__user_slug;
31+
return newNode;
32+
}
33+
34+
setUserDetails(userId: number, userName: string, userSlug: string): void {
35+
const self = this.getWritable();
36+
self.__user_id = userId;
37+
self.__user_name = userName;
38+
self.__user_slug = userSlug;
39+
}
40+
41+
isInline(): boolean {
42+
return true;
43+
}
44+
45+
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
46+
const element = document.createElement('a');
47+
element.setAttribute('target', '_blank');
48+
element.setAttribute('href', window.baseUrl('/users/' + this.__user_slug));
49+
element.setAttribute('data-user-mention-id', String(this.__user_id));
50+
element.textContent = '@' + this.__user_name;
51+
return element;
52+
}
53+
54+
updateDOM(prevNode: MentionNode): boolean {
55+
return prevNode.__user_id !== this.__user_id;
56+
}
57+
58+
static importDOM(): DOMConversionMap|null {
59+
return {
60+
a(node: HTMLElement): DOMConversion|null {
61+
if (node.hasAttribute('data-user-mention-id')) {
62+
return {
63+
conversion: (element: HTMLElement): DOMConversionOutput|null => {
64+
const node = new MentionNode();
65+
node.setUserDetails(
66+
Number(element.getAttribute('data-user-mention-id') || '0'),
67+
element.innerText.replace(/^@/, ''),
68+
element.getAttribute('href')?.split('/user/')[1] || ''
69+
);
70+
71+
return {
72+
node,
73+
};
74+
},
75+
priority: 4,
76+
};
77+
}
78+
return null;
79+
},
80+
};
81+
}
82+
83+
exportJSON(): SerializedMentionNode {
84+
return {
85+
...super.exportJSON(),
86+
type: 'mention',
87+
version: 1,
88+
user_id: this.__user_id,
89+
user_name: this.__user_name,
90+
user_slug: this.__user_slug,
91+
};
92+
}
93+
94+
static importJSON(serializedNode: SerializedMentionNode): MentionNode {
95+
return $createMentionNode(serializedNode.user_id, serializedNode.user_name, serializedNode.user_slug);
96+
}
97+
}
98+
99+
export function $createMentionNode(userId: number, userName: string, userSlug: string) {
100+
const node = new MentionNode();
101+
node.setUserDetails(userId, userName, userSlug);
102+
return node;
103+
}
104+
105+
export function $isMentionNode(node: LexicalNode | null | undefined): node is MentionNode {
106+
return node instanceof MentionNode;
107+
}

resources/js/wysiwyg/nodes.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {MediaNode} from "@lexical/rich-text/LexicalMediaNode";
1919
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
2020
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
2121
import {CaptionNode} from "@lexical/table/LexicalCaptionNode";
22+
import {MentionNode} from "@lexical/link/LexicalMentionNode";
2223

2324
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
2425
return [
@@ -51,6 +52,13 @@ export function getNodesForBasicEditor(): (KlassConstructor<typeof LexicalNode>
5152
];
5253
}
5354

55+
export function getNodesForCommentEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
56+
return [
57+
...getNodesForBasicEditor(),
58+
MentionNode,
59+
];
60+
}
61+
5462
export function registerCommonNodeMutationListeners(context: EditorUiContext): void {
5563
const decorated = [ImageNode, CodeBlockNode, DiagramNode];
5664

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {LexicalEditor, TextNode} from "lexical";
2+
3+
4+
export function registerMentions(editor: LexicalEditor): () => void {
5+
6+
const unregisterTransform = editor.registerNodeTransform(TextNode, (node: TextNode) =>{
7+
console.log(node);
8+
// TODO - If last character is @, show autocomplete selector list of users.
9+
// Filter list by any extra characters entered.
10+
// On enter, replace with name mention element.
11+
// On space/escape, hide autocomplete list.
12+
});
13+
14+
return (): void => {
15+
unregisterTransform();
16+
};
17+
}

0 commit comments

Comments
 (0)