From 483d9bf26ca5db0de17aa6fbf775874a596e4782 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 28 May 2024 22:56:58 +0100 Subject: [PATCH] Lexical: Added a range of format buttons --- resources/js/wysiwyg/helpers.ts | 24 +++++- resources/js/wysiwyg/ui/buttons.ts | 129 +++++++++++++++++++++++++++++ resources/js/wysiwyg/ui/index.ts | 35 ++++---- 3 files changed, 170 insertions(+), 18 deletions(-) create mode 100644 resources/js/wysiwyg/ui/buttons.ts diff --git a/resources/js/wysiwyg/helpers.ts b/resources/js/wysiwyg/helpers.ts index 720f3c6d5..737666ffa 100644 --- a/resources/js/wysiwyg/helpers.ts +++ b/resources/js/wysiwyg/helpers.ts @@ -1,7 +1,15 @@ -import {$createParagraphNode, $getSelection, BaseSelection, LexicalEditor} from "lexical"; +import { + $createParagraphNode, + $getSelection, + $isTextNode, + BaseSelection, + ElementFormatType, + LexicalEditor, TextFormatType +} from "lexical"; import {LexicalElementNodeCreator, LexicalNodeMatcher} from "./nodes"; import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils"; import {$setBlocksType} from "@lexical/selection"; +import {TextNodeThemeClasses} from "lexical/LexicalEditor"; export function selectionContainsNodeType(selection: BaseSelection|null, matcher: LexicalNodeMatcher): boolean { if (!selection) { @@ -23,6 +31,20 @@ export function selectionContainsNodeType(selection: BaseSelection|null, matcher return false; } +export function selectionContainsTextFormat(selection: BaseSelection|null, format: TextFormatType): boolean { + if (!selection) { + return false; + } + + for (const node of selection.getNodes()) { + if ($isTextNode(node) && node.hasFormat(format)) { + return true; + } + } + + return false; +} + export function toggleSelectionBlockNodeType(editor: LexicalEditor, matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) { editor.update(() => { const selection = $getSelection(); diff --git a/resources/js/wysiwyg/ui/buttons.ts b/resources/js/wysiwyg/ui/buttons.ts new file mode 100644 index 000000000..cf5660ef0 --- /dev/null +++ b/resources/js/wysiwyg/ui/buttons.ts @@ -0,0 +1,129 @@ +import {EditorButtonDefinition} from "./editor-button"; +import { + $createParagraphNode, + $isParagraphNode, + BaseSelection, FORMAT_TEXT_COMMAND, + LexicalEditor, + LexicalNode, + REDO_COMMAND, TextFormatType, + UNDO_COMMAND +} from "lexical"; +import {selectionContainsNodeType, selectionContainsTextFormat, toggleSelectionBlockNodeType} from "../helpers"; +import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../nodes/callout"; +import { + $createHeadingNode, + $createQuoteNode, + $isHeadingNode, + $isQuoteNode, + HeadingNode, + HeadingTagType +} from "@lexical/rich-text"; + +export const undoButton: EditorButtonDefinition = { + label: 'Undo', + action(editor: LexicalEditor) { + editor.dispatchCommand(UNDO_COMMAND); + }, + isActive(selection: BaseSelection|null): boolean { + return false; + } +} + +export const redoButton: EditorButtonDefinition = { + label: 'Redo', + action(editor: LexicalEditor) { + editor.dispatchCommand(REDO_COMMAND); + }, + isActive(selection: BaseSelection|null): boolean { + return false; + } +} + +function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition { + return { + label: `${name} Callout`, + action(editor: LexicalEditor) { + toggleSelectionBlockNodeType( + editor, + (node) => $isCalloutNodeOfCategory(node, category), + () => $createCalloutNode(category), + ) + }, + isActive(selection: BaseSelection|null): boolean { + return selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category)); + } + }; +} + +export const infoCalloutButton: EditorButtonDefinition = buildCalloutButton('info', 'Info'); +export const dangerCalloutButton: EditorButtonDefinition = buildCalloutButton('danger', 'Danger'); +export const warningCalloutButton: EditorButtonDefinition = buildCalloutButton('warning', 'Warning'); +export const successCalloutButton: EditorButtonDefinition = buildCalloutButton('success', 'Success'); + +const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => { + return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag; +}; + +function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition { + return { + label: name, + action(editor: LexicalEditor) { + toggleSelectionBlockNodeType( + editor, + (node) => isHeaderNodeOfTag(node, tag), + () => $createHeadingNode(tag), + ) + }, + isActive(selection: BaseSelection|null): boolean { + return selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag)); + } + }; +} + +export const h2Button: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header'); +export const h3Button: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header'); +export const h4Button: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header'); +export const h5Button: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header'); + +export const blockquoteButton: EditorButtonDefinition = { + label: 'Blockquote', + action(editor: LexicalEditor) { + toggleSelectionBlockNodeType(editor, $isQuoteNode, $createQuoteNode); + }, + isActive(selection: BaseSelection|null): boolean { + return selectionContainsNodeType(selection, $isQuoteNode); + } +}; + +export const paragraphButton: EditorButtonDefinition = { + label: 'Paragraph', + action(editor: LexicalEditor) { + toggleSelectionBlockNodeType(editor, $isParagraphNode, $createParagraphNode); + }, + isActive(selection: BaseSelection|null): boolean { + return selectionContainsNodeType(selection, $isParagraphNode); + } +} + +function buildFormatButton(label: string, format: TextFormatType): EditorButtonDefinition { + return { + label: label, + action(editor: LexicalEditor) { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, format); + }, + isActive(selection: BaseSelection|null): boolean { + return selectionContainsTextFormat(selection, format); + } + }; +} + +export const boldButton: EditorButtonDefinition = buildFormatButton('Bold', 'bold'); +export const italicButton: EditorButtonDefinition = buildFormatButton('Italic', 'italic'); +export const underlineButton: EditorButtonDefinition = buildFormatButton('Underline', 'underline'); +// Todo - Text color +// Todo - Highlight color +export const strikethroughButton: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough'); +export const superscriptButton: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript'); +export const subscriptButton: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript'); +export const codeButton: EditorButtonDefinition = buildFormatButton('Inline Code', 'code'); +// Todo - Clear formatting \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index 10eaa558f..d04808fae 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -1,30 +1,31 @@ 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"; +import { + blockquoteButton, boldButton, codeButton, + dangerCalloutButton, + h2Button, + h3Button, h4Button, h5Button, + infoCalloutButton, italicButton, paragraphButton, redoButton, strikethroughButton, subscriptButton, + successCalloutButton, superscriptButton, underlineButton, undoButton, + warningCalloutButton +} from "./buttons"; + -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, + undoButton, redoButton, + + infoCalloutButton, warningCalloutButton, dangerCalloutButton, successCalloutButton, + h2Button, h3Button, h4Button, h5Button, + blockquoteButton, paragraphButton, + + boldButton, italicButton, underlineButton, strikethroughButton, + superscriptButton, subscriptButton, codeButton, ]; export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {