[web] projects: text editor toolbar plugin system cleanup (#296)

Co-authored-by: Yangshun <tay.yang.shun@gmail.com>
Co-authored-by: GitHub Actions <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Nitesh Seram 2024-01-30 05:12:45 +05:30 committed by GitHub
parent ab56a57e31
commit fc9b6bab58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 576 additions and 313 deletions

View File

@ -3,13 +3,8 @@ import {
$getNodeByKey,
$getSelection,
$isRangeSelection,
CAN_REDO_COMMAND,
CAN_UNDO_COMMAND,
COMMAND_PRIORITY_CRITICAL,
FORMAT_TEXT_COMMAND,
REDO_COMMAND,
SELECTION_CHANGE_COMMAND,
UNDO_COMMAND,
} from 'lexical';
import { useCallback, useEffect, useMemo, useState } from 'react';
@ -25,45 +20,20 @@ import {
getCodeLanguages,
getDefaultCodeLanguage,
} from '@lexical/code';
import {
$isListNode,
INSERT_ORDERED_LIST_COMMAND,
INSERT_UNORDERED_LIST_COMMAND,
insertList,
ListNode,
REMOVE_LIST_COMMAND,
removeList,
} from '@lexical/list';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
$createHeadingNode,
$createQuoteNode,
$isHeadingNode,
$isQuoteNode,
} from '@lexical/rich-text';
import { $setBlocksType } from '@lexical/selection';
import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
import { mergeRegister } from '@lexical/utils';
const priority = COMMAND_PRIORITY_CRITICAL;
export default function useRichTextEditorOnClickListener() {
const [editor] = useLexicalComposerContext();
const [isItalic, setIsItalic] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const [canUndo, setCanUndo] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [isOrderedList, setIsOrderedList] = useState(false);
const [isUnorderedList, setIsUnorderedList] = useState(false);
const [isCode, setIsCode] = useState(false);
const [isQuote, setIsQuote] = useState(false);
const [codeLanguage, setCodeLanguage] = useState('');
const [selectedElementKey, setSelectedElementKey] = useState<string | null>(
null,
);
const [headingType, setHeadingType] = useState('normal');
const [specialCase, setSpecialCase] =
useState<RichTextEditorSpecialCase | null>(null);
const updateToolbar = useCallback(() => {
const selection = $getSelection();
@ -79,60 +49,13 @@ export default function useRichTextEditorOnClickListener() {
if (elementDOM !== null) {
setSelectedElementKey(elementKey);
if ($isListNode(element)) {
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
const type = parentList ? parentList.getTag() : element.getTag();
if (type === 'ol') {
setIsOrderedList(true);
}
if (type === 'ul') {
setIsUnorderedList(true);
}
if ($isCodeNode(element)) {
setIsCode(true);
setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage());
} else {
const type = $isHeadingNode(element)
? element.getTag()
: element.getType();
if (type === 'paragraph') {
setHeadingType('normal');
}
if (type === 'h1') {
setHeadingType('h1');
}
if (type === 'h2') {
setHeadingType('h2');
}
if (type === 'h3') {
setHeadingType('h3');
}
setIsOrderedList(false);
setIsUnorderedList(false);
if ($isCodeNode(element)) {
setIsCode(true);
setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage());
} else {
setIsCode(false);
}
if ($isQuoteNode(element)) {
setIsQuote(true);
} else {
setIsQuote(false);
}
setIsCode(false);
}
}
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
if (selection.hasFormat('strikethrough')) {
setSpecialCase('strikethrough' as RichTextEditorSpecialCase);
} else if (selection.hasFormat('subscript')) {
setSpecialCase('subscript' as RichTextEditorSpecialCase);
} else if (selection.hasFormat('superscript')) {
setSpecialCase('superscript' as RichTextEditorSpecialCase);
} else {
setSpecialCase(null);
}
}
}, [editor]);
@ -147,86 +70,12 @@ export default function useRichTextEditorOnClickListener() {
},
priority,
),
editor.registerCommand(
CAN_REDO_COMMAND,
(payload) => {
setCanRedo(payload);
return false;
},
priority,
),
editor.registerCommand(
CAN_UNDO_COMMAND,
(payload) => {
setCanUndo(payload);
return false;
},
priority,
),
editor.registerCommand(
INSERT_ORDERED_LIST_COMMAND,
() => {
insertList(editor, 'number');
return true;
},
priority,
),
editor.registerCommand(
INSERT_UNORDERED_LIST_COMMAND,
() => {
insertList(editor, 'bullet');
return true;
},
priority,
),
editor.registerCommand(
REMOVE_LIST_COMMAND,
() => {
removeList(editor);
setIsOrderedList(false);
setIsUnorderedList(false);
return true;
},
priority,
),
);
}, [editor, updateToolbar]);
const onClick = (
event: RichTextEditorEventType | RichTextEditorSpecialCase,
) => {
if (event === richTextEditorToolbarEventTypes.italic) {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
}
if (event === richTextEditorToolbarEventTypes.undo) {
editor.dispatchCommand(UNDO_COMMAND, undefined);
}
if (event === richTextEditorToolbarEventTypes.redo) {
editor.dispatchCommand(REDO_COMMAND, undefined);
}
if (event === richTextEditorToolbarEventTypes.underline) {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
}
if (event === richTextEditorToolbarEventTypes.ol) {
if (isOrderedList) {
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
} else {
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
}
}
if (event === richTextEditorToolbarEventTypes.ul) {
if (isUnorderedList) {
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
} else {
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
}
}
if (event === richTextEditorToolbarEventTypes.code) {
editor.update(() => {
const selection = $getSelection();
@ -240,76 +89,6 @@ export default function useRichTextEditorOnClickListener() {
}
});
}
if (event === richTextEditorToolbarEventTypes.quote) {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
if (!isQuote) {
$setBlocksType(selection, () => $createQuoteNode());
} else {
$setBlocksType(selection, () => $createParagraphNode());
}
}
});
}
if (event === richTextEditorToolbarEventTypes.normal) {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createParagraphNode());
}
});
}
if (event === richTextEditorToolbarEventTypes.h1) {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createHeadingNode('h1'));
}
});
}
if (event === richTextEditorToolbarEventTypes.h2) {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createHeadingNode('h2'));
}
});
}
if (event === richTextEditorToolbarEventTypes.h3) {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$setBlocksType(selection, () => $createHeadingNode('h3'));
}
});
}
if (event === richTextEditorToolbarEventTypes.strikethrough) {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
if (specialCase === richTextEditorToolbarEventTypes.strikethrough) {
setSpecialCase(null);
}
}
if (event === richTextEditorToolbarEventTypes.subscript) {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
if (specialCase === richTextEditorToolbarEventTypes.subscript) {
setSpecialCase(null);
}
}
if (event === richTextEditorToolbarEventTypes.superscript) {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
if (specialCase === richTextEditorToolbarEventTypes.superscript) {
setSpecialCase(null);
}
}
};
const codeLanguages = useMemo(() => getCodeLanguages(), []);
@ -330,19 +109,10 @@ export default function useRichTextEditorOnClickListener() {
);
return {
canRedo,
canUndo,
codeLanguage,
codeLanguages,
headingType,
isCode,
isItalic,
isOrderedList,
isQuote,
isUnderline,
isUnorderedList,
onClick,
onCodeLanguageSelect,
specialCase,
};
}

