diff --git a/ui/src/editor/plugins/index.tsx b/ui/src/editor/plugins/index.tsx index beebe697..b03e13b1 100644 --- a/ui/src/editor/plugins/index.tsx +++ b/ui/src/editor/plugins/index.tsx @@ -9,6 +9,7 @@ import { useEditorRef } from '@/context/editor/editor-ref-context'; import { FixIOSKoreanIssuePlugin } from '@/editor/plugins/fix-ios-korean-issue'; import { HistoryPlugin } from '@/editor/plugins/history'; +import { ListMaxIndentLevelPlugin } from '@/editor/plugins/list-max-indent-level'; import { MarkdownShortcutPlugin } from '@/editor/plugins/markdown-shorcut'; import { MaxLengthPlugin } from '@/editor/plugins/max-length'; @@ -37,6 +38,7 @@ export function Plugins({ maxLength }: { maxLength?: number }) { + {onRef !== undefined && } {maxLength && } diff --git a/ui/src/editor/plugins/list-max-indent-level/index.tsx b/ui/src/editor/plugins/list-max-indent-level/index.tsx new file mode 100644 index 00000000..252e7fc3 --- /dev/null +++ b/ui/src/editor/plugins/list-max-indent-level/index.tsx @@ -0,0 +1,78 @@ +import type { ListNode } from '@lexical/list'; +import { $getListDepth, $isListItemNode, $isListNode } from '@lexical/list'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import type { ElementNode, RangeSelection } from 'lexical'; +import { + $getSelection, + $isElementNode, + $isRangeSelection, + COMMAND_PRIORITY_CRITICAL, + INDENT_CONTENT_COMMAND, +} from 'lexical'; +import { useEffect } from 'react'; + +function getElementNodesInSelection(selection: RangeSelection): Set { + const nodesInSelection = selection.getNodes(); + // TODO: Remove this type assertion once the lexical types are updated. facebook#5710 + const anchor = selection.anchor.getNode() as ElementNode; + const focus = selection.focus.getNode() as ElementNode; + + if (nodesInSelection.length === 0) { + return new Set([anchor.getParentOrThrow(), focus.getParentOrThrow()]); + } + + return new Set(nodesInSelection.map((n) => ($isElementNode(n) ? n : n.getParentOrThrow()))); +} + +function shouldPreventIndent(maxDepth: number) { + const selection = $getSelection(); + + if (!$isRangeSelection(selection)) { + return false; + } + + const elementNodesInSelection: Set = getElementNodesInSelection(selection); + + let totalDepth = 0; + + for (const elementNode of elementNodesInSelection) { + let listNode: ListNode | null = null; + + if ($isListNode(elementNode)) { + listNode = elementNode; + } else if ($isListItemNode(elementNode)) { + // TODO: Remove this type assertion once the lexical types are updated. + const parent = elementNode.getParent() as ElementNode; + + if (!$isListNode(parent)) { + throw new Error( + 'ListMaxIndentLevelPlugin: A ListItemNode must have a ListNode for a parent.' + ); + } + + listNode = parent; + } + + if (listNode !== null) { + totalDepth = Math.max($getListDepth(listNode) + 1, totalDepth); + } + } + + return totalDepth > maxDepth; +} + +export function ListMaxIndentLevelPlugin({ maxDepth = 7 }: { maxDepth?: number }) { + const [editor] = useLexicalComposerContext(); + + useEffect( + () => + editor.registerCommand( + INDENT_CONTENT_COMMAND, + () => shouldPreventIndent(maxDepth), + COMMAND_PRIORITY_CRITICAL + ), + [editor, maxDepth] + ); + + return null; +}