mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-12-19 10:42:29 +03:00
Lexical: Changed mention to be a decorator node
Allows better selection. Also updated existing decorator file names to align with classes so they're easier to find. Also aligned/fixed decorator constuctor/setup methods.
This commit is contained in:
@@ -19,7 +19,7 @@ class HtmlDescriptionFilter
|
||||
*/
|
||||
protected static array $allowedAttrsByElements = [
|
||||
'p' => [],
|
||||
'a' => ['href', 'title', 'target'],
|
||||
'a' => ['href', 'title', 'target', 'data-mention-user-id'],
|
||||
'ol' => [],
|
||||
'ul' => [],
|
||||
'li' => [],
|
||||
|
||||
@@ -22,12 +22,13 @@ import {registerKeyboardHandling} from "./services/keyboard-handling";
|
||||
import {registerAutoLinks} from "./services/auto-links";
|
||||
import {contextToolbars, getBasicEditorToolbar, getMainEditorFullToolbar} from "./ui/defaults/toolbars";
|
||||
import {modals} from "./ui/defaults/modals";
|
||||
import {CodeBlockDecorator} from "./ui/decorators/code-block";
|
||||
import {DiagramDecorator} from "./ui/decorators/diagram";
|
||||
import {CodeBlockDecorator} from "./ui/decorators/CodeBlockDecorator";
|
||||
import {DiagramDecorator} from "./ui/decorators/DiagramDecorator";
|
||||
import {registerMouseHandling} from "./services/mouse-handling";
|
||||
import {registerSelectionHandling} from "./services/selection-handling";
|
||||
import {EditorApi} from "./api/api";
|
||||
import {registerMentions} from "./services/mentions";
|
||||
import {MentionDecorator} from "./ui/decorators/MentionDecorator";
|
||||
|
||||
const theme = {
|
||||
text: {
|
||||
@@ -151,7 +152,7 @@ export function createCommentEditorInstance(container: HTMLElement, htmlContent:
|
||||
});
|
||||
|
||||
// TODO - Dedupe this with the basic editor instance
|
||||
// Changed elements: namespace, registerMentions, toolbar, public event usage
|
||||
// Changed elements: namespace, registerMentions, toolbar, public event usage, mentioned decorator
|
||||
const context: EditorUiContext = buildEditorUI(container, editor, options);
|
||||
editor.setRootElement(context.editorDOM);
|
||||
|
||||
@@ -168,6 +169,7 @@ export function createCommentEditorInstance(container: HTMLElement, htmlContent:
|
||||
context.manager.registerContextToolbar('link', contextToolbars.link);
|
||||
context.manager.registerModal('link', modals.link);
|
||||
context.manager.onTeardown(editorTeardown);
|
||||
context.manager.registerDecoratorType('mention', MentionDecorator);
|
||||
|
||||
setEditorContentFromHtml(editor, htmlContent);
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import {
|
||||
DecoratorNode,
|
||||
DOMConversion,
|
||||
DOMConversionMap, DOMConversionOutput,
|
||||
DOMConversionMap, DOMConversionOutput, DOMExportOutput,
|
||||
type EditorConfig,
|
||||
ElementNode,
|
||||
LexicalEditor, LexicalNode,
|
||||
SerializedElementNode,
|
||||
SerializedLexicalNode,
|
||||
Spread
|
||||
} from "lexical";
|
||||
import {EditorDecoratorAdapter} from "../../ui/framework/decorator";
|
||||
|
||||
export type SerializedMentionNode = Spread<{
|
||||
user_id: number;
|
||||
user_name: string;
|
||||
user_slug: string;
|
||||
}, SerializedElementNode>
|
||||
}, SerializedLexicalNode>
|
||||
|
||||
export class MentionNode extends ElementNode {
|
||||
export class MentionNode extends DecoratorNode<EditorDecoratorAdapter> {
|
||||
__user_id: number = 0;
|
||||
__user_name: string = '';
|
||||
__user_slug: string = '';
|
||||
@@ -22,7 +23,6 @@ export class MentionNode extends ElementNode {
|
||||
static getType(): string {
|
||||
return 'mention';
|
||||
}
|
||||
|
||||
static clone(node: MentionNode): MentionNode {
|
||||
const newNode = new MentionNode(node.__key);
|
||||
newNode.__user_id = node.__user_id;
|
||||
@@ -42,12 +42,24 @@ export class MentionNode extends ElementNode {
|
||||
return true;
|
||||
}
|
||||
|
||||
isParentRequired(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
|
||||
return {
|
||||
type: 'mention',
|
||||
getNode: () => this,
|
||||
};
|
||||
}
|
||||
|
||||
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.setAttribute('href', window.baseUrl('/user/' + this.__user_slug));
|
||||
element.setAttribute('data-mention-user-id', String(this.__user_id));
|
||||
element.textContent = '@' + this.__user_name;
|
||||
// element.setAttribute('contenteditable', 'false');
|
||||
return element;
|
||||
}
|
||||
|
||||
@@ -55,21 +67,30 @@ export class MentionNode extends ElementNode {
|
||||
return prevNode.__user_id !== this.__user_id;
|
||||
}
|
||||
|
||||
exportDOM(editor: LexicalEditor): DOMExportOutput {
|
||||
const element = this.createDOM(editor._config, editor);
|
||||
// element.removeAttribute('contenteditable');
|
||||
return {element};
|
||||
}
|
||||
|
||||
static importDOM(): DOMConversionMap|null {
|
||||
return {
|
||||
a(node: HTMLElement): DOMConversion|null {
|
||||
if (node.hasAttribute('data-user-mention-id')) {
|
||||
if (node.hasAttribute('data-mention-user-id')) {
|
||||
return {
|
||||
conversion: (element: HTMLElement): DOMConversionOutput|null => {
|
||||
const node = new MentionNode();
|
||||
node.setUserDetails(
|
||||
Number(element.getAttribute('data-user-mention-id') || '0'),
|
||||
Number(element.getAttribute('data-mention-user-id') || '0'),
|
||||
element.innerText.replace(/^@/, ''),
|
||||
element.getAttribute('href')?.split('/user/')[1] || ''
|
||||
);
|
||||
|
||||
return {
|
||||
node,
|
||||
after(childNodes): LexicalNode[] {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: 4,
|
||||
@@ -82,7 +103,6 @@ export class MentionNode extends ElementNode {
|
||||
|
||||
exportJSON(): SerializedMentionNode {
|
||||
return {
|
||||
...super.exportJSON(),
|
||||
type: 'mention',
|
||||
version: 1,
|
||||
user_id: this.__user_id,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import {
|
||||
$createTextNode,
|
||||
$getSelection, $isRangeSelection,
|
||||
COMMAND_PRIORITY_NORMAL, RangeSelection, TextNode
|
||||
} from "lexical";
|
||||
import {KEY_AT_COMMAND} from "lexical/LexicalCommands";
|
||||
import {$createMentionNode} from "@lexical/link/LexicalMentionNode";
|
||||
import {el, htmlToDom} from "../utils/dom";
|
||||
import {EditorUiContext} from "../ui/framework/core";
|
||||
import {debounce} from "../../services/util";
|
||||
import {removeLoading, showLoading} from "../../services/dom";
|
||||
import {MentionDecorator} from "../ui/decorators/MentionDecorator";
|
||||
|
||||
|
||||
function enterUserSelectMode(context: EditorUiContext, selection: RangeSelection) {
|
||||
@@ -32,126 +29,13 @@ function enterUserSelectMode(context: EditorUiContext, selection: RangeSelection
|
||||
|
||||
const mention = $createMentionNode(0, '', '');
|
||||
newNode.replace(mention);
|
||||
mention.select();
|
||||
|
||||
const revertEditorMention = () => {
|
||||
context.editor.update(() => {
|
||||
const text = $createTextNode('@');
|
||||
mention.replace(text);
|
||||
text.selectEnd();
|
||||
});
|
||||
};
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const mentionDOM = context.editor.getElementByKey(mention.getKey());
|
||||
if (!mentionDOM) {
|
||||
revertEditorMention();
|
||||
return;
|
||||
const mentionDecorator = context.manager.getDecoratorByNodeKey(mention.getKey());
|
||||
if (mentionDecorator instanceof MentionDecorator) {
|
||||
mentionDecorator.showSelection()
|
||||
}
|
||||
|
||||
const selectList = buildAndShowUserSelectorAtElement(context, mentionDOM);
|
||||
handleUserListLoading(selectList);
|
||||
handleUserSelectCancel(context, selectList, revertEditorMention);
|
||||
});
|
||||
|
||||
|
||||
// TODO - On enter, replace with name mention element.
|
||||
}
|
||||
|
||||
function handleUserSelectCancel(context: EditorUiContext, selectList: HTMLElement, revertEditorMention: () => void) {
|
||||
const controller = new AbortController();
|
||||
|
||||
const onCancel = () => {
|
||||
revertEditorMention();
|
||||
selectList.remove();
|
||||
controller.abort();
|
||||
}
|
||||
|
||||
selectList.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
onCancel();
|
||||
}
|
||||
}, {signal: controller.signal});
|
||||
|
||||
const input = selectList.querySelector('input') as HTMLInputElement;
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Backspace' && input.value === '') {
|
||||
onCancel();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}, {signal: controller.signal});
|
||||
|
||||
context.editorDOM.addEventListener('click', (event) => {
|
||||
onCancel()
|
||||
}, {signal: controller.signal});
|
||||
context.editorDOM.addEventListener('keydown', (event) => {
|
||||
onCancel();
|
||||
}, {signal: controller.signal});
|
||||
}
|
||||
|
||||
function handleUserListLoading(selectList: HTMLElement) {
|
||||
const cache = new Map<string, string>();
|
||||
|
||||
const updateUserList = async (searchTerm: string) => {
|
||||
// Empty list
|
||||
for (const child of [...selectList.children].slice(1)) {
|
||||
child.remove();
|
||||
}
|
||||
|
||||
// Fetch new content
|
||||
let responseHtml = '';
|
||||
if (cache.has(searchTerm)) {
|
||||
responseHtml = cache.get(searchTerm) || '';
|
||||
} else {
|
||||
const loadingWrap = el('li');
|
||||
showLoading(loadingWrap);
|
||||
selectList.appendChild(loadingWrap);
|
||||
|
||||
const resp = await window.$http.get(`/search/users/mention?search=${searchTerm}`);
|
||||
responseHtml = resp.data as string;
|
||||
cache.set(searchTerm, responseHtml);
|
||||
loadingWrap.remove();
|
||||
}
|
||||
|
||||
const doc = htmlToDom(responseHtml);
|
||||
const toInsert = doc.querySelectorAll('li');
|
||||
for (const listEl of toInsert) {
|
||||
const adopted = window.document.adoptNode(listEl) as HTMLElement;
|
||||
selectList.appendChild(adopted);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Initial load
|
||||
updateUserList('');
|
||||
|
||||
const input = selectList.querySelector('input') as HTMLInputElement;
|
||||
const updateUserListDebounced = debounce(updateUserList, 200, false);
|
||||
input.addEventListener('input', () => {
|
||||
const searchTerm = input.value;
|
||||
updateUserListDebounced(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
function buildAndShowUserSelectorAtElement(context: EditorUiContext, mentionDOM: HTMLElement): HTMLElement {
|
||||
const searchInput = el('input', {type: 'text'});
|
||||
const searchItem = el('li', {}, [searchInput]);
|
||||
const userSelect = el('ul', {class: 'suggestion-box dropdown-menu'}, [searchItem]);
|
||||
|
||||
context.containerDOM.appendChild(userSelect);
|
||||
|
||||
userSelect.style.display = 'block';
|
||||
userSelect.style.top = '0';
|
||||
userSelect.style.left = '0';
|
||||
const mentionPos = mentionDOM.getBoundingClientRect();
|
||||
const userSelectPos = userSelect.getBoundingClientRect();
|
||||
userSelect.style.top = `${mentionPos.bottom - userSelectPos.top + 3}px`;
|
||||
userSelect.style.left = `${mentionPos.left - userSelectPos.left}px`;
|
||||
|
||||
searchInput.focus();
|
||||
|
||||
return userSelect;
|
||||
}
|
||||
|
||||
export function registerMentions(context: EditorUiContext): () => void {
|
||||
|
||||
@@ -14,7 +14,7 @@ export class CodeBlockDecorator extends EditorDecorator {
|
||||
// @ts-ignore
|
||||
protected editor: any = null;
|
||||
|
||||
setup(context: EditorUiContext, element: HTMLElement) {
|
||||
setup(element: HTMLElement) {
|
||||
const codeNode = this.getNode() as CodeBlockNode;
|
||||
const preEl = element.querySelector('pre');
|
||||
if (!preEl) {
|
||||
@@ -35,24 +35,24 @@ export class CodeBlockDecorator extends EditorDecorator {
|
||||
|
||||
element.addEventListener('click', event => {
|
||||
requestAnimationFrame(() => {
|
||||
context.editor.update(() => {
|
||||
this.context.editor.update(() => {
|
||||
$selectSingleNode(this.getNode());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
element.addEventListener('dblclick', event => {
|
||||
context.editor.getEditorState().read(() => {
|
||||
$openCodeEditorForNode(context.editor, (this.getNode() as CodeBlockNode));
|
||||
this.context.editor.getEditorState().read(() => {
|
||||
$openCodeEditorForNode(this.context.editor, (this.getNode() as CodeBlockNode));
|
||||
});
|
||||
});
|
||||
|
||||
const selectionChange = (selection: BaseSelection|null): void => {
|
||||
element.classList.toggle('selected', $selectionContainsNode(selection, codeNode));
|
||||
};
|
||||
context.manager.onSelectionChange(selectionChange);
|
||||
this.context.manager.onSelectionChange(selectionChange);
|
||||
this.onDestroy(() => {
|
||||
context.manager.offSelectionChange(selectionChange);
|
||||
this.context.manager.offSelectionChange(selectionChange);
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
@@ -89,11 +89,11 @@ export class CodeBlockDecorator extends EditorDecorator {
|
||||
}
|
||||
}
|
||||
|
||||
render(context: EditorUiContext, element: HTMLElement): void {
|
||||
render(element: HTMLElement): void {
|
||||
if (this.completedSetup) {
|
||||
this.update();
|
||||
} else {
|
||||
this.setup(context, element);
|
||||
this.setup(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,33 +9,33 @@ import {$openDrawingEditorForNode} from "../../utils/diagrams";
|
||||
export class DiagramDecorator extends EditorDecorator {
|
||||
protected completedSetup: boolean = false;
|
||||
|
||||
setup(context: EditorUiContext, element: HTMLElement) {
|
||||
setup(element: HTMLElement) {
|
||||
const diagramNode = this.getNode();
|
||||
element.classList.add('editor-diagram');
|
||||
|
||||
context.editor.registerCommand(CLICK_COMMAND, (event: MouseEvent): boolean => {
|
||||
this.context.editor.registerCommand(CLICK_COMMAND, (event: MouseEvent): boolean => {
|
||||
if (!element.contains(event.target as HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
context.editor.update(() => {
|
||||
this.context.editor.update(() => {
|
||||
$selectSingleNode(this.getNode());
|
||||
});
|
||||
return true;
|
||||
}, COMMAND_PRIORITY_NORMAL);
|
||||
|
||||
element.addEventListener('dblclick', event => {
|
||||
context.editor.getEditorState().read(() => {
|
||||
$openDrawingEditorForNode(context, (this.getNode() as DiagramNode));
|
||||
this.context.editor.getEditorState().read(() => {
|
||||
$openDrawingEditorForNode(this.context, (this.getNode() as DiagramNode));
|
||||
});
|
||||
});
|
||||
|
||||
const selectionChange = (selection: BaseSelection|null): void => {
|
||||
element.classList.toggle('selected', $selectionContainsNode(selection, diagramNode));
|
||||
};
|
||||
context.manager.onSelectionChange(selectionChange);
|
||||
this.context.manager.onSelectionChange(selectionChange);
|
||||
this.onDestroy(() => {
|
||||
context.manager.offSelectionChange(selectionChange);
|
||||
this.context.manager.offSelectionChange(selectionChange);
|
||||
});
|
||||
|
||||
this.completedSetup = true;
|
||||
@@ -45,11 +45,11 @@ export class DiagramDecorator extends EditorDecorator {
|
||||
//
|
||||
}
|
||||
|
||||
render(context: EditorUiContext, element: HTMLElement): void {
|
||||
render(element: HTMLElement): void {
|
||||
if (this.completedSetup) {
|
||||
this.update();
|
||||
} else {
|
||||
this.setup(context, element);
|
||||
this.setup(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
172
resources/js/wysiwyg/ui/decorators/MentionDecorator.ts
Normal file
172
resources/js/wysiwyg/ui/decorators/MentionDecorator.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import {EditorDecorator} from "../framework/decorator";
|
||||
import {EditorUiContext} from "../framework/core";
|
||||
import {el, htmlToDom} from "../../utils/dom";
|
||||
import {showLoading} from "../../../services/dom";
|
||||
import {MentionNode} from "@lexical/link/LexicalMentionNode";
|
||||
import {debounce} from "../../../services/util";
|
||||
import {$createTextNode} from "lexical";
|
||||
|
||||
function userClickHandler(onSelect: (id: number, name: string, slug: string)=>void): (event: PointerEvent) => void {
|
||||
return (event: PointerEvent) => {
|
||||
const userItem = (event.target as HTMLElement).closest('a[data-id]') as HTMLAnchorElement | null;
|
||||
if (!userItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = Number(userItem.dataset.id || '0');
|
||||
const name = userItem.dataset.name || '';
|
||||
const slug = userItem.dataset.slug || '';
|
||||
|
||||
onSelect(id, name, slug);
|
||||
event.preventDefault();
|
||||
};
|
||||
}
|
||||
|
||||
function handleUserSelectCancel(context: EditorUiContext, selectList: HTMLElement, controller: AbortController, onCancel: () => void): void {
|
||||
selectList.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
onCancel();
|
||||
}
|
||||
}, {signal: controller.signal});
|
||||
|
||||
const input = selectList.querySelector('input') as HTMLInputElement;
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Backspace' && input.value === '') {
|
||||
onCancel();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}, {signal: controller.signal});
|
||||
|
||||
context.editorDOM.addEventListener('click', (event) => {
|
||||
onCancel()
|
||||
}, {signal: controller.signal});
|
||||
context.editorDOM.addEventListener('keydown', (event) => {
|
||||
onCancel();
|
||||
}, {signal: controller.signal});
|
||||
}
|
||||
|
||||
function handleUserListLoading(selectList: HTMLElement) {
|
||||
const cache = new Map<string, string>();
|
||||
|
||||
const updateUserList = async (searchTerm: string) => {
|
||||
// Empty list
|
||||
for (const child of [...selectList.children].slice(1)) {
|
||||
child.remove();
|
||||
}
|
||||
|
||||
// Fetch new content
|
||||
let responseHtml = '';
|
||||
if (cache.has(searchTerm)) {
|
||||
responseHtml = cache.get(searchTerm) || '';
|
||||
} else {
|
||||
const loadingWrap = el('li');
|
||||
showLoading(loadingWrap);
|
||||
selectList.appendChild(loadingWrap);
|
||||
|
||||
const resp = await window.$http.get(`/search/users/mention?search=${searchTerm}`);
|
||||
responseHtml = resp.data as string;
|
||||
cache.set(searchTerm, responseHtml);
|
||||
loadingWrap.remove();
|
||||
}
|
||||
|
||||
const doc = htmlToDom(responseHtml);
|
||||
const toInsert = doc.querySelectorAll('li');
|
||||
for (const listEl of toInsert) {
|
||||
const adopted = window.document.adoptNode(listEl) as HTMLElement;
|
||||
selectList.appendChild(adopted);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Initial load
|
||||
updateUserList('');
|
||||
|
||||
const input = selectList.querySelector('input') as HTMLInputElement;
|
||||
const updateUserListDebounced = debounce(updateUserList, 200, false);
|
||||
input.addEventListener('input', () => {
|
||||
const searchTerm = input.value;
|
||||
updateUserListDebounced(searchTerm);
|
||||
});
|
||||
}
|
||||
|
||||
function buildAndShowUserSelectorAtElement(context: EditorUiContext, mentionDOM: HTMLElement): HTMLElement {
|
||||
const searchInput = el('input', {type: 'text'});
|
||||
const searchItem = el('li', {}, [searchInput]);
|
||||
const userSelect = el('ul', {class: 'suggestion-box dropdown-menu'}, [searchItem]);
|
||||
|
||||
context.containerDOM.appendChild(userSelect);
|
||||
|
||||
userSelect.style.display = 'block';
|
||||
userSelect.style.top = '0';
|
||||
userSelect.style.left = '0';
|
||||
const mentionPos = mentionDOM.getBoundingClientRect();
|
||||
const userSelectPos = userSelect.getBoundingClientRect();
|
||||
userSelect.style.top = `${mentionPos.bottom - userSelectPos.top + 3}px`;
|
||||
userSelect.style.left = `${mentionPos.left - userSelectPos.left}px`;
|
||||
|
||||
searchInput.focus();
|
||||
|
||||
return userSelect;
|
||||
}
|
||||
|
||||
export class MentionDecorator extends EditorDecorator {
|
||||
protected completedSetup: boolean = false;
|
||||
protected abortController: AbortController | null = null;
|
||||
protected selectList: HTMLElement | null = null;
|
||||
protected mentionElement: HTMLElement | null = null;
|
||||
|
||||
setup(element: HTMLElement) {
|
||||
this.mentionElement = element;
|
||||
this.completedSetup = true;
|
||||
}
|
||||
|
||||
showSelection() {
|
||||
if (!this.mentionElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideSelection();
|
||||
this.abortController = new AbortController();
|
||||
|
||||
this.selectList = buildAndShowUserSelectorAtElement(this.context, this.mentionElement);
|
||||
handleUserListLoading(this.selectList);
|
||||
|
||||
this.selectList.addEventListener('click', userClickHandler((id, name, slug) => {
|
||||
this.context.editor.update(() => {
|
||||
const mentionNode = this.getNode() as MentionNode;
|
||||
this.hideSelection();
|
||||
mentionNode.setUserDetails(id, name, slug);
|
||||
mentionNode.selectNext();
|
||||
});
|
||||
}), {signal: this.abortController.signal});
|
||||
|
||||
handleUserSelectCancel(this.context, this.selectList, this.abortController, this.revertMention.bind(this));
|
||||
}
|
||||
|
||||
hideSelection() {
|
||||
this.abortController?.abort();
|
||||
this.selectList?.remove();
|
||||
}
|
||||
|
||||
revertMention() {
|
||||
this.hideSelection();
|
||||
this.context.editor.update(() => {
|
||||
const text = $createTextNode('@');
|
||||
this.getNode().replace(text);
|
||||
text.selectEnd();
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
//
|
||||
}
|
||||
|
||||
render(element: HTMLElement): void {
|
||||
if (this.completedSetup) {
|
||||
this.update();
|
||||
} else {
|
||||
this.setup(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export abstract class EditorDecorator {
|
||||
* If an element is returned, this will be appended to the element
|
||||
* that is being decorated.
|
||||
*/
|
||||
abstract render(context: EditorUiContext, decorated: HTMLElement): HTMLElement|void;
|
||||
abstract render(decorated: HTMLElement): HTMLElement|void;
|
||||
|
||||
/**
|
||||
* Destroy this decorator. Used for tear-down operations upon destruction
|
||||
|
||||
@@ -90,7 +90,7 @@ export class EditorUIManager {
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const decorator = new decoratorClass(nodeKey);
|
||||
const decorator = new decoratorClass(this.getContext());
|
||||
this.decoratorInstancesByNodeKey[nodeKey] = decorator;
|
||||
return decorator;
|
||||
}
|
||||
@@ -262,7 +262,7 @@ export class EditorUIManager {
|
||||
const adapter = decorators[key];
|
||||
const decorator = this.getDecorator(adapter.type, key);
|
||||
decorator.setNode(adapter.getNode());
|
||||
const decoratorEl = decorator.render(this.getContext(), decoratedEl);
|
||||
const decoratorEl = decorator.render(decoratedEl);
|
||||
if (decoratorEl) {
|
||||
decoratedEl.append(decoratorEl);
|
||||
}
|
||||
|
||||
@@ -199,3 +199,22 @@ body .page-content img,
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
a[data-mention-user-id] {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
color: var(--color-link);
|
||||
padding: 0.1em 0.2em;
|
||||
&:after {
|
||||
content: '';
|
||||
background-color: currentColor;
|
||||
opacity: 0.2;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user