diff --git a/app/soapbox/features/compose/editor/nodes.ts b/app/soapbox/features/compose/editor/nodes.ts index 9614f8ab58..cf263cbb3c 100644 --- a/app/soapbox/features/compose/editor/nodes.ts +++ b/app/soapbox/features/compose/editor/nodes.ts @@ -10,6 +10,7 @@ LICENSE file in the /app/soapbox/features/compose/editor directory. import { CodeHighlightNode, CodeNode } from '@lexical/code'; import { HashtagNode } from '@lexical/hashtag'; import { AutoLinkNode, LinkNode } from '@lexical/link'; +import { ListItemNode, ListNode } from '@lexical/list'; import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode'; import { HeadingNode, QuoteNode } from '@lexical/rich-text'; @@ -24,6 +25,8 @@ const ComposeNodes: Array> = [ CodeHighlightNode, AutoLinkNode, LinkNode, + ListItemNode, + ListNode, HorizontalRuleNode, HashtagNode, MentionNode, diff --git a/app/soapbox/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx b/app/soapbox/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx index b76ffdd213..07db51541c 100644 --- a/app/soapbox/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx +++ b/app/soapbox/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx @@ -7,15 +7,35 @@ This source code is licensed under the MIT license found in the LICENSE file in the /app/soapbox/features/compose/editor directory. */ -import { $isCodeHighlightNode } from '@lexical/code'; +import { $createCodeNode, $isCodeHighlightNode } from '@lexical/code'; import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'; -import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; -import { mergeRegister } from '@lexical/utils'; import { + $isListNode, + INSERT_ORDERED_LIST_COMMAND, + INSERT_UNORDERED_LIST_COMMAND, + ListNode, + REMOVE_LIST_COMMAND, +} from '@lexical/list'; +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import { + $createHeadingNode, + $createQuoteNode, + $isHeadingNode, + HeadingTagType, +} from '@lexical/rich-text'; +import { + $setBlocksType, +} from '@lexical/selection'; +import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from '@lexical/utils'; +import clsx from 'clsx'; +import { + $createParagraphNode, $getSelection, $isRangeSelection, + $isRootOrShadowRoot, $isTextNode, COMMAND_PRIORITY_LOW, + DEPRECATED_$isGridSelection, FORMAT_TEXT_COMMAND, LexicalEditor, SELECTION_CHANGE_COMMAND, @@ -30,9 +50,203 @@ import { getDOMRangeRect } from '../utils/get-dom-range-rect'; import { getSelectedNode } from '../utils/get-selected-node'; import { setFloatingElemPosition } from '../utils/set-floating-elem-position'; +const blockTypeToIcon = { + bullet: require('@tabler/icons/list.svg'), + check: require('@tabler/icons/list-check.svg'), + code: require('@tabler/icons/code.svg'), + h1: require('@tabler/icons/h-1.svg'), + h2: require('@tabler/icons/h-2.svg'), + h3: require('@tabler/icons/h-3.svg'), + h4: require('@tabler/icons/h-4.svg'), + h5: require('@tabler/icons/h-5.svg'), + h6: require('@tabler/icons/h-6.svg'), + number: require('@tabler/icons/list-numbers.svg'), + paragraph: require('@tabler/icons/align-left.svg'), + quote: require('@tabler/icons/blockquote.svg'), +}; + +const blockTypeToBlockName = { + bullet: 'Bulleted List', + check: 'Check List', + code: 'Code Block', + h1: 'Heading 1', + h2: 'Heading 2', + h3: 'Heading 3', + h4: 'Heading 4', + h5: 'Heading 5', + h6: 'Heading 6', + number: 'Numbered List', + paragraph: 'Normal', + quote: 'Quote', +}; + +const BlockTypeDropdown = ({ editor, anchorElem, blockType, icon }: { + editor: LexicalEditor + anchorElem: HTMLElement + blockType: keyof typeof blockTypeToBlockName + icon: string +}) => { + const [showDropDown, setShowDropDown] = useState(false); + + const formatParagraph = () => { + editor.update(() => { + const selection = $getSelection(); + if ( + $isRangeSelection(selection) || + DEPRECATED_$isGridSelection(selection) + ) { + $setBlocksType(selection, () => $createParagraphNode()); + } + }); + }; + + const formatHeading = (headingSize: HeadingTagType) => { + if (blockType !== headingSize) { + editor.update(() => { + const selection = $getSelection(); + if ( + $isRangeSelection(selection) || + DEPRECATED_$isGridSelection(selection) + ) { + $setBlocksType(selection, () => $createHeadingNode(headingSize)); + } + }); + } + }; + + const formatBulletList = () => { + if (blockType !== 'bullet') { + editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined); + } else { + editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined); + } + }; + + const formatNumberedList = () => { + if (blockType !== 'number') { + editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined); + } else { + editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined); + } + }; + + const formatQuote = () => { + if (blockType !== 'quote') { + editor.update(() => { + const selection = $getSelection(); + if ( + $isRangeSelection(selection) || + DEPRECATED_$isGridSelection(selection) + ) { + $setBlocksType(selection, () => $createQuoteNode()); + } + }); + } + }; + + const formatCode = () => { + if (blockType !== 'code') { + editor.update(() => { + let selection = $getSelection(); + + if ( + $isRangeSelection(selection) || + DEPRECATED_$isGridSelection(selection) + ) { + if (selection.isCollapsed()) { + $setBlocksType(selection, () => $createCodeNode()); + } else { + const textContent = selection.getTextContent(); + const codeNode = $createCodeNode(); + selection.insertNodes([codeNode]); + selection = $getSelection(); + if ($isRangeSelection(selection)) + selection.insertRawText(textContent); + } + } + }); + } + }; + + return ( + <> + + + + + + + + + + )} + + + ); +}; + const TextFormatFloatingToolbar = ({ editor, anchorElem, + blockType, isLink, isBold, isItalic, @@ -42,6 +256,7 @@ const TextFormatFloatingToolbar = ({ }: { editor: LexicalEditor anchorElem: HTMLElement + blockType: keyof typeof blockTypeToBlockName isBold: boolean isCode: boolean isItalic: boolean @@ -132,16 +347,12 @@ const TextFormatFloatingToolbar = ({
{editor.isEditable() && ( <> - +