View File

@ -5,18 +5,4 @@ export const richTextEditorToolbarEventTypes: Record<
RichTextEditorEventType
> = {
code: 'code',
h1: 'h1',
h2: 'h2',
h3: 'h3',
italic: 'italic',
normal: 'normal',
ol: 'ol',
quote: 'quote',
redo: 'redo',
strikethrough: 'strikethrough',
subscript: 'subscript',
superscript: 'superscript',
ul: 'ul',
underline: 'underline',
undo: 'undo',
};

View File

@ -1,13 +1,52 @@
import {
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
FORMAT_TEXT_COMMAND,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import { useCallback, useEffect, useState } from 'react';
import { RiItalic } from 'react-icons/ri';
import { useIntl } from 'react-intl';
import RichTextEditorToolbarActionNode from '~/components/ui/RichTextEditor/components/RichTextEditorToolbarActionNode';
import useRichTextEditorOnClickListener from '~/components/ui/RichTextEditor/hooks/useRichTextEditorOnClickListener';
import { richTextEditorToolbarEventTypes } from '~/components/ui/RichTextEditor/misc';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { mergeRegister } from '@lexical/utils';
export default function RichTextEditorItalicPlugin() {
const intl = useIntl();
const { isItalic, onClick } = useRichTextEditorOnClickListener();
const [editor] = useLexicalComposerContext();
const [isItalic, setIsItalic] = useState(false);
const $updateState = useCallback(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return;
}
setIsItalic(selection.hasFormat('italic'));
}, []);
useEffect(() => {
return mergeRegister(
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
$updateState();
return false;
},
COMMAND_PRIORITY_CRITICAL,
),
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
$updateState();
});
}),
);
}, [editor, $updateState]);
return (
<RichTextEditorToolbarActionNode
@ -18,7 +57,7 @@ export default function RichTextEditorItalicPlugin() {
description: 'Italic tooltip for Richtext toolbar',
id: '4uXotU',
})}
onClick={() => onClick(richTextEditorToolbarEventTypes.italic)}
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')}
/>
);
}

View File

@ -1,13 +1,111 @@
import {
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
COMMAND_PRIORITY_LOW,
INSERT_PARAGRAPH_COMMAND,
} from 'lexical';
import { useCallback, useEffect, useState } from 'react';
import { RiListOrdered2 } from 'react-icons/ri';
import { useIntl } from 'react-intl';
import RichTextEditorToolbarActionNode from '~/components/ui/RichTextEditor/components/RichTextEditorToolbarActionNode';
import useRichTextEditorOnClickListener from '~/components/ui/RichTextEditor/hooks/useRichTextEditorOnClickListener';
import { richTextEditorToolbarEventTypes } from '~/components/ui/RichTextEditor/misc';
import {
$handleListInsertParagraph,
$isListNode,
INSERT_ORDERED_LIST_COMMAND,
insertList,
ListNode,
REMOVE_LIST_COMMAND,
removeList,
} from '@lexical/list';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
export default function RichTextEditorOrderedListPlugin() {
const intl = useIntl();
const { isOrderedList, onClick } = useRichTextEditorOnClickListener();
const [editor] = useLexicalComposerContext();
const [isOrderedList, setIsOrderedList] = useState(false);
const $updateState = useCallback(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return;
}
const anchorNode = selection.anchor.getNode();
const element =
anchorNode.getKey() === 'root'
? anchorNode
: anchorNode.getTopLevelElementOrThrow();
const elementKey = element.getKey();
const elementDOM = editor.getElementByKey(elementKey);
if (elementDOM === null) {
return;
}
if (!$isListNode(element)) {
setIsOrderedList(false);
return;
}
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
const type = parentList ? parentList.getTag() : element.getTag();
if (type === 'ol') {
setIsOrderedList(true);
} else {
setIsOrderedList(false);
}
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerCommand(
INSERT_ORDERED_LIST_COMMAND,
() => {
insertList(editor, 'number');
return true;
},
COMMAND_PRIORITY_CRITICAL,
),
editor.registerCommand(
REMOVE_LIST_COMMAND,
() => {
removeList(editor);
setIsOrderedList(false);
return true;
},
COMMAND_PRIORITY_CRITICAL,
),
editor.registerCommand(
INSERT_PARAGRAPH_COMMAND,
() => {
return $handleListInsertParagraph();
},
COMMAND_PRIORITY_LOW,
),
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
$updateState();
});
}),
);
}, [editor, $updateState]);
const onFormatOrderedList = () => {
if (isOrderedList) {
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
} else {
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
}
};
return (
<RichTextEditorToolbarActionNode
@ -18,7 +116,7 @@ export default function RichTextEditorOrderedListPlugin() {
description: 'Numbered list tooltip for Richtext toolbar',
id: 'hkKaPn',
})}
onClick={() => onClick(richTextEditorToolbarEventTypes.ol)}
onClick={onFormatOrderedList}
/>
);
}

View File

