[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:
parent
ab56a57e31
commit
fc9b6bab58
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue