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:
36
resources/js/wysiwyg/helpers.ts
Normal file
36
resources/js/wysiwyg/helpers.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
@ -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');
|
||||
// });
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
45
resources/js/wysiwyg/ui/editor-button.ts
Normal file
45
resources/js/wysiwyg/ui/editor-button.ts
Normal 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);
|
||||
}
|
||||
}
|
51
resources/js/wysiwyg/ui/index.ts
Normal file
51
resources/js/wysiwyg/ui/index.ts
Normal 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);
|
||||
}
|
Reference in New Issue
Block a user