Skip to content

Commit

Permalink
feat: implement useBlockTypeActions
Browse files Browse the repository at this point in the history
  • Loading branch information
2wheeh committed Apr 21, 2024
1 parent 018ac0c commit 2bb3ab8
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 0 deletions.
115 changes: 115 additions & 0 deletions ui/src/hooks/editor/use-block-type-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
'use client';

import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list';
import type { HeadingTagType } from '@lexical/rich-text';
import { $createHeadingNode, $createQuoteNode } from '@lexical/rich-text';
import { $setBlocksType } from '@lexical/selection';
import type { LexicalEditor } from 'lexical';
import {
$createParagraphNode,
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import { useCallback, useEffect, useState } from 'react';

import {
$getBlockAndAnchorFromSelection,
$getBlockType,
} from '@/lib/utils/editor/block-type-actions';

const blockTypeToBlockName = {
paragraph: 'Normal',
quote: 'Quote',
h1: 'Heading 1',
h2: 'Heading 2',
h3: 'Heading 3',
bullet: 'Bulleted List',
number: 'Numbered List',
};

export function useBlockTypeActions({ editor }: { editor: LexicalEditor | undefined | null }) {
const [blockType, setBlockType] = useState<keyof typeof blockTypeToBlockName>('paragraph');

const $updateBlockType = useCallback(() => {
const selection = $getSelection();
const { blockNode, anchorNode } = $getBlockAndAnchorFromSelection(selection) ?? {};
const type = $getBlockType(blockNode, anchorNode);

if (type && type in blockTypeToBlockName) {
setBlockType(type as keyof typeof blockTypeToBlockName);
}
}, []);

useEffect(() => {
return editor?.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
$updateBlockType();
return false;
},
COMMAND_PRIORITY_CRITICAL
);
}, [editor, $updateBlockType]);

useEffect(() => {
return editor?.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
$updateBlockType();
});
});
}, [editor, $updateBlockType]);

const formatParagraph = () => {
editor?.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createParagraphNode());
}
});
};

const formatHeading = (headingType: HeadingTagType) => {
if (blockType !== headingType) {
editor?.update(() => {
const selection = $getSelection();
$setBlocksType(selection, () => $createHeadingNode(headingType));
});
}
};

const formatBulletList = () => {
if (blockType !== 'bullet') {
editor?.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
} else {
formatParagraph();
}
};

const formatNumberedList = () => {
if (blockType !== 'number') {
editor?.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
} else {
formatParagraph();
}
};

const formatQuote = () => {
if (blockType !== 'quote') {
editor?.update(() => {
const selection = $getSelection();
$setBlocksType(selection, () => $createQuoteNode());
});
}
};

return {
blockType,
formatParagraph,
formatQuote,
formatHeading,
formatBulletList,
formatNumberedList,
};
}
45 changes: 45 additions & 0 deletions ui/src/lib/utils/editor/block-type-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { $isListNode, ListNode } from '@lexical/list';
import { $isHeadingNode } from '@lexical/rich-text';
import { $findMatchingParent, $getNearestNodeOfType } from '@lexical/utils';
import type { BaseSelection, LexicalNode } from 'lexical';
import { $isRangeSelection, $isRootOrShadowRoot } from 'lexical';

export function $getBlockAndAnchorFromSelection(selection: BaseSelection | null) {
if (!$isRangeSelection(selection)) {
return null;
}

const anchorNode = selection.anchor.getNode();
let blockNode =
anchorNode.getKey() === 'root'
? anchorNode
: $findMatchingParent(anchorNode, (e) => {
const parent = e.getParent();
return parent !== null && $isRootOrShadowRoot(parent);
});

if (blockNode === null) {
blockNode = anchorNode.getTopLevelElementOrThrow();
}

return { blockNode, anchorNode };
}

export function $getBlockType(blockNode?: LexicalNode, anchorNode?: LexicalNode) {
if (!blockNode || !anchorNode) {
return null;
}

if ($isListNode(blockNode)) {
/*
list <- blockNode
listitem
list <- nestedListNode
listitem <- anchorNode
*/
const nestedListNode = $getNearestNodeOfType(anchorNode, ListNode);
return nestedListNode ? nestedListNode.getListType() : blockNode.getListType();
} else {
return $isHeadingNode(blockNode) ? blockNode.getTag() : blockNode.getType();
}
}

0 comments on commit 2bb3ab8

Please sign in to comment.