Lexical: Standardised helper function format

This commit is contained in:
Dan Brown 2024-07-17 16:45:57 +01:00
parent b367490edc
commit 5002a89754
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
5 changed files with 69 additions and 65 deletions

View File

@ -4,7 +4,7 @@ import {
$getSelection, $isElementNode, $getSelection, $isElementNode,
$isTextNode, $setSelection, $isTextNode, $setSelection,
BaseSelection, ElementFormatType, ElementNode, BaseSelection, ElementFormatType, ElementNode,
LexicalEditor, LexicalNode, TextFormatType LexicalNode, TextFormatType
} from "lexical"; } from "lexical";
import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes"; import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes";
import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils"; import {$findMatchingParent, $getNearestBlockElementAncestorOrThrow} from "@lexical/utils";
@ -30,11 +30,11 @@ export function el(tag: string, attrs: Record<string, string|null> = {}, childre
return el; return el;
} }
export function selectionContainsNodeType(selection: BaseSelection|null, matcher: LexicalNodeMatcher): boolean { export function $selectionContainsNodeType(selection: BaseSelection|null, matcher: LexicalNodeMatcher): boolean {
return getNodeFromSelection(selection, matcher) !== null; return $getNodeFromSelection(selection, matcher) !== null;
} }
export function getNodeFromSelection(selection: BaseSelection|null, matcher: LexicalNodeMatcher): LexicalNode|null { export function $getNodeFromSelection(selection: BaseSelection|null, matcher: LexicalNodeMatcher): LexicalNode|null {
if (!selection) { if (!selection) {
return null; return null;
} }
@ -54,7 +54,7 @@ export function getNodeFromSelection(selection: BaseSelection|null, matcher: Lex
return null; return null;
} }
export function selectionContainsTextFormat(selection: BaseSelection|null, format: TextFormatType): boolean { export function $selectionContainsTextFormat(selection: BaseSelection|null, format: TextFormatType): boolean {
if (!selection) { if (!selection) {
return false; return false;
} }
@ -68,19 +68,17 @@ export function selectionContainsTextFormat(selection: BaseSelection|null, forma
return false; return false;
} }
export function toggleSelectionBlockNodeType(editor: LexicalEditor, matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) { export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) {
editor.update(() => { const selection = $getSelection();
const selection = $getSelection(); const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null; if (selection && matcher(blockElement)) {
if (selection && matcher(blockElement)) { $setBlocksType(selection, $createParagraphNode);
$setBlocksType(selection, $createParagraphNode); } else {
} else { $setBlocksType(selection, creator);
$setBlocksType(selection, creator); }
}
});
} }
export function insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: boolean = true) { export function $insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: boolean = true) {
const selection = $getSelection(); const selection = $getSelection();
const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null; const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null;
@ -95,13 +93,13 @@ export function insertNewBlockNodeAtSelection(node: LexicalNode, insertAfter: bo
} }
} }
export function selectSingleNode(node: LexicalNode) { export function $selectSingleNode(node: LexicalNode) {
const nodeSelection = $createNodeSelection(); const nodeSelection = $createNodeSelection();
nodeSelection.add(node.getKey()); nodeSelection.add(node.getKey());
$setSelection(nodeSelection); $setSelection(nodeSelection);
} }
export function selectionContainsNode(selection: BaseSelection|null, node: LexicalNode): boolean { export function $selectionContainsNode(selection: BaseSelection|null, node: LexicalNode): boolean {
if (!selection) { if (!selection) {
return false; return false;
} }
@ -116,8 +114,8 @@ export function selectionContainsNode(selection: BaseSelection|null, node: Lexic
return false; return false;
} }
export function selectionContainsElementFormat(selection: BaseSelection|null, format: ElementFormatType): boolean { export function $selectionContainsElementFormat(selection: BaseSelection|null, format: ElementFormatType): boolean {
const nodes = getBlockElementNodesInSelection(selection); const nodes = $getBlockElementNodesInSelection(selection);
for (const node of nodes) { for (const node of nodes) {
if (node.getFormatType() === format) { if (node.getFormatType() === format) {
return true; return true;
@ -127,7 +125,7 @@ export function selectionContainsElementFormat(selection: BaseSelection|null, fo
return false; return false;
} }
export function getBlockElementNodesInSelection(selection: BaseSelection|null): ElementNode[] { export function $getBlockElementNodesInSelection(selection: BaseSelection|null): ElementNode[] {
if (!selection) { if (!selection) {
return []; return [];
} }

View File

@ -1,7 +1,7 @@
import {EditorDecorator} from "../framework/decorator"; import {EditorDecorator} from "../framework/decorator";
import {EditorUiContext} from "../framework/core"; import {EditorUiContext} from "../framework/core";
import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block"; import {$openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block";
import {selectionContainsNode, selectSingleNode} from "../../helpers"; import {$selectionContainsNode, $selectSingleNode} from "../../helpers";
import {context} from "esbuild"; import {context} from "esbuild";
import {BaseSelection} from "lexical"; import {BaseSelection} from "lexical";
@ -36,7 +36,7 @@ export class CodeBlockDecorator extends EditorDecorator {
element.addEventListener('click', event => { element.addEventListener('click', event => {
context.editor.update(() => { context.editor.update(() => {
selectSingleNode(this.getNode()); $selectSingleNode(this.getNode());
}) })
}); });
@ -47,7 +47,7 @@ export class CodeBlockDecorator extends EditorDecorator {
}); });
const selectionChange = (selection: BaseSelection|null): void => { const selectionChange = (selection: BaseSelection|null): void => {
element.classList.toggle('selected', selectionContainsNode(selection, codeNode)); element.classList.toggle('selected', $selectionContainsNode(selection, codeNode));
}; };
context.manager.onSelectionChange(selectionChange); context.manager.onSelectionChange(selectionChange);
this.onDestroy(() => { this.onDestroy(() => {

View File

@ -1,5 +1,5 @@
import {EditorDecorator} from "../framework/decorator"; import {EditorDecorator} from "../framework/decorator";
import {el, selectSingleNode} from "../../helpers"; import {el, $selectSingleNode} from "../../helpers";
import {$createNodeSelection, $setSelection} from "lexical"; import {$createNodeSelection, $setSelection} from "lexical";
import {EditorUiContext} from "../framework/core"; import {EditorUiContext} from "../framework/core";
import {ImageNode} from "../../nodes/image"; import {ImageNode} from "../../nodes/image";
@ -41,7 +41,7 @@ export class ImageDecorator extends EditorDecorator {
tracker = this.setupTracker(decorateEl, context); tracker = this.setupTracker(decorateEl, context);
context.editor.update(() => { context.editor.update(() => {
selectSingleNode(this.getNode()); $selectSingleNode(this.getNode());
}); });
}; };

View File

@ -21,11 +21,11 @@ import {
UNDO_COMMAND UNDO_COMMAND
} from "lexical"; } from "lexical";
import { import {
getBlockElementNodesInSelection, $getBlockElementNodesInSelection,
getNodeFromSelection, insertNewBlockNodeAtSelection, selectionContainsElementFormat, $getNodeFromSelection, $insertNewBlockNodeAtSelection, $selectionContainsElementFormat,
selectionContainsNodeType, $selectionContainsNodeType,
selectionContainsTextFormat, $selectionContainsTextFormat,
toggleSelectionBlockNodeType $toggleSelectionBlockNodeType
} from "../../helpers"; } from "../../helpers";
import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout"; import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout";
import { import {
@ -116,14 +116,15 @@ function buildCalloutButton(category: CalloutCategory, name: string): EditorButt
return { return {
label: `${name} Callout`, label: `${name} Callout`,
action(context: EditorUiContext) { action(context: EditorUiContext) {
toggleSelectionBlockNodeType( context.editor.update(() => {
context.editor, $toggleSelectionBlockNodeType(
(node) => $isCalloutNodeOfCategory(node, category), (node) => $isCalloutNodeOfCategory(node, category),
() => $createCalloutNode(category), () => $createCalloutNode(category),
) )
});
}, },
isActive(selection: BaseSelection|null): boolean { isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category)); return $selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category));
} }
}; };
} }
@ -141,14 +142,15 @@ function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefin
return { return {
label: name, label: name,
action(context: EditorUiContext) { action(context: EditorUiContext) {
toggleSelectionBlockNodeType( context.editor.update(() => {
context.editor, $toggleSelectionBlockNodeType(
(node) => isHeaderNodeOfTag(node, tag), (node) => isHeaderNodeOfTag(node, tag),
() => $createHeadingNode(tag), () => $createHeadingNode(tag),
) )
});
}, },
isActive(selection: BaseSelection|null): boolean { isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag)); return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
} }
}; };
} }
@ -161,20 +163,24 @@ export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header')
export const blockquote: EditorButtonDefinition = { export const blockquote: EditorButtonDefinition = {
label: 'Blockquote', label: 'Blockquote',
action(context: EditorUiContext) { action(context: EditorUiContext) {
toggleSelectionBlockNodeType(context.editor, $isQuoteNode, $createQuoteNode); context.editor.update(() => {
$toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
});
}, },
isActive(selection: BaseSelection|null): boolean { isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, $isQuoteNode); return $selectionContainsNodeType(selection, $isQuoteNode);
} }
}; };
export const paragraph: EditorButtonDefinition = { export const paragraph: EditorButtonDefinition = {
label: 'Paragraph', label: 'Paragraph',
action(context: EditorUiContext) { action(context: EditorUiContext) {
toggleSelectionBlockNodeType(context.editor, $isParagraphNode, $createParagraphNode); context.editor.update(() => {
$toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
});
}, },
isActive(selection: BaseSelection|null): boolean { isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, $isParagraphNode); return $selectionContainsNodeType(selection, $isParagraphNode);
} }
} }
@ -186,7 +192,7 @@ function buildFormatButton(label: string, format: TextFormatType, icon: string):
context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format); context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
}, },
isActive(selection: BaseSelection|null): boolean { isActive(selection: BaseSelection|null): boolean {
return selectionContainsTextFormat(selection, format); return $selectionContainsTextFormat(selection, format);
} }
}; };
} }
@ -222,7 +228,7 @@ export const clearFormating: EditorButtonDefinition = {
function setAlignmentForSection(alignment: ElementFormatType): void { function setAlignmentForSection(alignment: ElementFormatType): void {
const selection = $getSelection(); const selection = $getSelection();
const elements = getBlockElementNodesInSelection(selection); const elements = $getBlockElementNodesInSelection(selection);
for (const node of elements) { for (const node of elements) {
node.setFormat(alignment); node.setFormat(alignment);
} }
@ -235,7 +241,7 @@ export const alignLeft: EditorButtonDefinition = {
context.editor.update(() => setAlignmentForSection('left')); context.editor.update(() => setAlignmentForSection('left'));
}, },
isActive(selection: BaseSelection|null) { isActive(selection: BaseSelection|null) {
return selectionContainsElementFormat(selection, 'left'); return $selectionContainsElementFormat(selection, 'left');
} }
}; };
@ -246,7 +252,7 @@ export const alignCenter: EditorButtonDefinition = {
context.editor.update(() => setAlignmentForSection('center')); context.editor.update(() => setAlignmentForSection('center'));
}, },
isActive(selection: BaseSelection|null) { isActive(selection: BaseSelection|null) {
return selectionContainsElementFormat(selection, 'center'); return $selectionContainsElementFormat(selection, 'center');
} }
}; };
@ -257,7 +263,7 @@ export const alignRight: EditorButtonDefinition = {
context.editor.update(() => setAlignmentForSection('right')); context.editor.update(() => setAlignmentForSection('right'));
}, },
isActive(selection: BaseSelection|null) { isActive(selection: BaseSelection|null) {
return selectionContainsElementFormat(selection, 'right'); return $selectionContainsElementFormat(selection, 'right');
} }
}; };
@ -268,7 +274,7 @@ export const alignJustify: EditorButtonDefinition = {
context.editor.update(() => setAlignmentForSection('justify')); context.editor.update(() => setAlignmentForSection('justify'));
}, },
isActive(selection: BaseSelection|null) { isActive(selection: BaseSelection|null) {
return selectionContainsElementFormat(selection, 'justify'); return $selectionContainsElementFormat(selection, 'justify');
} }
}; };
@ -288,7 +294,7 @@ function buildListButton(label: string, type: ListType, icon: string): EditorBut
}); });
}, },
isActive(selection: BaseSelection|null): boolean { isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => { return $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
return $isListNode(node) && (node as ListNode).getListType() === type; return $isListNode(node) && (node as ListNode).getListType() === type;
}); });
} }
@ -307,7 +313,7 @@ export const link: EditorButtonDefinition = {
const linkModal = context.manager.createModal('link'); const linkModal = context.manager.createModal('link');
context.editor.getEditorState().read(() => { context.editor.getEditorState().read(() => {
const selection = $getSelection(); const selection = $getSelection();
const selectedLink = getNodeFromSelection(selection, $isLinkNode) as LinkNode|null; const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
let formDefaults = {}; let formDefaults = {};
if (selectedLink) { if (selectedLink) {
@ -329,7 +335,7 @@ export const link: EditorButtonDefinition = {
}); });
}, },
isActive(selection: BaseSelection|null): boolean { isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, $isLinkNode); return $selectionContainsNodeType(selection, $isLinkNode);
} }
}; };
@ -339,7 +345,7 @@ export const unlink: EditorButtonDefinition = {
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.update(() => { context.editor.update(() => {
const selection = context.lastSelection; const selection = context.lastSelection;
const selectedLink = getNodeFromSelection(selection, $isLinkNode) as LinkNode|null; const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
const selectionPoints = selection?.getStartEndPoints(); const selectionPoints = selection?.getStartEndPoints();
if (selectedLink) { if (selectedLink) {
@ -369,7 +375,7 @@ export const image: EditorButtonDefinition = {
action(context: EditorUiContext) { action(context: EditorUiContext) {
const imageModal = context.manager.createModal('image'); const imageModal = context.manager.createModal('image');
const selection = context.lastSelection; const selection = context.lastSelection;
const selectedImage = getNodeFromSelection(selection, $isImageNode) as ImageNode|null; const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode|null;
context.editor.getEditorState().read(() => { context.editor.getEditorState().read(() => {
let formDefaults = {}; let formDefaults = {};
@ -392,7 +398,7 @@ export const image: EditorButtonDefinition = {
}); });
}, },
isActive(selection: BaseSelection|null): boolean { isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, $isImageNode); return $selectionContainsNodeType(selection, $isImageNode);
} }
}; };
@ -401,11 +407,11 @@ export const horizontalRule: EditorButtonDefinition = {
icon: horizontalRuleIcon, icon: horizontalRuleIcon,
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.update(() => { context.editor.update(() => {
insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false); $insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false);
}); });
}, },
isActive(selection: BaseSelection|null): boolean { isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, $isHorizontalRuleNode); return $selectionContainsNodeType(selection, $isHorizontalRuleNode);
} }
}; };
@ -415,12 +421,12 @@ export const codeBlock: EditorButtonDefinition = {
action(context: EditorUiContext) { action(context: EditorUiContext) {
context.editor.getEditorState().read(() => { context.editor.getEditorState().read(() => {
const selection = $getSelection(); const selection = $getSelection();
const codeBlock = getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode|null); const codeBlock = $getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode|null);
if (codeBlock === null) { if (codeBlock === null) {
context.editor.update(() => { context.editor.update(() => {
const codeBlock = $createCodeBlockNode(); const codeBlock = $createCodeBlockNode();
codeBlock.setCode(selection?.getTextContent() || ''); codeBlock.setCode(selection?.getTextContent() || '');
insertNewBlockNodeAtSelection(codeBlock, true); $insertNewBlockNodeAtSelection(codeBlock, true);
$openCodeEditorForNode(context.editor, codeBlock); $openCodeEditorForNode(context.editor, codeBlock);
codeBlock.selectStart(); codeBlock.selectStart();
}); });
@ -430,7 +436,7 @@ export const codeBlock: EditorButtonDefinition = {
}); });
}, },
isActive(selection: BaseSelection|null): boolean { isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, $isCodeBlockNode); return $selectionContainsNodeType(selection, $isCodeBlockNode);
} }
}; };
@ -463,7 +469,7 @@ export const details: EditorButtonDefinition = {
}); });
}, },
isActive(selection: BaseSelection|null): boolean { isActive(selection: BaseSelection|null): boolean {
return selectionContainsNodeType(selection, $isDetailsNode); return $selectionContainsNodeType(selection, $isDetailsNode);
} }
} }

View File

@ -1,4 +1,4 @@
import {el, insertNewBlockNodeAtSelection} from "../../../helpers"; import {el, $insertNewBlockNodeAtSelection} from "../../../helpers";
import {EditorUiElement} from "../core"; import {EditorUiElement} from "../core";
import {$createTableNodeWithDimensions} from "@lexical/table"; import {$createTableNodeWithDimensions} from "@lexical/table";
import {CustomTableNode} from "../../../nodes/custom-table"; import {CustomTableNode} from "../../../nodes/custom-table";
@ -75,7 +75,7 @@ export class EditorTableCreator extends EditorUiElement {
this.getContext().editor.update(() => { this.getContext().editor.update(() => {
const table = $createTableNodeWithDimensions(rows, columns, false) as CustomTableNode; const table = $createTableNodeWithDimensions(rows, columns, false) as CustomTableNode;
insertNewBlockNodeAtSelection(table); $insertNewBlockNodeAtSelection(table);
}); });
} }
} }