@ -1,13 +1,84 @@
import {
$createParagraphNode,
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import { useCallback, useEffect, useState } from 'react';
import { RiQuoteText } from 'react-icons/ri';
import { useIntl } from 'react-intl';
import RichTextEditorToolbarActionNode from '~/components/ui/RichTextEditor/components/RichTextEditorToolbarActionNode';
import useRichTextEditorOnClickListener from '~/components/ui/RichTextEditor/hooks/useRichTextEditorOnClickListener';
import { richTextEditorToolbarEventTypes } from '~/components/ui/RichTextEditor/misc';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $createQuoteNode, $isQuoteNode } from '@lexical/rich-text';
import { $setBlocksType } from '@lexical/selection';
import { mergeRegister } from '@lexical/utils';
export default function RichTextEditorQuotePlugin() {
const intl = useIntl();
const { isQuote, onClick } = useRichTextEditorOnClickListener();
const [editor] = useLexicalComposerContext();
const [isQuote, setIsQuote] = useState(false);
const $updateState = useCallback(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return;
}
const anchorNode = selection.anchor.getNode();
const element =
anchorNode.getKey() === 'root'
? anchorNode
: anchorNode.getTopLevelElementOrThrow();
const elementKey = element.getKey();
const elementDOM = editor.getElementByKey(elementKey);
if (elementDOM === null) {
return;
}
if ($isQuoteNode(element)) {
setIsQuote(true);
} else {
setIsQuote(false);
}
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
$updateState();
return false;
},
COMMAND_PRIORITY_CRITICAL,
),
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
$updateState();
});
}),
);
}, [editor, $updateState]);
const onFormatQuote = () => {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
if (!isQuote) {
$setBlocksType(selection, () => $createQuoteNode());
} else {
$setBlocksType(selection, () => $createParagraphNode());
}
}
});
};
return (
<RichTextEditorToolbarActionNode
@ -18,7 +89,7 @@ export default function RichTextEditorQuotePlugin() {
description: 'Quote tooltip for richtext toolbar',
id: 'd4ZiFU',
})}
onClick={() => onClick(richTextEditorToolbarEventTypes.quote)}
onClick={onFormatQuote}
/>
);
}

View File

@ -1,13 +1,32 @@
import {
CAN_REDO_COMMAND,
COMMAND_PRIORITY_CRITICAL,
REDO_COMMAND,
} from 'lexical';
import { useEffect, useState } from 'react';
import { RiArrowGoForwardLine } from 'react-icons/ri';
import { useIntl } from 'react-intl';
import RichTextEditorToolbarActionNode from '~/components/ui/RichTextEditor/components/RichTextEditorToolbarActionNode';
import useRichTextEditorOnClickListener from '~/components/ui/RichTextEditor/hooks/useRichTextEditorOnClickListener';
import { richTextEditorToolbarEventTypes } from '~/components/ui/RichTextEditor/misc';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
export default function RichTextEditorRedoPlugin() {
const intl = useIntl();
const { canRedo, onClick } = useRichTextEditorOnClickListener();
const [editor] = useLexicalComposerContext();
const [canRedo, setCanRedo] = useState(false);
useEffect(() => {
return editor.registerCommand(
CAN_REDO_COMMAND,
(payload) => {
setCanRedo(payload);
return false;
},
COMMAND_PRIORITY_CRITICAL,
);
}, [editor]);
return (
<RichTextEditorToolbarActionNode
@ -18,7 +37,7 @@ export default function RichTextEditorRedoPlugin() {
description: 'Redo tooltip for Richtext toolbar',
id: 'u2wpqq',
})}
onClick={() => onClick(richTextEditorToolbarEventTypes.redo)}
onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
/>
);
}

View File

