1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-07-31 15:24:31 +03:00

Lexical: Started UI fundementals with basic button

This commit is contained in:
Dan Brown
2024-05-28 18:04:48 +01:00
parent 0f8bd869d8
commit b24d60e98d
8 changed files with 288 additions and 32 deletions

View File

@ -0,0 +1,36 @@
import {$createParagraphNode, $getSelection, BaseSelection, LexicalEditor} from "lexical";
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes";
import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
import {$setBlocksType} from "@lexical/selection";
export function selectionContainsNodeType(selection: BaseSelection|null, matcher: LexicalNodeMatcher): boolean {
if (!selection) {
return false;
}
for (const node of selection.getNodes()) {
if (matcher(node)) {
return true;
}
for (const parent of node.getParents()) {
if (matcher(parent)) {
return true;
}
}
}
return false;
}
export function toggleSelectionBlockNodeType(editor: LexicalEditor, matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) {
editor.update(() => {
const selection = $getSelection();
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
if (selection && matcher(blockElement)) {
$setBlocksType(selection, $createParagraphNode);
} else {
$setBlocksType(selection, creator);
}
});
}

View File

@ -1,18 +1,10 @@
import {
$createParagraphNode,
$getRoot,
$getSelection,
COMMAND_PRIORITY_LOW,
createCommand,
createEditor, CreateEditorArgs,
} from 'lexical';
import {$getRoot, createEditor, CreateEditorArgs} from 'lexical';
import {createEmptyHistoryState, registerHistory} from '@lexical/history';
import {registerRichText} from '@lexical/rich-text';
import {$getNearestBlockElementAncestorOrThrow, mergeRegister} from '@lexical/utils';
import {mergeRegister} from '@lexical/utils';
import {$generateNodesFromDOM} from '@lexical/html';
import {$setBlocksType} from '@lexical/selection';
import {getNodesForPageEditor} from './nodes';
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from './nodes/callout';
import {buildEditorUI} from "./ui";
export function createPageEditorInstance(editArea: HTMLElement) {
const config: CreateEditorArgs = {
@ -42,25 +34,29 @@ export function createPageEditorInstance(editArea: HTMLElement) {
const debugView = document.getElementById('lexical-debug');
editor.registerUpdateListener(({editorState}) => {
console.log('editorState', editorState.toJSON());
debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2);
if (debugView) {
debugView.textContent = JSON.stringify(editorState.toJSON(), null, 2);
}
});
buildEditorUI(editArea, editor);
// Example of creating, registering and using a custom command
const SET_BLOCK_CALLOUT_COMMAND = createCommand();
editor.registerCommand(SET_BLOCK_CALLOUT_COMMAND, (category: CalloutCategory = 'info') => {
const selection = $getSelection();
const blockElement = $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]);
if ($isCalloutNode(blockElement)) {
$setBlocksType(selection, $createParagraphNode);
} else {
$setBlocksType(selection, () => $createCalloutNode(category));
}
return true;
}, COMMAND_PRIORITY_LOW);
const button = document.getElementById('lexical-button');
button.addEventListener('click', event => {
editor.dispatchCommand(SET_BLOCK_CALLOUT_COMMAND, 'info');
});
// const SET_BLOCK_CALLOUT_COMMAND = createCommand();
// editor.registerCommand(SET_BLOCK_CALLOUT_COMMAND, (category: CalloutCategory = 'info') => {
// const selection = $getSelection();
// const blockElement = $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]);
// if ($isCalloutNode(blockElement)) {
// $setBlocksType(selection, $createParagraphNode);
// } else {
// $setBlocksType(selection, () => $createCalloutNode(category));
// }
// return true;
// }, COMMAND_PRIORITY_LOW);
//
// const button = document.getElementById('lexical-button');
// button.addEventListener('click', event => {
// editor.dispatchCommand(SET_BLOCK_CALLOUT_COMMAND, 'info');
// });
}

View File

