mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-12-19 10:42:29 +03:00
Lexical: Created mention node, started mention service, split comment editor out
This commit is contained in:
@@ -2,7 +2,7 @@ import {Component} from './component';
|
||||
import {getLoading, htmlToDom} from '../services/dom';
|
||||
import {PageCommentReference} from "./page-comment-reference";
|
||||
import {HttpError} from "../services/http";
|
||||
import {SimpleWysiwygEditorInterface} from "../wysiwyg";
|
||||
import {createCommentEditorInstance, SimpleWysiwygEditorInterface} from "../wysiwyg";
|
||||
import {el} from "../wysiwyg/utils/dom";
|
||||
|
||||
export interface PageCommentReplyEventData {
|
||||
@@ -104,7 +104,7 @@ export class PageComment extends Component {
|
||||
this.input.parentElement?.appendChild(container);
|
||||
this.input.hidden = true;
|
||||
|
||||
this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, editorContent, {
|
||||
this.wysiwygEditor = wysiwygModule.createCommentEditorInstance(container as HTMLElement, editorContent, {
|
||||
darkMode: document.documentElement.classList.contains('dark-mode'),
|
||||
textDirection: this.$opts.textDirection,
|
||||
translations: (window as unknown as Record<string, Object>).editor_translations,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {PageCommentReference} from "./page-comment-reference";
|
||||
import {scrollAndHighlightElement} from "../services/util";
|
||||
import {PageCommentArchiveEventData, PageCommentReplyEventData} from "./page-comment";
|
||||
import {el} from "../wysiwyg/utils/dom";
|
||||
import {SimpleWysiwygEditorInterface} from "../wysiwyg";
|
||||
import {createCommentEditorInstance, SimpleWysiwygEditorInterface} from "../wysiwyg";
|
||||
|
||||
export class PageComments extends Component {
|
||||
|
||||
@@ -200,7 +200,7 @@ export class PageComments extends Component {
|
||||
this.formInput.parentElement?.appendChild(container);
|
||||
this.formInput.hidden = true;
|
||||
|
||||
this.wysiwygEditor = wysiwygModule.createBasicEditorInstance(container as HTMLElement, '<p></p>', {
|
||||
this.wysiwygEditor = wysiwygModule.createCommentEditorInstance(container as HTMLElement, '<p></p>', {
|
||||
darkMode: document.documentElement.classList.contains('dark-mode'),
|
||||
textDirection: this.wysiwygTextDirection,
|
||||
translations: (window as unknown as Record<string, Object>).editor_translations,
|
||||
|
||||
@@ -2,7 +2,12 @@ import {createEditor} from 'lexical';
|
||||
import {createEmptyHistoryState, registerHistory} from '@lexical/history';
|
||||
import {registerRichText} from '@lexical/rich-text';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {getNodesForBasicEditor, getNodesForPageEditor, registerCommonNodeMutationListeners} from './nodes';
|
||||
import {
|
||||
getNodesForBasicEditor,
|
||||
getNodesForCommentEditor,
|
||||
getNodesForPageEditor,
|
||||
registerCommonNodeMutationListeners
|
||||
} from './nodes';
|
||||
import {buildEditorUI} from "./ui";
|
||||
import {focusEditor, getEditorContentAsHtml, setEditorContentFromHtml} from "./utils/actions";
|
||||
import {registerTableResizer} from "./ui/framework/helpers/table-resizer";
|
||||
@@ -22,6 +27,7 @@ import {DiagramDecorator} from "./ui/decorators/diagram";
|
||||
import {registerMouseHandling} from "./services/mouse-handling";
|
||||
import {registerSelectionHandling} from "./services/selection-handling";
|
||||
import {EditorApi} from "./api/api";
|
||||
import {registerMentions} from "./services/mentions";
|
||||
|
||||
const theme = {
|
||||
text: {
|
||||
@@ -136,6 +142,43 @@ export function createBasicEditorInstance(container: HTMLElement, htmlContent: s
|
||||
return new SimpleWysiwygEditorInterface(context);
|
||||
}
|
||||
|
||||
export function createCommentEditorInstance(container: HTMLElement, htmlContent: string, options: Record<string, any> = {}): SimpleWysiwygEditorInterface {
|
||||
const editor = createEditor({
|
||||
namespace: 'BookStackCommentEditor',
|
||||
nodes: getNodesForCommentEditor(),
|
||||
onError: console.error,
|
||||
theme: theme,
|
||||
});
|
||||
|
||||
// TODO - Dedupe this with the basic editor instance
|
||||
// Changed elements: namespace, registerMentions, toolbar, public event usage
|
||||
const context: EditorUiContext = buildEditorUI(container, editor, options);
|
||||
editor.setRootElement(context.editorDOM);
|
||||
|
||||
const editorTeardown = mergeRegister(
|
||||
registerRichText(editor),
|
||||
registerHistory(editor, createEmptyHistoryState(), 300),
|
||||
registerShortcuts(context),
|
||||
registerAutoLinks(editor),
|
||||
registerMentions(editor),
|
||||
);
|
||||
|
||||
// Register toolbars, modals & decorators
|
||||
context.manager.setToolbar(getBasicEditorToolbar(context));
|
||||
context.manager.registerContextToolbar('link', contextToolbars.link);
|
||||
context.manager.registerModal('link', modals.link);
|
||||
context.manager.onTeardown(editorTeardown);
|
||||
|
||||
setEditorContentFromHtml(editor, htmlContent);
|
||||
|
||||
window.$events.emitPublic(container, 'editor-wysiwyg::post-init', {
|
||||
usage: 'comment-editor',
|
||||
api: new EditorApi(context),
|
||||
});
|
||||
|
||||
return new SimpleWysiwygEditorInterface(context);
|
||||
}
|
||||
|
||||
export class SimpleWysiwygEditorInterface {
|
||||
protected context: EditorUiContext;
|
||||
protected onChangeListeners: (() => void)[] = [];
|
||||
|
||||
107
resources/js/wysiwyg/lexical/link/LexicalMentionNode.ts
Normal file
107
resources/js/wysiwyg/lexical/link/LexicalMentionNode.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
DOMConversion,
|
||||
DOMConversionMap, DOMConversionOutput,
|
||||
type EditorConfig,
|
||||
ElementNode,
|
||||
LexicalEditor, LexicalNode,
|
||||
SerializedElementNode,
|
||||
Spread
|
||||
} from "lexical";
|
||||
|
||||
export type SerializedMentionNode = Spread<{
|
||||
user_id: number;
|
||||
user_name: string;
|
||||
user_slug: string;
|
||||
}, SerializedElementNode>
|
||||
|
||||
export class MentionNode extends ElementNode {
|
||||
__user_id: number = 0;
|
||||
__user_name: string = '';
|
||||
__user_slug: string = '';
|
||||
|
||||
static getType(): string {
|
||||
return 'mention';
|
||||
}
|
||||
|
||||
static clone(node: MentionNode): MentionNode {
|
||||
const newNode = new MentionNode(node.__key);
|
||||
newNode.__user_id = node.__user_id;
|
||||
newNode.__user_name = node.__user_name;
|
||||
newNode.__user_slug = node.__user_slug;
|
||||
return newNode;
|
||||
}
|
||||
|
||||
setUserDetails(userId: number, userName: string, userSlug: string): void {
|
||||
const self = this.getWritable();
|
||||
self.__user_id = userId;
|
||||
self.__user_name = userName;
|
||||
self.__user_slug = userSlug;
|
||||
}
|
||||
|
||||
isInline(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute('target', '_blank');
|
||||
element.setAttribute('href', window.baseUrl('/users/' + this.__user_slug));
|
||||
element.setAttribute('data-user-mention-id', String(this.__user_id));
|
||||
element.textContent = '@' + this.__user_name;
|
||||
return element;
|
||||
}
|
||||
|
||||
updateDOM(prevNode: MentionNode): boolean {
|
||||
return prevNode.__user_id !== this.__user_id;
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap|null {
|
||||
return {
|
||||
a(node: HTMLElement): DOMConversion|null {
|
||||
if (node.hasAttribute('data-user-mention-id')) {
|
||||
return {
|
||||
conversion: (element: HTMLElement): DOMConversionOutput|null => {
|
||||
const node = new MentionNode();
|
||||
node.setUserDetails(
|
||||
Number(element.getAttribute('data-user-mention-id') || '0'),
|
||||
element.innerText.replace(/^@/, ''),
|
||||
element.getAttribute('href')?.split('/user/')[1] || ''
|
||||
);
|
||||
|
||||
return {
|
||||
node,
|
||||
};
|
||||
},
|
||||
priority: 4,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
exportJSON(): SerializedMentionNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'mention',
|
||||
version: 1,
|
||||
user_id: this.__user_id,
|
||||
user_name: this.__user_name,
|
||||
user_slug: this.__user_slug,
|
||||
};
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedMentionNode): MentionNode {
|
||||
return $createMentionNode(serializedNode.user_id, serializedNode.user_name, serializedNode.user_slug);
|
||||
}
|
||||
}
|
||||
|
||||
export function $createMentionNode(userId: number, userName: string, userSlug: string) {
|
||||
const node = new MentionNode();
|
||||
node.setUserDetails(userId, userName, userSlug);
|
||||
return node;
|
||||
}
|
||||
|
||||
export function $isMentionNode(node: LexicalNode | null | undefined): node is MentionNode {
|
||||
return node instanceof MentionNode;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {MediaNode} from "@lexical/rich-text/LexicalMediaNode";
|
||||
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
|
||||
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
|
||||
import {CaptionNode} from "@lexical/table/LexicalCaptionNode";
|
||||
import {MentionNode} from "@lexical/link/LexicalMentionNode";
|
||||
|
||||
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
|
||||
return [
|
||||
@@ -51,6 +52,13 @@ export function getNodesForBasicEditor(): (KlassConstructor<typeof LexicalNode>
|
||||
];
|
||||
}
|
||||
|
||||
export function getNodesForCommentEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
|
||||
return [
|
||||
...getNodesForBasicEditor(),
|
||||
MentionNode,
|
||||
];
|
||||
}
|
||||
|
||||
export function registerCommonNodeMutationListeners(context: EditorUiContext): void {
|
||||
const decorated = [ImageNode, CodeBlockNode, DiagramNode];
|
||||
|
||||
|
||||
17
resources/js/wysiwyg/services/mentions.ts
Normal file
17
resources/js/wysiwyg/services/mentions.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {LexicalEditor, TextNode} from "lexical";
|
||||
|
||||
|
||||
export function registerMentions(editor: LexicalEditor): () => void {
|
||||
|
||||
const unregisterTransform = editor.registerNodeTransform(TextNode, (node: TextNode) =>{
|
||||
console.log(node);
|
||||
// TODO - If last character is @, show autocomplete selector list of users.
|
||||
// Filter list by any extra characters entered.
|
||||
// On enter, replace with name mention element.
|
||||
// On space/escape, hide autocomplete list.
|
||||
});
|
||||
|
||||
return (): void => {
|
||||
unregisterTransform();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user