@ -1,3 +1,11 @@
import {
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
FORMAT_TEXT_COMMAND,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import { useCallback, useEffect, useState } from 'react';
import {
RiFontSize,
RiStrikethrough,
@ -7,13 +15,18 @@ import {
import { useIntl } from 'react-intl';
import DropdownMenu from '~/components/ui/DropdownMenu';
import useRichTextEditorOnClickListener from '~/components/ui/RichTextEditor/hooks/useRichTextEditorOnClickListener';
import type { RichTextEditorSpecialCase } from '~/components/ui/RichTextEditor/types';
import Tooltip from '~/components/ui/Tooltip';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { mergeRegister } from '@lexical/utils';
export default function RichTextEditorSpecialCasePlugin() {
const intl = useIntl();
const { specialCase, onClick } = useRichTextEditorOnClickListener();
const [editor] = useLexicalComposerContext();
const [isStrikethrough, setIsStrikethrough] = useState(false);
const [isSubscript, setIsSubscript] = useState(false);
const [isSuperscript, setIsSuperscript] = useState(false);
const caseOptions: Array<{
icon: (props: React.ComponentProps<'svg'>) => JSX.Element;
label: string;
@ -48,14 +61,58 @@ export default function RichTextEditorSpecialCasePlugin() {
},
];
const selectedValue = caseOptions.find((type) => type.value === specialCase);
const $updateState = useCallback(() => {
const selection = $getSelection();
const menu = (
if (!$isRangeSelection(selection)) {
return;
}
setIsStrikethrough(selection.hasFormat('strikethrough'));
setIsSubscript(selection.hasFormat('subscript'));
setIsSuperscript(selection.hasFormat('superscript'));
}, []);
useEffect(() => {
return mergeRegister(
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
$updateState();
return false;
},
COMMAND_PRIORITY_CRITICAL,
),
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
$updateState();
});
}),
);
}, [editor, $updateState]);
const isSelectedValue = (value: RichTextEditorSpecialCase) => {
if (value === 'strikethrough') {
return isStrikethrough;
}
if (value === 'subscript') {
return isSubscript;
}
return isSuperscript;
};
return (
<DropdownMenu
align="end"
icon={selectedValue?.icon ?? RiFontSize}
icon={RiFontSize}
isLabelHidden={true}
label={selectedValue?.label ?? ''}
label={intl.formatMessage({
defaultMessage: 'Special case for richtext editor',
description: 'Special case action for richtext editor toolbar',
id: '/CfHhU',
})}
labelColor="inherit"
size="xs"
variant="flat">
@ -63,19 +120,11 @@ export default function RichTextEditorSpecialCasePlugin() {
<DropdownMenu.Item
key={value}
icon={icon}
isSelected={specialCase === value}
isSelected={isSelectedValue(value)}
label={label}
onClick={() => onClick(value)}
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, value)}
/>
))}
</DropdownMenu>
);
return specialCase == null ? (
menu
) : (
<Tooltip label={selectedValue?.label} position="above">
{menu}
</Tooltip>
);
}

View File

@ -1,13 +1,27 @@
import {
$createParagraphNode,
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import { useCallback, useEffect, useState } from 'react';
import { RiFontSize2, RiH1, RiH2, RiH3 } from 'react-icons/ri';
import { useIntl } from 'react-intl';
import DropdownMenu from '~/components/ui/DropdownMenu';
import useRichTextEditorOnClickListener from '~/components/ui/RichTextEditor/hooks/useRichTextEditorOnClickListener';
import type { RichTextEditorHeadingType } from '~/components/ui/RichTextEditor/types';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $createHeadingNode, $isHeadingNode } from '@lexical/rich-text';
import { $setBlocksType } from '@lexical/selection';
import { mergeRegister } from '@lexical/utils';
export default function RichTextEditorTextTypePlugin() {
const intl = useIntl();
const { headingType, onClick } = useRichTextEditorOnClickListener();
const [editor] = useLexicalComposerContext();
const [headingType, setHeadingType] = useState('normal');
const typeOptions: Array<{
icon: (props: React.ComponentProps<'svg'>) => JSX.Element;
label: string;
@ -51,8 +65,76 @@ export default function RichTextEditorTextTypePlugin() {
},
];
const $updateState = useCallback(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return;
}
const anchorNode = selection.anchor.getNode();
const element =
anchorNode.getKey() === 'root'
? anchorNode
: anchorNode.getTopLevelElementOrThrow();
const elementKey = element.getKey();
const elementDOM = editor.getElementByKey(elementKey);
if (elementDOM === null) {
return;
}
const type = $isHeadingNode(element) ? element.getTag() : element.getType();
if (type === 'paragraph') {
setHeadingType('normal');
}
if (type === 'h1') {
setHeadingType('h1');
}
if (type === 'h2') {
setHeadingType('h2');
}
if (type === 'h3') {
setHeadingType('h3');
}
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
$updateState();
return false;
},
COMMAND_PRIORITY_CRITICAL,
),
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
$updateState();
});
}),
);
}, [editor, $updateState]);
const selectedValue = typeOptions.find((type) => type.value === headingType);
const onFormatHeading = (type: RichTextEditorHeadingType) => {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
if (type === 'normal') {
$setBlocksType(selection, () => $createParagraphNode());
} else {
$setBlocksType(selection, () => $createHeadingNode(type));
}
}
});
};
return (
<DropdownMenu
align="end"
@ -74,7 +156,7 @@ export default function RichTextEditorTextTypePlugin() {
icon={icon}
isSelected={headingType === value}
label={label}
onClick={() => onClick(value)}
onClick={() => onFormatHeading(value)}
/>
))}
</DropdownMenu>