@ -33,6 +33,16 @@ export class CalloutNode extends ElementNode {
this.__category = category;
}
setCategory(category: CalloutCategory) {
const self = this.getWritable();
self.__category = category;
}
getCategory(): CalloutCategory {
const self = this.getLatest();
return self.__category;
}
createDOM(_config: EditorConfig, _editor: LexicalEditor) {
const element = document.createElement('p');
element.classList.add('callout', this.__category || '');
@ -112,3 +122,7 @@ export function $createCalloutNode(category: CalloutCategory = 'info') {
export function $isCalloutNode(node: LexicalNode | null | undefined) {
return node instanceof CalloutNode;
}
export function $isCalloutNodeOfCategory(node: LexicalNode | null | undefined, category: CalloutCategory = 'info') {
return node instanceof CalloutNode && (node as CalloutNode).getCategory() === category;
}

View File

@ -1,6 +1,6 @@
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {CalloutNode} from './callout';
import {KlassConstructor, LexicalNode, LexicalNodeReplacement, ParagraphNode} from "lexical";
import {ElementNode, KlassConstructor, LexicalNode, LexicalNodeReplacement, ParagraphNode} from "lexical";
import {CustomParagraphNode} from "./custom-paragraph";
/**
@ -20,3 +20,6 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
}
];
}
export type LexicalNodeMatcher = (node: LexicalNode|null|undefined) => boolean;
export type LexicalElementNodeCreator = () => ElementNode;

View File

@ -0,0 +1,45 @@
import {BaseSelection, LexicalEditor} from "lexical";
export interface EditorButtonDefinition {
label: string;
action: (editor: LexicalEditor) => void;
isActive: (selection: BaseSelection|null) => boolean;
}
export class EditorButton {
#definition: EditorButtonDefinition;
#editor: LexicalEditor;
#dom: HTMLButtonElement;
constructor(definition: EditorButtonDefinition, editor: LexicalEditor) {
this.#definition = definition;
this.#editor = editor;
this.#dom = this.buildDOM();
}
private buildDOM(): HTMLButtonElement {
const button = document.createElement("button");
button.setAttribute('type', 'button');
button.textContent = this.#definition.label;
button.classList.add('editor-toolbar-button');
button.addEventListener('click', event => {
this.runAction();
});
return button;
}
getDOMElement(): HTMLButtonElement {
return this.#dom;
}
runAction() {
this.#definition.action(this.#editor);
}
updateActiveState(selection: BaseSelection|null) {
const isActive = this.#definition.isActive(selection);
this.#dom.classList.toggle('editor-toolbar-button-active', isActive);
}
}

View File

@ -0,0 +1,51 @@
import {
$getSelection,
BaseSelection,
COMMAND_PRIORITY_LOW,
LexicalEditor,
SELECTION_CHANGE_COMMAND
} from "lexical";
import {$createCalloutNode, $isCalloutNodeOfCategory} from "../nodes/callout";
import {selectionContainsNodeType, toggleSelectionBlockNodeType} from "../helpers";
import {EditorButton, EditorButtonDefinition} from "./editor-button";
const calloutButton: EditorButtonDefinition = {
label: 'Info Callout',
action(editor: LexicalEditor) {
toggleSelectionBlockNodeType(
editor,
(node) => $isCalloutNodeOfCategory(node, 'info'),
() => $createCalloutNode('info'),
)
},
isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, 'info'));
}
}
const toolbarButtonDefinitions: EditorButtonDefinition[] = [
calloutButton,
];
export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
const toolbarContainer = document.createElement('div');
toolbarContainer.classList.add('editor-toolbar-container');
const buttons = toolbarButtonDefinitions.map(definition => {
return new EditorButton(definition, editor);
});
const buttonElements = buttons.map(button => button.getDOMElement());
toolbarContainer.append(...buttonElements);
element.before(toolbarContainer);
// Update button states on editor selection change
editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
const selection = $getSelection();
for (const button of buttons) {
button.updateActiveState(selection);
}
return false;
}, COMMAND_PRIORITY_LOW);
}