View File

@ -1,13 +1,52 @@
import {
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
FORMAT_TEXT_COMMAND,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import { useCallback, useEffect, useState } from 'react';
import { RiUnderline } from 'react-icons/ri';
import { useIntl } from 'react-intl';
import RichTextEditorToolbarActionNode from '~/components/ui/RichTextEditor/components/RichTextEditorToolbarActionNode';
import useRichTextEditorOnClickListener from '~/components/ui/RichTextEditor/hooks/useRichTextEditorOnClickListener';
import { richTextEditorToolbarEventTypes } from '~/components/ui/RichTextEditor/misc';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { mergeRegister } from '@lexical/utils';
export default function RichTextEditorUnderlinePlugin() {
const intl = useIntl();
const { isUnderline, onClick } = useRichTextEditorOnClickListener();
const [editor] = useLexicalComposerContext();
const [isUnderline, setIsUnderline] = useState(false);
const $updateState = useCallback(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return;
}
setIsUnderline(selection.hasFormat('underline'));
}, []);
useEffect(() => {
return mergeRegister(
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
$updateState();
return false;
},
COMMAND_PRIORITY_CRITICAL,
),
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
$updateState();
});
}),
);
}, [editor, $updateState]);
return (
<RichTextEditorToolbarActionNode
@ -18,7 +57,7 @@ export default function RichTextEditorUnderlinePlugin() {
description: 'Underline tooltip for Richtext toolbar',
id: '7tztU9',
})}
onClick={() => onClick(richTextEditorToolbarEventTypes.underline)}
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')}
/>
);
}

View File

@ -1,13 +1,32 @@
import {
CAN_UNDO_COMMAND,
COMMAND_PRIORITY_CRITICAL,
UNDO_COMMAND,
} from 'lexical';
import { useEffect, useState } from 'react';
import { RiArrowGoBackLine } from 'react-icons/ri';
import { useIntl } from 'react-intl';
import RichTextEditorToolbarActionNode from '~/components/ui/RichTextEditor/components/RichTextEditorToolbarActionNode';
import useRichTextEditorOnClickListener from '~/components/ui/RichTextEditor/hooks/useRichTextEditorOnClickListener';
import { richTextEditorToolbarEventTypes } from '~/components/ui/RichTextEditor/misc';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
export default function RichTextEditorUndoPlugin() {
const intl = useIntl();
const { canUndo, onClick } = useRichTextEditorOnClickListener();
const [editor] = useLexicalComposerContext();
const [canUndo, setCanUndo] = useState(false);
useEffect(() => {
return editor.registerCommand(
CAN_UNDO_COMMAND,
(payload) => {
setCanUndo(payload);
return false;
},
COMMAND_PRIORITY_CRITICAL,
);
}, [editor]);
return (
<RichTextEditorToolbarActionNode
@ -18,7 +37,7 @@ export default function RichTextEditorUndoPlugin() {
description: 'Undo tooltip for richtext toolbar',
id: 'KQfZUe',
})}
onClick={() => onClick(richTextEditorToolbarEventTypes.undo)}
onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
/>
);
}

View File

@ -1,13 +1,111 @@
import {
$getSelection,
$isRangeSelection,
COMMAND_PRIORITY_CRITICAL,
COMMAND_PRIORITY_LOW,
INSERT_PARAGRAPH_COMMAND,
} from 'lexical';
import { useCallback, useEffect, useState } from 'react';
import { RiListUnordered } from 'react-icons/ri';
import { useIntl } from 'react-intl';
import RichTextEditorToolbarActionNode from '~/components/ui/RichTextEditor/components/RichTextEditorToolbarActionNode';
import useRichTextEditorOnClickListener from '~/components/ui/RichTextEditor/hooks/useRichTextEditorOnClickListener';
import { richTextEditorToolbarEventTypes } from '~/components/ui/RichTextEditor/misc';
import {
$handleListInsertParagraph,
$isListNode,
INSERT_UNORDERED_LIST_COMMAND,
insertList,
ListNode,
REMOVE_LIST_COMMAND,
removeList,
} from '@lexical/list';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
export default function RichTextEditorUnorderedListPlugin() {
const intl = useIntl();
const { isUnorderedList, onClick } = useRichTextEditorOnClickListener();
const [editor] = useLexicalComposerContext();
const [isUnorderedList, setIsUnorderedList] = useState(false);
const $updateState = useCallback(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection)) {
return;
}
const anchorNode = selection.anchor.getNode();
const element =
anchorNode.getKey() === 'root'
? anchorNode
: anchorNode.getTopLevelElementOrThrow();
const elementKey = element.getKey();
const elementDOM = editor.getElementByKey(elementKey);
if (elementDOM === null) {
return;
}
if (!$isListNode(element)) {
setIsUnorderedList(false);
return;
}
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
const type = parentList ? parentList.getTag() : element.getTag();
if (type === 'ul') {
setIsUnorderedList(true);
} else {
setIsUnorderedList(false);
}
}, [editor]);
useEffect(() => {
return mergeRegister(
editor.registerCommand(
INSERT_UNORDERED_LIST_COMMAND,
() => {
insertList(editor, 'bullet');
return true;
},
COMMAND_PRIORITY_CRITICAL,
),
editor.registerCommand(
REMOVE_LIST_COMMAND,
() => {
removeList(editor);
setIsUnorderedList(false);
return true;
},
COMMAND_PRIORITY_CRITICAL,
),
editor.registerCommand(
INSERT_PARAGRAPH_COMMAND,
() => {
return $handleListInsertParagraph();
},
COMMAND_PRIORITY_LOW,
),
editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
$updateState();
});
}),
);
}, [editor, $updateState]);
const onFormatUnorderedList = () => {
if (isUnorderedList) {
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
} else {
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
}
};
return (
<RichTextEditorToolbarActionNode
@ -18,7 +116,7 @@ export default function RichTextEditorUnorderedListPlugin() {
description: 'Bullet list tooltip for Richtext toolbar',
id: 'KbLzJO',
})}
onClick={() => onClick(richTextEditorToolbarEventTypes.ul)}
onClick={onFormatUnorderedList}
/>
);
}

View File

@ -1,19 +1,4 @@
export type RichTextEditorEventType =
| 'code'
| 'h1'
| 'h2'
| 'h3'
| 'italic'
| 'normal'
| 'ol'
| 'quote'
| 'redo'
| 'strikethrough'
| 'subscript'
| 'superscript'
| 'ul'
| 'underline'
| 'undo';
export type RichTextEditorEventType = 'code';
export type RichTextEditorHeadingType = 'h1' | 'h2' | 'h3' | 'normal';

View File

@ -135,6 +135,10 @@
"defaultMessage": "Challenge brief",
"description": "Projects challenge submission hero card title"
},
"/CfHhU": {
"defaultMessage": "Special case for richtext editor",
"description": "Special case action for richtext editor toolbar"
},
"/F80kH": {
"defaultMessage": "Read our <link>Front End Interview Guidebook</link>",
"description": "Link to front end interview guidebook"
@ -4283,6 +4287,10 @@
"defaultMessage": "Copy line down",
"description": "Text describing command to copy lines down in the coding workspace shortcuts menu"
},
"d4ZiFU": {
"defaultMessage": "Quote",
"description": "Quote tooltip for richtext toolbar"
},
"d7gVP2": {
"defaultMessage": "Chennai, India",
"description": "Chennai in India"